refactor(editor): Update RunDataAi helpers to use connections instead of Workflow object (no-changelog) (#17630)

This commit is contained in:
Alex Grozav
2025-07-24 18:29:25 +03:00
committed by GitHub
parent 4b626e5282
commit 3f2e43e919
3 changed files with 76 additions and 32 deletions

View File

@@ -31,11 +31,20 @@ const selectedRun: Ref<IAiData[]> = ref([]);
const i18n = useI18n(); const i18n = useI18n();
const aiData = computed<AIResult[]>(() => const aiData = computed<AIResult[]>(() =>
createAiData(props.node.name, props.workflow, workflowsStore.getWorkflowResultDataByNodeName), createAiData(
props.node.name,
props.workflow.connectionsBySourceNode,
workflowsStore.getWorkflowResultDataByNodeName,
),
); );
const executionTree = computed<TreeNode[]>(() => const executionTree = computed<TreeNode[]>(() =>
getTreeNodeData(props.node.name, props.workflow, aiData.value, props.runIndex), getTreeNodeData(
props.node.name,
props.workflow.connectionsBySourceNode,
aiData.value,
props.runIndex,
),
); );
function isTreeNodeSelected(node: TreeNode) { function isTreeNodeSelected(node: TreeNode) {

View File

@@ -89,8 +89,12 @@ describe(getTreeNodeData, () => {
expect( expect(
getTreeNodeData( getTreeNodeData(
'A', 'A',
workflow, workflow.connectionsBySourceNode,
createAiData('A', workflow, (name) => taskDataByNodeName[name] ?? null), createAiData(
'A',
workflow.connectionsBySourceNode,
(name) => taskDataByNodeName[name] ?? null,
),
0, 0,
), ),
).toEqual([ ).toEqual([
@@ -215,10 +219,14 @@ describe(getTreeNodeData, () => {
}), }),
], ],
}; };
const aiData = createAiData('A', workflow, (name) => taskDataByNodeName[name] ?? null); const aiData = createAiData(
'A',
workflow.connectionsBySourceNode,
(name) => taskDataByNodeName[name] ?? null,
);
const treeRun0 = getTreeNodeData('A', workflow, aiData, 0); const treeRun0 = getTreeNodeData('A', workflow.connectionsBySourceNode, aiData, 0);
const treeRun1 = getTreeNodeData('A', workflow, aiData, 1); const treeRun1 = getTreeNodeData('A', workflow.connectionsBySourceNode, aiData, 1);
expect(treeRun0).toHaveLength(1); expect(treeRun0).toHaveLength(1);
expect(treeRun0[0].node).toBe('A'); expect(treeRun0[0].node).toBe('A');
@@ -293,13 +301,23 @@ describe(getTreeNodeData, () => {
const aiData = [sharedSubNodeData1, sharedSubNodeData2]; const aiData = [sharedSubNodeData1, sharedSubNodeData2];
// Test for RootNode1 - should only show SharedSubNode with source RootNode1 // Test for RootNode1 - should only show SharedSubNode with source RootNode1
const rootNode1Tree = getTreeNodeData('RootNode1', workflowWithSharedSubNode, aiData, 0); const rootNode1Tree = getTreeNodeData(
'RootNode1',
workflowWithSharedSubNode.connectionsBySourceNode,
aiData,
0,
);
expect(rootNode1Tree[0].children.length).toBe(1); expect(rootNode1Tree[0].children.length).toBe(1);
expect(rootNode1Tree[0].children[0].node).toBe('SharedSubNode'); expect(rootNode1Tree[0].children[0].node).toBe('SharedSubNode');
expect(rootNode1Tree[0].children[0].runIndex).toBe(0); expect(rootNode1Tree[0].children[0].runIndex).toBe(0);
// Test for RootNode2 - should only show SharedSubNode with source RootNode2 // Test for RootNode2 - should only show SharedSubNode with source RootNode2
const rootNode2Tree = getTreeNodeData('RootNode2', workflowWithSharedSubNode, aiData, 0); const rootNode2Tree = getTreeNodeData(
'RootNode2',
workflowWithSharedSubNode.connectionsBySourceNode,
aiData,
0,
);
expect(rootNode2Tree[0].children.length).toBe(1); expect(rootNode2Tree[0].children.length).toBe(1);
expect(rootNode2Tree[0].children[0].node).toBe('SharedSubNode'); expect(rootNode2Tree[0].children[0].node).toBe('SharedSubNode');
expect(rootNode2Tree[0].children[0].runIndex).toBe(1); expect(rootNode2Tree[0].children[0].runIndex).toBe(1);
@@ -352,13 +370,13 @@ describe(getTreeNodeData, () => {
const aiData = [subNodeData1, subNodeData2]; const aiData = [subNodeData1, subNodeData2];
// Test for run #1 of RootNode - should only show SubNode with source run index 0 // Test for run #1 of RootNode - should only show SubNode with source run index 0
const rootNode1Tree = getTreeNodeData('RootNode', workflow, aiData, 0); const rootNode1Tree = getTreeNodeData('RootNode', workflow.connectionsBySourceNode, aiData, 0);
expect(rootNode1Tree[0].children.length).toBe(1); expect(rootNode1Tree[0].children.length).toBe(1);
expect(rootNode1Tree[0].children[0].node).toBe('SubNode'); expect(rootNode1Tree[0].children[0].node).toBe('SubNode');
expect(rootNode1Tree[0].children[0].runIndex).toBe(0); expect(rootNode1Tree[0].children[0].runIndex).toBe(0);
// Test for run #2 of RootNode - should only show SubNode with source run index 1 // Test for run #2 of RootNode - should only show SubNode with source run index 1
const rootNode2Tree = getTreeNodeData('RootNode', workflow, aiData, 1); const rootNode2Tree = getTreeNodeData('RootNode', workflow.connectionsBySourceNode, aiData, 1);
expect(rootNode2Tree[0].children.length).toBe(1); expect(rootNode2Tree[0].children.length).toBe(1);
expect(rootNode2Tree[0].children[0].node).toBe('SubNode'); expect(rootNode2Tree[0].children[0].node).toBe('SubNode');
expect(rootNode2Tree[0].children[0].runIndex).toBe(1); expect(rootNode2Tree[0].children[0].runIndex).toBe(1);
@@ -395,7 +413,7 @@ describe(getTreeNodeData, () => {
const aiData = [subNodeData]; const aiData = [subNodeData];
// Test for RootNode - should still show SubNode even without source info // Test for RootNode - should still show SubNode even without source info
const rootNodeTree = getTreeNodeData('RootNode', workflow, aiData, 0); const rootNodeTree = getTreeNodeData('RootNode', workflow.connectionsBySourceNode, aiData, 0);
expect(rootNodeTree[0].children.length).toBe(1); expect(rootNodeTree[0].children.length).toBe(1);
expect(rootNodeTree[0].children[0].node).toBe('SubNode'); expect(rootNodeTree[0].children[0].node).toBe('SubNode');
expect(rootNodeTree[0].children[0].runIndex).toBe(0); expect(rootNodeTree[0].children[0].runIndex).toBe(0);
@@ -432,7 +450,7 @@ describe(getTreeNodeData, () => {
const aiData = [subNodeData]; const aiData = [subNodeData];
// Test for RootNode - should still show SubNode even with empty source array // Test for RootNode - should still show SubNode even with empty source array
const rootNodeTree = getTreeNodeData('RootNode', workflow, aiData, 0); const rootNodeTree = getTreeNodeData('RootNode', workflow.connectionsBySourceNode, aiData, 0);
expect(rootNodeTree[0].children.length).toBe(1); expect(rootNodeTree[0].children.length).toBe(1);
expect(rootNodeTree[0].children[0].node).toBe('SubNode'); expect(rootNodeTree[0].children[0].node).toBe('SubNode');
expect(rootNodeTree[0].children[0].runIndex).toBe(0); expect(rootNodeTree[0].children[0].runIndex).toBe(0);
@@ -468,7 +486,7 @@ describe(getTreeNodeData, () => {
// Create test AI data array // Create test AI data array
const aiData = [subNodeData]; const aiData = [subNodeData];
const rootNodeTree = getTreeNodeData('RootNode', workflow, aiData, 0); const rootNodeTree = getTreeNodeData('RootNode', workflow.connectionsBySourceNode, aiData, 0);
expect(rootNodeTree[0].children.length).toBe(1); expect(rootNodeTree[0].children.length).toBe(1);
expect(rootNodeTree[0].children[0].node).toBe('SubNode'); expect(rootNodeTree[0].children[0].node).toBe('SubNode');
@@ -564,7 +582,7 @@ describe(getTreeNodeData, () => {
const aiData = [sharedSubNodeData1, sharedSubNodeData2, deepSubNodeData1, deepSubNodeData2]; const aiData = [sharedSubNodeData1, sharedSubNodeData2, deepSubNodeData1, deepSubNodeData2];
// Test filtering for RootNode1 // Test filtering for RootNode1
const rootNode1Tree = getTreeNodeData('RootNode1', workflow, aiData, 0); const rootNode1Tree = getTreeNodeData('RootNode1', workflow.connectionsBySourceNode, aiData, 0);
expect(rootNode1Tree[0].children.length).toBe(1); expect(rootNode1Tree[0].children.length).toBe(1);
expect(rootNode1Tree[0].children[0].node).toBe('SharedSubNode'); expect(rootNode1Tree[0].children[0].node).toBe('SharedSubNode');
expect(rootNode1Tree[0].children[0].runIndex).toBe(0); expect(rootNode1Tree[0].children[0].runIndex).toBe(0);
@@ -573,7 +591,7 @@ describe(getTreeNodeData, () => {
expect(rootNode1Tree[0].children[0].children[0].runIndex).toBe(0); expect(rootNode1Tree[0].children[0].children[0].runIndex).toBe(0);
// Test filtering for RootNode2 // Test filtering for RootNode2
const rootNode2Tree = getTreeNodeData('RootNode2', workflow, aiData, 0); const rootNode2Tree = getTreeNodeData('RootNode2', workflow.connectionsBySourceNode, aiData, 0);
expect(rootNode2Tree[0].children.length).toBe(1); expect(rootNode2Tree[0].children.length).toBe(1);
expect(rootNode2Tree[0].children[0].node).toBe('SharedSubNode'); expect(rootNode2Tree[0].children[0].node).toBe('SharedSubNode');
expect(rootNode2Tree[0].children[0].runIndex).toBe(1); expect(rootNode2Tree[0].children[0].runIndex).toBe(1);

View File

@@ -1,16 +1,17 @@
import { type LlmTokenUsageData, type IAiDataContent } from '@/Interface'; import { type LlmTokenUsageData, type IAiDataContent } from '@/Interface';
import { addTokenUsageData, emptyTokenUsageData } from '@/utils/aiUtils'; import { addTokenUsageData, emptyTokenUsageData } from '@/utils/aiUtils';
import { import type {
type INodeExecutionData, IConnections,
type ITaskData, INodeExecutionData,
type ITaskDataConnections, ITaskData,
type NodeConnectionType, ITaskDataConnections,
type Workflow, NodeConnectionType,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { splitTextBySearch } from '@/utils/stringUtils'; import { splitTextBySearch } from '@/utils/stringUtils';
import { escapeHtml } from 'xss'; import { escapeHtml } from 'xss';
import type MarkdownIt from 'markdown-it'; import type MarkdownIt from 'markdown-it';
import { unescapeAll } from 'markdown-it/lib/common/utils'; import { unescapeAll } from 'markdown-it/lib/common/utils';
import * as workflowUtils from 'n8n-workflow/common';
export interface AIResult { export interface AIResult {
node: string; node: string;
@@ -51,26 +52,28 @@ function createNode(
export function getTreeNodeData( export function getTreeNodeData(
nodeName: string, nodeName: string,
workflow: Workflow, connectionsBySourceNode: IConnections,
aiData: AIResult[] | undefined, aiData: AIResult[] | undefined,
runIndex: number, runIndex: number,
): TreeNode[] { ): TreeNode[] {
return getTreeNodeDataRec(undefined, nodeName, 0, workflow, aiData, runIndex); return getTreeNodeDataRec(undefined, nodeName, 0, connectionsBySourceNode, aiData, runIndex);
} }
function getTreeNodeDataRec( function getTreeNodeDataRec(
parent: TreeNode | undefined, parent: TreeNode | undefined,
nodeName: string, nodeName: string,
currentDepth: number, currentDepth: number,
workflow: Workflow, connectionsBySourceNode: IConnections,
aiData: AIResult[] | undefined, aiData: AIResult[] | undefined,
runIndex: number, runIndex: number,
): TreeNode[] { ): TreeNode[] {
const connections = workflow.connectionsByDestinationNode[nodeName]; const connectionsByDestinationNode =
workflowUtils.mapConnectionsByDestination(connectionsBySourceNode);
const nodeConnections = connectionsByDestinationNode[nodeName];
const resultData = const resultData =
aiData?.filter((data) => data.node === nodeName && runIndex === data.runIndex) ?? []; aiData?.filter((data) => data.node === nodeName && runIndex === data.runIndex) ?? [];
if (!connections) { if (!nodeConnections) {
return resultData.map((d) => createNode(parent, nodeName, currentDepth, d.runIndex, d)); return resultData.map((d) => createNode(parent, nodeName, currentDepth, d.runIndex, d));
} }
@@ -88,14 +91,26 @@ function getTreeNodeDataRec(
}); });
// Get the first level of children // Get the first level of children
const connectedSubNodes = workflow.getParentNodes(nodeName, 'ALL_NON_MAIN', 1); const connectedSubNodes = workflowUtils.getParentNodes(
connectionsByDestinationNode,
nodeName,
'ALL_NON_MAIN',
1,
);
const treeNode = createNode(parent, nodeName, currentDepth, runIndex); const treeNode = createNode(parent, nodeName, currentDepth, runIndex);
// Only include sub-nodes which have data // Only include sub-nodes which have data
const children = (filteredAiData ?? []).flatMap((data) => const children = (filteredAiData ?? []).flatMap((data) =>
connectedSubNodes.includes(data.node) connectedSubNodes.includes(data.node)
? getTreeNodeDataRec(treeNode, data.node, currentDepth + 1, workflow, aiData, data.runIndex) ? getTreeNodeDataRec(
treeNode,
data.node,
currentDepth + 1,
connectionsBySourceNode,
aiData,
data.runIndex,
)
: [], : [],
); );
@@ -112,11 +127,13 @@ function getTreeNodeDataRec(
export function createAiData( export function createAiData(
nodeName: string, nodeName: string,
workflow: Workflow, connectionsBySourceNode: IConnections,
getWorkflowResultDataByNodeName: (nodeName: string) => ITaskData[] | null, getWorkflowResultDataByNodeName: (nodeName: string) => ITaskData[] | null,
): AIResult[] { ): AIResult[] {
return workflow const connectionsByDestinationNode =
.getParentNodes(nodeName, 'ALL_NON_MAIN') workflowUtils.mapConnectionsByDestination(connectionsBySourceNode);
return workflowUtils
.getParentNodes(connectionsByDestinationNode, nodeName, 'ALL_NON_MAIN')
.flatMap((node) => .flatMap((node) =>
(getWorkflowResultDataByNodeName(node) ?? []).map((task, index) => ({ node, task, index })), (getWorkflowResultDataByNodeName(node) ?? []).map((task, index) => ({ node, task, index })),
) )