From 0c4398fd2f72ad473a38f925e99cc524c4ad2038 Mon Sep 17 00:00:00 2001 From: Suguru Inoue Date: Tue, 13 May 2025 14:14:01 +0200 Subject: [PATCH] feat(editor): Show sub workflow runs in the log view (#15163) --- .../frontend/editor-ui/src/__tests__/mocks.ts | 5 + .../components/CanvasChat/__test__/data.ts | 22 +- .../CanvasChat/future/LogsPanel.test.ts | 22 +- .../CanvasChat/future/LogsPanel.vue | 25 +- .../future/components/LogDetailsPanel.test.ts | 46 +- .../future/components/LogDetailsPanel.vue | 14 +- .../components/LogsOverviewPanel.test.ts | 21 +- .../future/components/LogsOverviewPanel.vue | 95 ++-- .../future/components/LogsOverviewRow.vue | 17 +- .../future/components/RunDataView.vue | 13 +- .../composables/useExecutionData.test.ts | 110 ++++ .../future/composables/useExecutionData.ts | 74 ++- .../src/components/CanvasChat/types/logs.ts | 6 +- .../editor-ui/src/components/RunData.vue | 11 +- .../src/components/RunDataAi/utils.test.ts | 522 +++++++++++------- .../src/components/RunDataAi/utils.ts | 310 +++++++---- .../src/composables/useNodeHelpers.ts | 9 +- 17 files changed, 873 insertions(+), 449 deletions(-) create mode 100644 packages/frontend/editor-ui/src/components/CanvasChat/future/composables/useExecutionData.test.ts diff --git a/packages/frontend/editor-ui/src/__tests__/mocks.ts b/packages/frontend/editor-ui/src/__tests__/mocks.ts index 65b7f44fcb..f8bf30328e 100644 --- a/packages/frontend/editor-ui/src/__tests__/mocks.ts +++ b/packages/frontend/editor-ui/src/__tests__/mocks.ts @@ -256,6 +256,8 @@ export function createTestTaskData(partialData: Partial = {}): ITaskD } export function createTestLogEntry(data: Partial = {}): LogEntry { + const executionId = data.executionId ?? 'test-execution-id'; + return { node: createTestNode(), runIndex: 0, @@ -264,6 +266,9 @@ export function createTestLogEntry(data: Partial = {}): LogEntry { children: [], consumedTokens: { completionTokens: 0, totalTokens: 0, promptTokens: 0, isEstimate: false }, depth: 0, + workflow: createTestWorkflowObject(), + executionId, + execution: createTestWorkflowExecutionResponse({ id: executionId }).data!, ...data, }; } diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/__test__/data.ts b/packages/frontend/editor-ui/src/components/CanvasChat/__test__/data.ts index a1564f8f3f..c69ca36bc3 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/__test__/data.ts +++ b/packages/frontend/editor-ui/src/components/CanvasChat/__test__/data.ts @@ -1,4 +1,5 @@ import { createTestNode, createTestWorkflow, mockNodeTypeDescription } from '@/__tests__/mocks'; +import type { LogTreeCreationContext } from '@/components/RunDataAi/utils'; import { AGENT_NODE_TYPE, AI_CATEGORY_AGENTS, @@ -7,7 +8,26 @@ import { MANUAL_TRIGGER_NODE_TYPE, } from '@/constants'; import { type IExecutionResponse } from '@/Interface'; -import { WorkflowOperationError } from 'n8n-workflow'; +import { WorkflowOperationError, type IRunData, type Workflow } from 'n8n-workflow'; + +export function createTestLogTreeCreationContext( + workflow: Workflow, + runData: IRunData, +): LogTreeCreationContext { + return { + parent: undefined, + workflow, + workflows: {}, + subWorkflowData: {}, + executionId: 'test-execution-id', + depth: 0, + data: { + resultData: { + runData, + }, + }, + }; +} export const nodeTypes = [ mockNodeTypeDescription({ 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 398257cac0..ea4b9901a6 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 @@ -19,6 +19,7 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { LOGS_PANEL_STATE } from '../types/logs'; import { IN_PROGRESS_EXECUTION_ID } from '@/constants'; import { useCanvasOperations } from '@/composables/useCanvasOperations'; +import { createTestTaskData } from '@/__tests__/mocks'; describe('LogsPanel', () => { const VIEWPORT_HEIGHT = 800; @@ -211,7 +212,9 @@ describe('LogsPanel', () => { finished: false, startedAt: new Date('2025-04-20T12:34:50.000Z'), stoppedAt: undefined, - data: { resultData: { runData: {} } }, + data: { + resultData: { runData: { Chat: [createTestTaskData()] } }, + }, }); const rendered = render(); @@ -227,10 +230,15 @@ describe('LogsPanel', () => { data: { executionIndex: 0, startTime: Date.parse('2025-04-20T12:34:51.000Z'), source: [] }, }); - const treeItem = within(await rendered.findByRole('treeitem')); + const lastTreeItem = await waitFor(() => { + const items = rendered.getAllByRole('treeitem'); - expect(treeItem.getByText('AI Agent')).toBeInTheDocument(); - expect(treeItem.getByText('Running')).toBeInTheDocument(); + expect(items).toHaveLength(2); + return within(items[1]); + }); + + expect(lastTreeItem.getByText('AI Agent')).toBeInTheDocument(); + expect(lastTreeItem.getByText('Running')).toBeInTheDocument(); workflowsStore.updateNodeExecutionData({ nodeName: 'AI Agent', @@ -243,11 +251,11 @@ describe('LogsPanel', () => { executionStatus: 'success', }, }); - expect(await treeItem.findByText('AI Agent')).toBeInTheDocument(); - expect(treeItem.getByText('Success in 33ms')).toBeInTheDocument(); + expect(await lastTreeItem.findByText('AI Agent')).toBeInTheDocument(); + expect(lastTreeItem.getByText('Success in 33ms')).toBeInTheDocument(); workflowsStore.setWorkflowExecutionData({ - ...aiChatExecutionResponse, + ...workflowsStore.workflowExecutionData!, id: '1234', status: 'success', finished: true, 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 3603824d43..11c7f04507 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.vue +++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.vue @@ -47,11 +47,12 @@ const { displayExecution, } = useChatState(props.isReadOnly); -const { workflow, execution, hasChat, latestNodeNameById, resetExecutionData } = useExecutionData(); +const { entries, execution, hasChat, latestNodeNameById, resetExecutionData, loadSubExecution } = + useExecutionData(); const manualLogEntrySelection = ref({ type: 'initial' }); const selectedLogEntry = computed(() => - findSelectedLogEntry(manualLogEntrySelection.value, execution.value), + findSelectedLogEntry(manualLogEntrySelection.value as LogEntrySelection, entries.value), ); const isLogDetailsOpen = computed(() => isOpen.value && selectedLogEntry.value !== undefined); const isLogDetailsVisuallyOpen = computed( @@ -66,16 +67,8 @@ const logsPanelActionsProps = computed['$p })); function handleSelectLogEntry(selected: LogEntry | undefined) { - const workflowId = execution.value?.workflowData.id; - - if (!workflowId) { - return; - } - manualLogEntrySelection.value = - selected === undefined - ? { type: 'none', workflowId } - : { type: 'selected', workflowId, data: selected }; + selected === undefined ? { type: 'none' } : { type: 'selected', id: selected.id }; } function handleResizeOverviewPanelEnd() { @@ -145,15 +138,17 @@ function handleResizeOverviewPanelEnd() { :is-read-only="isReadOnly" :is-compact="isLogDetailsVisuallyOpen" :selected="selectedLogEntry" + :entries="entries" :execution="execution" :scroll-to-selection=" manualLogEntrySelection.type !== 'selected' || - manualLogEntrySelection.data.id !== selectedLogEntry?.id + manualLogEntrySelection.id !== selectedLogEntry?.id " :latest-node-info="latestNodeNameById" @click-header="onToggleOpen(true)" @select="handleSelectLogEntry" @clear-execution-data="resetExecutionData" + @load-sub-execution="loadSubExecution" >