From d550382a4a43c54cae47e9071236aa18efe38a5d Mon Sep 17 00:00:00 2001 From: autologie Date: Fri, 28 Feb 2025 09:21:44 +0100 Subject: [PATCH] fix(editor): Don't show duplicate logs when tree is deeply nested (#13537) --- .../src/components/RunDataAi/RunDataAi.vue | 142 ++-------------- .../src/components/RunDataAi/utils.test.ts | 95 +++++++++++ .../src/components/RunDataAi/utils.ts | 160 ++++++++++++++++++ 3 files changed, 270 insertions(+), 127 deletions(-) create mode 100644 packages/editor-ui/src/components/RunDataAi/utils.test.ts create mode 100644 packages/editor-ui/src/components/RunDataAi/utils.ts diff --git a/packages/editor-ui/src/components/RunDataAi/RunDataAi.vue b/packages/editor-ui/src/components/RunDataAi/RunDataAi.vue index 2f90ca0faa..2094586c16 100644 --- a/packages/editor-ui/src/components/RunDataAi/RunDataAi.vue +++ b/packages/editor-ui/src/components/RunDataAi/RunDataAi.vue @@ -1,28 +1,22 @@ diff --git a/packages/editor-ui/src/components/RunDataAi/utils.test.ts b/packages/editor-ui/src/components/RunDataAi/utils.test.ts new file mode 100644 index 0000000000..17573fc019 --- /dev/null +++ b/packages/editor-ui/src/components/RunDataAi/utils.test.ts @@ -0,0 +1,95 @@ +import { createTestNode, createTestWorkflowObject } from '@/__tests__/mocks'; +import { createAiData, getTreeNodeData } from '@/components/RunDataAi/utils'; +import { type ITaskData, NodeConnectionType } from 'n8n-workflow'; + +describe(getTreeNodeData, () => { + function createTaskData(partialData: Partial): ITaskData { + return { + startTime: 0, + executionTime: 1, + source: [], + executionStatus: 'success', + data: { main: [[{ json: {} }]] }, + ...partialData, + }; + } + + it('should generate one node per execution', () => { + const workflow = createTestWorkflowObject({ + nodes: [ + createTestNode({ name: 'A' }), + createTestNode({ name: 'B' }), + createTestNode({ name: 'C' }), + ], + connections: { + B: { ai_tool: [[{ node: 'A', type: NodeConnectionType.AiTool, index: 0 }]] }, + C: { + ai_languageModel: [[{ node: 'B', type: NodeConnectionType.AiLanguageModel, index: 0 }]], + }, + }, + }); + const taskDataByNodeName: Record = { + A: [createTaskData({ startTime: +new Date('2025-02-26T00:00:00.000Z') })], + B: [ + createTaskData({ startTime: +new Date('2025-02-26T00:00:01.000Z') }), + createTaskData({ startTime: +new Date('2025-02-26T00:00:03.000Z') }), + ], + C: [ + createTaskData({ startTime: +new Date('2025-02-26T00:00:02.000Z') }), + createTaskData({ startTime: +new Date('2025-02-26T00:00:04.000Z') }), + ], + }; + + expect( + getTreeNodeData( + 'A', + workflow, + createAiData('A', workflow, (name) => taskDataByNodeName[name] ?? null), + ), + ).toEqual([ + { + depth: 0, + id: 'A', + node: 'A', + runIndex: 0, + startTime: 0, + children: [ + { + depth: 1, + id: 'B', + node: 'B', + runIndex: 0, + startTime: +new Date('2025-02-26T00:00:01.000Z'), + children: [ + { + children: [], + depth: 2, + id: 'C', + node: 'C', + runIndex: 0, + startTime: +new Date('2025-02-26T00:00:02.000Z'), + }, + ], + }, + { + depth: 1, + id: 'B', + node: 'B', + runIndex: 1, + startTime: +new Date('2025-02-26T00:00:03.000Z'), + children: [ + { + children: [], + depth: 2, + id: 'C', + node: 'C', + runIndex: 1, + startTime: +new Date('2025-02-26T00:00:04.000Z'), + }, + ], + }, + ], + }, + ]); + }); +}); diff --git a/packages/editor-ui/src/components/RunDataAi/utils.ts b/packages/editor-ui/src/components/RunDataAi/utils.ts new file mode 100644 index 0000000000..9c8dbb43db --- /dev/null +++ b/packages/editor-ui/src/components/RunDataAi/utils.ts @@ -0,0 +1,160 @@ +import { type IAiDataContent } from '@/Interface'; +import { + type ITaskData, + type ITaskDataConnections, + type NodeConnectionType, + type Workflow, +} from 'n8n-workflow'; + +export interface AIResult { + node: string; + runIndex: number; + data: IAiDataContent | undefined; +} + +export interface TreeNode { + node: string; + id: string; + children: TreeNode[]; + depth: number; + startTime: number; + runIndex: number; +} + +function createNode( + nodeName: string, + currentDepth: number, + r?: AIResult, + children: TreeNode[] = [], +): TreeNode { + return { + node: nodeName, + id: nodeName, + depth: currentDepth, + startTime: r?.data?.metadata?.startTime ?? 0, + runIndex: r?.runIndex ?? 0, + children, + }; +} + +export function getTreeNodeData( + nodeName: string, + workflow: Workflow, + aiData: AIResult[] | undefined, +): TreeNode[] { + return getTreeNodeDataRec(nodeName, 0, workflow, aiData, undefined); +} + +function getTreeNodeDataRec( + nodeName: string, + currentDepth: number, + workflow: Workflow, + aiData: AIResult[] | undefined, + runIndex: number | undefined, +): TreeNode[] { + const connections = workflow.connectionsByDestinationNode[nodeName]; + const resultData = + aiData?.filter( + (data) => data.node === nodeName && (runIndex === undefined || runIndex === data.runIndex), + ) ?? []; + + if (!connections) { + return resultData.map((d) => createNode(nodeName, currentDepth, d)); + } + + // Get the first level of children + const connectedSubNodes = workflow.getParentNodes(nodeName, 'ALL_NON_MAIN', 1); + + const children = connectedSubNodes.flatMap((name) => { + // Only include sub-nodes which have data + return ( + aiData + ?.filter( + (data) => data.node === name && (runIndex === undefined || data.runIndex === runIndex), + ) + .flatMap((data) => + getTreeNodeDataRec(name, currentDepth + 1, workflow, aiData, data.runIndex), + ) ?? [] + ); + }); + + children.sort((a, b) => a.startTime - b.startTime); + + if (resultData.length) { + return resultData.map((r) => createNode(nodeName, currentDepth, r, children)); + } + + return [createNode(nodeName, currentDepth, undefined, children)]; +} + +export function createAiData( + nodeName: string, + workflow: Workflow, + getWorkflowResultDataByNodeName: (nodeName: string) => ITaskData[] | null, +): AIResult[] { + const result: AIResult[] = []; + const connectedSubNodes = workflow.getParentNodes(nodeName, 'ALL_NON_MAIN'); + + connectedSubNodes.forEach((node) => { + const nodeRunData = getWorkflowResultDataByNodeName(node) ?? []; + + nodeRunData.forEach((run, runIndex) => { + const referenceData = { + data: getReferencedData(run, false, true)[0], + node, + runIndex, + }; + + result.push(referenceData); + }); + }); + + // Sort the data by start time + result.sort((a, b) => { + const aTime = a.data?.metadata?.startTime ?? 0; + const bTime = b.data?.metadata?.startTime ?? 0; + return aTime - bTime; + }); + + return result; +} + +export function getReferencedData( + taskData: ITaskData, + withInput: boolean, + withOutput: boolean, +): IAiDataContent[] { + if (!taskData) { + return []; + } + + const returnData: IAiDataContent[] = []; + + function addFunction(data: ITaskDataConnections | undefined, inOut: 'input' | 'output') { + if (!data) { + return; + } + + Object.keys(data).map((type) => { + returnData.push({ + data: data[type][0], + inOut, + type: type as NodeConnectionType, + metadata: { + executionTime: taskData.executionTime, + startTime: taskData.startTime, + subExecution: taskData.metadata?.subExecution, + }, + }); + }); + } + + if (withInput) { + addFunction(taskData.inputOverride, 'input'); + } + if (withOutput) { + addFunction(taskData.data, 'output'); + } + + return returnData; +}