mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
refactor(editor): Update RunDataAi helpers to use connections instead of Workflow object (no-changelog) (#17630)
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 })),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user