diff --git a/packages/frontend/@n8n/chat/src/components/MessagesList.vue b/packages/frontend/@n8n/chat/src/components/MessagesList.vue index a0ca0becdf..ba941f0625 100644 --- a/packages/frontend/@n8n/chat/src/components/MessagesList.vue +++ b/packages/frontend/@n8n/chat/src/components/MessagesList.vue @@ -34,7 +34,7 @@ watch( v-if="emptyText && initialMessages.length === 0 && messages.length === 0" class="empty-container" > -
+
{{ emptyText }} diff --git a/packages/frontend/editor-ui/src/__tests__/mocks.ts b/packages/frontend/editor-ui/src/__tests__/mocks.ts index 8ef9ad3637..373c3fdc30 100644 --- a/packages/frontend/editor-ui/src/__tests__/mocks.ts +++ b/packages/frontend/editor-ui/src/__tests__/mocks.ts @@ -58,6 +58,7 @@ export const mockNodeTypeDescription = ({ outputs = [NodeConnectionTypes.Main], codex = undefined, properties = [], + group, }: { name?: INodeTypeDescription['name']; icon?: INodeTypeDescription['icon']; @@ -67,6 +68,7 @@ export const mockNodeTypeDescription = ({ outputs?: INodeTypeDescription['outputs']; codex?: INodeTypeDescription['codex']; properties?: INodeTypeDescription['properties']; + group?: INodeTypeDescription['group']; } = {}) => mock({ name, @@ -80,7 +82,7 @@ export const mockNodeTypeDescription = ({ defaultVersion: Array.isArray(version) ? version[version.length - 1] : version, properties: properties as [], maxNodes: Infinity, - group: EXECUTABLE_TRIGGER_NODE_TYPES.includes(name) ? ['trigger'] : [], + group: (group ?? EXECUTABLE_TRIGGER_NODE_TYPES.includes(name)) ? ['trigger'] : [], inputs, outputs, codex, diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.test.ts b/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.test.ts index 5554006bf8..689af763c2 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.test.ts +++ b/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.test.ts @@ -24,6 +24,7 @@ import { useToast } from '@/composables/useToast'; import type { IExecutionResponse, INodeUi } from '@/Interface'; import type { ChatMessage } from '@n8n/chat/types'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; +import { LOGS_PANEL_STATE } from './types/logs'; vi.mock('@/composables/useToast', () => { const showMessage = vi.fn(); @@ -174,7 +175,7 @@ describe('CanvasChat', () => { return matchedNode; }); - workflowsStore.chatPanelState = 'attached'; + workflowsStore.chatPanelState = LOGS_PANEL_STATE.ATTACHED; workflowsStore.isLogsPanelOpen = true; workflowsStore.getWorkflowExecution = mockWorkflowExecution as unknown as IExecutionResponse; workflowsStore.getPastChatMessages = ['Previous message 1', 'Previous message 2']; @@ -197,7 +198,7 @@ describe('CanvasChat', () => { }); it('should not render chat when panel is closed', async () => { - workflowsStore.chatPanelState = 'closed'; + workflowsStore.chatPanelState = LOGS_PANEL_STATE.CLOSED; const { queryByTestId } = renderComponent(); await waitFor(() => { expect(queryByTestId('canvas-chat')).not.toBeInTheDocument(); @@ -387,7 +388,7 @@ describe('CanvasChat', () => { isLoading: computed(() => false), }); - workflowsStore.chatPanelState = 'attached'; + workflowsStore.chatPanelState = LOGS_PANEL_STATE.ATTACHED; workflowsStore.allowFileUploads = true; }); @@ -549,7 +550,7 @@ describe('CanvasChat', () => { }); // Close chat panel - workflowsStore.chatPanelState = 'closed'; + workflowsStore.chatPanelState = LOGS_PANEL_STATE.CLOSED; await waitFor(() => { expect(canvasStore.setPanelHeight).toHaveBeenCalledWith(0); }); @@ -559,14 +560,14 @@ describe('CanvasChat', () => { const { unmount, rerender } = renderComponent(); // Set initial state - workflowsStore.chatPanelState = 'attached'; + workflowsStore.chatPanelState = LOGS_PANEL_STATE.ATTACHED; workflowsStore.isLogsPanelOpen = true; // Unmount and remount unmount(); await rerender({}); - expect(workflowsStore.chatPanelState).toBe('attached'); + expect(workflowsStore.chatPanelState).toBe(LOGS_PANEL_STATE.ATTACHED); expect(workflowsStore.isLogsPanelOpen).toBe(true); }); }); @@ -592,10 +593,10 @@ describe('CanvasChat', () => { getChatMessages: getChatMessagesSpy, }); - workflowsStore.chatPanelState = 'closed'; + workflowsStore.chatPanelState = LOGS_PANEL_STATE.CLOSED; const { rerender } = renderComponent(); - workflowsStore.chatPanelState = 'attached'; + workflowsStore.chatPanelState = LOGS_PANEL_STATE.ATTACHED; await rerender({}); expect(getChatMessagesSpy).toHaveBeenCalled(); diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.vue b/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.vue index 0b662ba9fb..5d9f29ea2b 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.vue +++ b/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.vue @@ -15,6 +15,7 @@ import { usePiPWindow } from '@/components/CanvasChat/composables/usePiPWindow'; import { N8nResizeWrapper } from '@n8n/design-system'; import { useTelemetry } from '@/composables/useTelemetry'; import { useChatState } from '@/components/CanvasChat/composables/useChatState'; +import { LOGS_PANEL_STATE } from '@/components/CanvasChat/types/logs'; const workflowsStore = useWorkflowsStore(); const canvasStore = useCanvasStore(); @@ -49,14 +50,14 @@ const { canPopOut, isPoppedOut, pipWindow } = usePiPWindow({ initialWidth: window.document.body.offsetWidth * 0.8, container: pipContainer, content: pipContent, - shouldPopOut: computed(() => chatPanelState.value === 'floating'), + shouldPopOut: computed(() => chatPanelState.value === LOGS_PANEL_STATE.FLOATING), onRequestClose: () => { - if (chatPanelState.value === 'closed') { + if (chatPanelState.value === LOGS_PANEL_STATE.CLOSED) { return; } telemetry.track('User toggled log view', { new_state: 'attached' }); - workflowsStore.setPanelState('attached'); + workflowsStore.setPanelState(LOGS_PANEL_STATE.ATTACHED); }, }); @@ -79,17 +80,17 @@ defineExpose({ }); const closePanel = () => { - workflowsStore.setPanelState('closed'); + workflowsStore.setPanelState(LOGS_PANEL_STATE.CLOSED); }; function onPopOut() { telemetry.track('User toggled log view', { new_state: 'floating' }); - workflowsStore.setPanelState('floating'); + workflowsStore.setPanelState(LOGS_PANEL_STATE.FLOATING); } // Watchers watchEffect(() => { - canvasStore.setPanelHeight(chatPanelState.value === 'attached' ? height.value : 0); + canvasStore.setPanelHeight(chatPanelState.value === LOGS_PANEL_STATE.ATTACHED ? height.value : 0); }); @@ -98,15 +99,15 @@ watchEffect(() => {
-
+
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/composables/useChatState.ts b/packages/frontend/editor-ui/src/components/CanvasChat/composables/useChatState.ts index a353ea11d6..dce6a9d810 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/composables/useChatState.ts +++ b/packages/frontend/editor-ui/src/components/CanvasChat/composables/useChatState.ts @@ -17,6 +17,7 @@ import { v4 as uuid } from 'uuid'; import type { Ref } from 'vue'; import { computed, provide, ref, watch } from 'vue'; import { useRouter } from 'vue-router'; +import { LOGS_PANEL_STATE } from '../types/logs'; interface ChatState { currentSessionId: Ref; @@ -129,7 +130,7 @@ export function useChatState(isDisabled: Ref, onWindowResize: () => voi watch( () => chatPanelState.value, (state) => { - if (state !== 'closed') { + if (state !== LOGS_PANEL_STATE.CLOSED) { setChatTriggerNode(); setConnectedNode(); diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts index 48c2675d8b..c6d8bc9ee5 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts +++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts @@ -1,5 +1,5 @@ import { renderComponent } from '@/__tests__/render'; -import { fireEvent, waitFor } from '@testing-library/vue'; +import { fireEvent, within } from '@testing-library/vue'; import { mockedStore } from '@/__tests__/utils'; import LogsPanel from '@/components/CanvasChat/future/LogsPanel.vue'; import { useSettingsStore } from '@/stores/settings.store'; @@ -7,14 +7,15 @@ import { createTestingPinia, type TestingPinia } from '@pinia/testing'; import { setActivePinia } from 'pinia'; import { createRouter, createWebHistory } from 'vue-router'; import { useWorkflowsStore } from '@/stores/workflows.store'; -import { createTestNode } from '@/__tests__/mocks'; -import { CHAT_TRIGGER_NODE_TYPE } from '@/constants'; import { h } from 'vue'; +import { executionResponse, aiChatWorkflow, simpleWorkflow, nodeTypes } from '../__test__/data'; +import { useNodeTypesStore } from '@/stores/nodeTypes.store'; describe('LogsPanel', () => { let pinia: TestingPinia; let settingsStore: ReturnType>; let workflowsStore: ReturnType>; + let nodeTypeStore: ReturnType>; function render() { return renderComponent(LogsPanel, { @@ -39,50 +40,97 @@ describe('LogsPanel', () => { settingsStore.isNewLogsEnabled = true; workflowsStore = mockedStore(useWorkflowsStore); + workflowsStore.setWorkflowExecutionData(null); + + nodeTypeStore = mockedStore(useNodeTypesStore); + nodeTypeStore.setNodeTypes(nodeTypes); }); - it('renders collapsed panel by default', async () => { - const rendered = render(); - - expect(await rendered.findByText('Logs')).toBeInTheDocument(); - expect( - rendered.queryByText('Nothing to display yet', { exact: false }), - ).not.toBeInTheDocument(); - }); - - it('renders chat panel if the workflow has chat trigger', async () => { - workflowsStore.workflowTriggerNodes = [createTestNode({ type: CHAT_TRIGGER_NODE_TYPE })]; + it('should render collapsed panel by default', async () => { + workflowsStore.setWorkflow(simpleWorkflow); const rendered = render(); - expect(await rendered.findByText('Chat')).toBeInTheDocument(); + expect(await rendered.findByTestId('logs-overview-header')).toBeInTheDocument(); + expect(rendered.queryByTestId('logs-overview-empty')).not.toBeInTheDocument(); + }); + + it('should render logs panel only if the workflow has no chat trigger', async () => { + workflowsStore.setWorkflow(simpleWorkflow); + + const rendered = render(); + + expect(await rendered.findByTestId('logs-overview-header')).toBeInTheDocument(); + expect(rendered.queryByTestId('chat-header')).not.toBeInTheDocument(); + }); + + it('should render chat panel and logs panel if the workflow has chat trigger', async () => { + workflowsStore.setWorkflow(aiChatWorkflow); + + const rendered = render(); + + expect(await rendered.findByTestId('logs-overview-header')).toBeInTheDocument(); + expect(await rendered.findByTestId('chat-header')).toBeInTheDocument(); }); it('opens collapsed panel when clicked', async () => { + workflowsStore.setWorkflow(aiChatWorkflow); + const rendered = render(); - await rendered.findByText('Logs'); + await fireEvent.click(await rendered.findByTestId('logs-overview-header')); - await fireEvent.click(rendered.getByText('Logs')); - - expect( - await rendered.findByText('Nothing to display yet', { exact: false }), - ).toBeInTheDocument(); + expect(await rendered.findByTestId('logs-overview-empty')).toBeInTheDocument(); }); - it('toggles panel when chevron icon button is clicked', async () => { + it('should toggle panel when chevron icon button in the overview panel is clicked', async () => { + workflowsStore.setWorkflow(aiChatWorkflow); + const rendered = render(); - await rendered.findByText('Logs'); + const overviewPanel = await rendered.findByTestId('logs-overview-header'); - await fireEvent.click(rendered.getAllByRole('button').pop()!); - expect(rendered.getByText('Nothing to display yet', { exact: false })).toBeInTheDocument(); + await fireEvent.click(within(overviewPanel).getByLabelText('Open panel')); + expect(rendered.getByTestId('logs-overview-empty')).toBeInTheDocument(); - await fireEvent.click(rendered.getAllByRole('button').pop()!); - await waitFor(() => - expect( - rendered.queryByText('Nothing to display yet', { exact: false }), - ).not.toBeInTheDocument(), - ); + await fireEvent.click(within(overviewPanel).getByLabelText('Collapse panel')); + expect(rendered.queryByTestId('logs-overview-empty')).not.toBeInTheDocument(); + }); + + it('should open log details panel when a log entry is clicked in the logs overview panel', async () => { + workflowsStore.setWorkflow(aiChatWorkflow); + workflowsStore.setWorkflowExecutionData(executionResponse); + + const rendered = render(); + + await fireEvent.click(await rendered.findByTestId('logs-overview-header')); + await fireEvent.click(await rendered.findByText('AI Agent')); + expect(rendered.getByTestId('log-details')).toBeInTheDocument(); + + // Click again to close the panel + await fireEvent.click(await rendered.findByText('AI Agent')); + expect(rendered.queryByTestId('log-details')).not.toBeInTheDocument(); + }); + + it("should show the button to toggle panel in the header of log details panel when it's opened", async () => { + workflowsStore.setWorkflow(aiChatWorkflow); + workflowsStore.setWorkflowExecutionData(executionResponse); + + const rendered = render(); + + await fireEvent.click(await rendered.findByTestId('logs-overview-header')); + await fireEvent.click(await rendered.findByText('AI Agent')); + + const detailsPanel = rendered.getByTestId('log-details'); + + // Click the toggle button to close the panel + await fireEvent.click(within(detailsPanel).getByLabelText('Collapse panel')); + expect(rendered.queryByTestId('chat-messages-empty')).not.toBeInTheDocument(); + expect(rendered.queryByText('AI Agent', { exact: false })).not.toBeInTheDocument(); + + // Click again to open the panel + await fireEvent.click(within(detailsPanel).getByLabelText('Open panel')); + expect(await rendered.findByTestId('chat-messages-empty')).toBeInTheDocument(); + expect(await rendered.findByText('AI Agent', { exact: false })).toBeInTheDocument(); }); }); diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.vue b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.vue index d6ec7c6c1d..18273b1c10 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.vue +++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.vue @@ -1,7 +1,7 @@ @@ -88,16 +102,16 @@ watch([panelState, height], ([state, h]) => {
{ > { /> + + + - +
@@ -209,4 +204,17 @@ watch([panelState, height], ([state, h]) => { flex-shrink: 0; max-width: 100%; } + +.logsOverview { + flex-basis: 20%; + flex-grow: 1; + flex-shrink: 1; + min-width: 360px; +} + +.logDetails { + flex-basis: 60%; + flex-grow: 1; + flex-shrink: 1; +} diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogDetailsPanel.vue b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogDetailsPanel.vue new file mode 100644 index 0000000000..f390f4529d --- /dev/null +++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogDetailsPanel.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.test.ts b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.test.ts index ddf5e9ecfe..2fe03c864a 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.test.ts +++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.test.ts @@ -1,6 +1,5 @@ import { renderComponent } from '@/__tests__/render'; import LogsOverviewPanel from './LogsOverviewPanel.vue'; -import { createTestNode, createTestWorkflow } from '@/__tests__/mocks'; import { setActivePinia } from 'pinia'; import { createTestingPinia, type TestingPinia } from '@pinia/testing'; import { mockedStore } from '@/__tests__/utils'; @@ -8,26 +7,12 @@ import { useWorkflowsStore } from '@/stores/workflows.store'; import { createRouter, createWebHistory } from 'vue-router'; import { h, type ExtractPropTypes } from 'vue'; import { fireEvent, waitFor, within } from '@testing-library/vue'; +import { aiAgentNode, executionResponse, aiChatWorkflow } from '../../__test__/data'; describe('LogsOverviewPanel', () => { let pinia: TestingPinia; let workflowsStore: ReturnType>; - const triggerNode = createTestNode({ name: 'Chat' }); - const aiAgentNode = createTestNode({ name: 'AI Agent' }); - const aiModelNode = createTestNode({ name: 'AI Model' }); - const workflow = createTestWorkflow({ - nodes: [triggerNode, aiAgentNode, aiModelNode], - connections: { - Chat: { - main: [[{ node: 'AI Agent', index: 0, type: 'main' }]], - }, - 'AI Model': { - ai_languageModel: [[{ node: 'AI Agent', index: 0, type: 'ai_languageModel' }]], - }, - }, - }); - function render(props: ExtractPropTypes) { return renderComponent(LogsOverviewPanel, { props, @@ -49,73 +34,24 @@ describe('LogsOverviewPanel', () => { setActivePinia(pinia); workflowsStore = mockedStore(useWorkflowsStore); - workflowsStore.setWorkflow(workflow); + workflowsStore.setWorkflow(aiChatWorkflow); workflowsStore.setWorkflowExecutionData(null); }); it('should not render body if the panel is not open', () => { const rendered = render({ isOpen: false, node: null }); - expect( - rendered.queryByText('Nothing to display yet', { exact: false }), - ).not.toBeInTheDocument(); + expect(rendered.queryByTestId('logs-overview-empty')).not.toBeInTheDocument(); }); it('should render empty text if there is no execution', () => { const rendered = render({ isOpen: true, node: null }); - expect(rendered.queryByText('Nothing to display yet', { exact: false })).toBeInTheDocument(); + expect(rendered.queryByTestId('logs-overview-empty')).toBeInTheDocument(); }); it('should render summary text and executed nodes if there is an execution', async () => { - workflowsStore.setWorkflowExecutionData({ - id: 'test-exec-id', - finished: true, - mode: 'manual', - status: 'success', - data: { - resultData: { - runData: { - 'AI Agent': [ - { - executionStatus: 'success', - startTime: +new Date('2025-03-26T00:00:00.002Z'), - executionTime: 1778, - source: [], - data: {}, - }, - ], - 'AI Model': [ - { - executionStatus: 'success', - startTime: +new Date('2025-03-26T00:00:00.003Z'), - executionTime: 1777, - source: [], - data: { - ai_languageModel: [ - [ - { - json: { - tokenUsage: { - completionTokens: 222, - promptTokens: 333, - totalTokens: 555, - }, - }, - }, - ], - ], - }, - }, - ], - }, - }, - }, - workflowData: workflow, - createdAt: new Date('2025-03-26T00:00:00.000Z'), - startedAt: new Date('2025-03-26T00:00:00.001Z'), - stoppedAt: new Date('2025-03-26T00:00:02.000Z'), - }); + workflowsStore.setWorkflowExecutionData(executionResponse); const rendered = render({ isOpen: true, node: aiAgentNode }); const summary = within(rendered.container.querySelector('.summary')!); @@ -131,14 +67,15 @@ describe('LogsOverviewPanel', () => { expect(row1.queryByText('AI Agent')).toBeInTheDocument(); expect(row1.queryByText('Success in 1.778s')).toBeInTheDocument(); - expect(row1.queryByText('Started 2025-03-26T00:00:00.002Z')).toBeInTheDocument(); + expect(row1.queryByText('Started 00:00:00.002, 26 Mar')).toBeInTheDocument(); expect(row1.queryByText('555 Tokens')).toBeInTheDocument(); const row2 = within(tree.queryAllByRole('treeitem')[1]); expect(row2.queryByText('AI Model')).toBeInTheDocument(); - expect(row2.queryByText('Success in 1.777s')).toBeInTheDocument(); - expect(row2.queryByText('Started 2025-03-26T00:00:00.003Z')).toBeInTheDocument(); + expect(row2.queryByText('Error')).toBeInTheDocument(); + expect(row2.queryByText('in 1.777s')).toBeInTheDocument(); + expect(row2.queryByText('Started 00:00:00.003, 26 Mar')).toBeInTheDocument(); expect(row2.queryByText('555 Tokens')).toBeInTheDocument(); // collapse tree diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue index b6a4d15baa..79af0f6717 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue +++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue @@ -5,7 +5,7 @@ import { useI18n } from '@/composables/useI18n'; import { useNodeHelpers } from '@/composables/useNodeHelpers'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { N8nButton, N8nRadioButtons, N8nText, N8nTooltip } from '@n8n/design-system'; -import { computed, ref } from 'vue'; +import { computed } from 'vue'; import { ElTree, type TreeNode as ElTreeNode } from 'element-plus'; import { createAiData, @@ -18,10 +18,16 @@ import { type INodeUi } from '@/Interface'; import { upperFirst } from 'lodash-es'; import { useTelemetry } from '@/composables/useTelemetry'; import ConsumedTokenCountText from '@/components/CanvasChat/future/components/ConsumedTokenCountText.vue'; +import { type LogEntryIdentity } from '@/components/CanvasChat/types/logs'; +import LogsOverviewRow from '@/components/CanvasChat/future/components/LogsOverviewRow.vue'; -const { node, isOpen } = defineProps<{ isOpen: boolean; node: INodeUi | null }>(); +const { node, isOpen, selected } = defineProps<{ + isOpen: boolean; + node: INodeUi | null; + selected?: LogEntryIdentity; +}>(); -const emit = defineEmits<{ clickHeader: [] }>(); +const emit = defineEmits<{ clickHeader: []; select: [LogEntryIdentity | undefined] }>(); defineSlots<{ actions: {} }>(); @@ -41,9 +47,9 @@ const executionTree = computed(() => : [], ); const isEmpty = computed(() => workflowsStore.workflowExecutionData === null); -const switchViewOptions = computed>(() => [ - { label: locale.baseText('logs.overview.header.switch.details'), value: 'details' }, - { label: locale.baseText('logs.overview.header.switch.overview'), value: 'overview' }, +const switchViewOptions = computed(() => [ + { label: locale.baseText('logs.overview.header.switch.details'), value: 'details' as const }, + { label: locale.baseText('logs.overview.header.switch.overview'), value: 'overview' as const }, ]); const executionStatusText = computed(() => { const execution = workflowsStore.workflowExecutionData; @@ -70,20 +76,18 @@ const consumedTokens = computed(() => getTotalConsumedTokens(...executionTree.value.map(getSubtreeTotalConsumedTokens)), ); -const selectedRun = ref<{ node: string; runIndex: number } | undefined>(undefined); - function onClearExecutionData() { workflowsStore.setWorkflowExecutionData(null); nodeHelpers.updateNodesExecutionIssues(); } function handleClickNode(clicked: TreeNode) { - if (selectedRun.value?.node === clicked.node && selectedRun.value.runIndex === clicked.runIndex) { - selectedRun.value = undefined; + if (selected?.node === clicked.node && selected.runIndex === clicked.runIndex) { + emit('select', undefined); return; } - selectedRun.value = { node: clicked.node, runIndex: clicked.runIndex }; + emit('select', { node: clicked.node, runIndex: clicked.runIndex }); telemetry.track('User selected node in log view', { node_type: workflowsStore.nodesByName[clicked.node].type, node_id: workflowsStore.nodesByName[clicked.node].id, @@ -92,15 +96,23 @@ function handleClickNode(clicked: TreeNode) { }); } +function handleSwitchView(value: 'overview' | 'details') { + emit( + 'select', + value === 'overview' || executionTree.value.length === 0 ? undefined : executionTree.value[0], + ); +} + function handleToggleExpanded(treeNode: ElTreeNode) { treeNode.expanded = !treeNode.expanded; }