mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix(editor): Fix AI Node Logs View to Filter Duplicate Executions (#15049)
Co-authored-by: autologie <suguru@n8n.io>
This commit is contained in:
@@ -75,6 +75,7 @@ function createRunDataWithError(inputMessage: string) {
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
source: [{ previousNode: AGENT_NODE_NAME, previousNodeRun: 0 }],
|
||||||
error: {
|
error: {
|
||||||
message: 'Internal error',
|
message: 'Internal error',
|
||||||
timestamp: 1722591723244,
|
timestamp: 1722591723244,
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ describe('Langchain Integration', () => {
|
|||||||
metadata: {
|
metadata: {
|
||||||
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
|
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
|
||||||
},
|
},
|
||||||
|
source: [{ previousNode: AGENT_NODE_NAME, previousNodeRun: 0 }],
|
||||||
inputOverride: {
|
inputOverride: {
|
||||||
ai_languageModel: [
|
ai_languageModel: [
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import type {
|
|||||||
Workflow,
|
Workflow,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
NodeConnectionType,
|
NodeConnectionType,
|
||||||
|
ISourceData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { createDeferredPromise, NodeConnectionTypes } from 'n8n-workflow';
|
import { createDeferredPromise, NodeConnectionTypes } from 'n8n-workflow';
|
||||||
|
|
||||||
@@ -42,6 +43,8 @@ export class SupplyDataContext extends BaseExecuteContext implements ISupplyData
|
|||||||
|
|
||||||
readonly getNodeParameter: ISupplyDataFunctions['getNodeParameter'];
|
readonly getNodeParameter: ISupplyDataFunctions['getNodeParameter'];
|
||||||
|
|
||||||
|
readonly parentNode?: INode;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
workflow: Workflow,
|
workflow: Workflow,
|
||||||
node: INode,
|
node: INode,
|
||||||
@@ -55,6 +58,7 @@ export class SupplyDataContext extends BaseExecuteContext implements ISupplyData
|
|||||||
executeData: IExecuteData,
|
executeData: IExecuteData,
|
||||||
private readonly closeFunctions: CloseFunction[],
|
private readonly closeFunctions: CloseFunction[],
|
||||||
abortSignal?: AbortSignal,
|
abortSignal?: AbortSignal,
|
||||||
|
parentNode?: INode,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
workflow,
|
workflow,
|
||||||
@@ -69,6 +73,8 @@ export class SupplyDataContext extends BaseExecuteContext implements ISupplyData
|
|||||||
abortSignal,
|
abortSignal,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.parentNode = parentNode;
|
||||||
|
|
||||||
this.helpers = {
|
this.helpers = {
|
||||||
createDeferredPromise,
|
createDeferredPromise,
|
||||||
copyInputItems,
|
copyInputItems,
|
||||||
@@ -126,6 +132,7 @@ export class SupplyDataContext extends BaseExecuteContext implements ISupplyData
|
|||||||
this.executeData,
|
this.executeData,
|
||||||
this.closeFunctions,
|
this.closeFunctions,
|
||||||
this.abortSignal,
|
this.abortSignal,
|
||||||
|
this.parentNode,
|
||||||
);
|
);
|
||||||
context.addInputData(NodeConnectionTypes.AiTool, replacements.inputData);
|
context.addInputData(NodeConnectionTypes.AiTool, replacements.inputData);
|
||||||
return context;
|
return context;
|
||||||
@@ -230,13 +237,17 @@ export class SupplyDataContext extends BaseExecuteContext implements ISupplyData
|
|||||||
} = this;
|
} = this;
|
||||||
|
|
||||||
let taskData: ITaskData | undefined;
|
let taskData: ITaskData | undefined;
|
||||||
|
const source: ISourceData[] = this.parentNode
|
||||||
|
? [{ previousNode: this.parentNode.name, previousNodeRun: sourceNodeRunIndex }]
|
||||||
|
: [];
|
||||||
|
|
||||||
if (type === 'input') {
|
if (type === 'input') {
|
||||||
taskData = {
|
taskData = {
|
||||||
startTime: Date.now(),
|
startTime: Date.now(),
|
||||||
executionTime: 0,
|
executionTime: 0,
|
||||||
executionIndex: additionalData.currentNodeExecutionIndex++,
|
executionIndex: additionalData.currentNodeExecutionIndex++,
|
||||||
executionStatus: 'running',
|
executionStatus: 'running',
|
||||||
source: [null],
|
source,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// At the moment we expect that there is always an input sent before the output
|
// At the moment we expect that there is always an input sent before the output
|
||||||
@@ -249,6 +260,7 @@ export class SupplyDataContext extends BaseExecuteContext implements ISupplyData
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
taskData.metadata = metadata;
|
taskData.metadata = metadata;
|
||||||
|
taskData.source = source;
|
||||||
}
|
}
|
||||||
taskData = taskData!;
|
taskData = taskData!;
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export async function getInputConnectionData(
|
|||||||
executeData,
|
executeData,
|
||||||
closeFunctions,
|
closeFunctions,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
|
parentNode,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!connectedNodeType.supplyData) {
|
if (!connectedNodeType.supplyData) {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import type {
|
|||||||
IPersonalizationSurveyAnswersV4,
|
IPersonalizationSurveyAnswersV4,
|
||||||
AnnotationVote,
|
AnnotationVote,
|
||||||
ITaskData,
|
ITaskData,
|
||||||
|
ISourceData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -189,6 +190,7 @@ export interface IAiDataContent {
|
|||||||
data: INodeExecutionData[] | null;
|
data: INodeExecutionData[] | null;
|
||||||
inOut: 'input' | 'output';
|
inOut: 'input' | 'output';
|
||||||
type: NodeConnectionType;
|
type: NodeConnectionType;
|
||||||
|
source?: Array<ISourceData | null>;
|
||||||
metadata: {
|
metadata: {
|
||||||
executionTime: number;
|
executionTime: number;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
|
|||||||
@@ -189,6 +189,313 @@ describe(getTreeNodeData, () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should filter node executions based on source node', () => {
|
||||||
|
const workflowWithSharedSubNode = createTestWorkflowObject({
|
||||||
|
nodes: [
|
||||||
|
createTestNode({ name: 'RootNode1' }),
|
||||||
|
createTestNode({ name: 'RootNode2' }),
|
||||||
|
createTestNode({ name: 'SharedSubNode' }),
|
||||||
|
],
|
||||||
|
connections: {
|
||||||
|
SharedSubNode: {
|
||||||
|
ai_tool: [
|
||||||
|
[{ node: 'RootNode1', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||||
|
[{ node: 'RootNode2', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create test AI data with source information
|
||||||
|
const sharedSubNodeData1 = {
|
||||||
|
node: 'SharedSubNode',
|
||||||
|
runIndex: 0,
|
||||||
|
data: {
|
||||||
|
data: [{ json: { result: 'from RootNode1' } }],
|
||||||
|
inOut: 'output' as const,
|
||||||
|
type: NodeConnectionTypes.AiTool,
|
||||||
|
source: [{ previousNode: 'RootNode1', previousNodeRun: 0 }],
|
||||||
|
metadata: {
|
||||||
|
executionTime: 100,
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:01.000Z'),
|
||||||
|
subExecution: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const sharedSubNodeData2 = {
|
||||||
|
node: 'SharedSubNode',
|
||||||
|
runIndex: 1,
|
||||||
|
data: {
|
||||||
|
data: [{ json: { result: 'from RootNode2' } }],
|
||||||
|
inOut: 'output' as const,
|
||||||
|
type: NodeConnectionTypes.AiTool,
|
||||||
|
source: [{ previousNode: 'RootNode2', previousNodeRun: 0 }],
|
||||||
|
metadata: {
|
||||||
|
executionTime: 100,
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:02.000Z'),
|
||||||
|
subExecution: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create test AI data array
|
||||||
|
const aiData = [sharedSubNodeData1, sharedSubNodeData2];
|
||||||
|
|
||||||
|
// Test for RootNode1 - should only show SharedSubNode with source RootNode1
|
||||||
|
const rootNode1Tree = getTreeNodeData('RootNode1', workflowWithSharedSubNode, aiData);
|
||||||
|
expect(rootNode1Tree[0].children.length).toBe(1);
|
||||||
|
expect(rootNode1Tree[0].children[0].node).toBe('SharedSubNode');
|
||||||
|
expect(rootNode1Tree[0].children[0].runIndex).toBe(0);
|
||||||
|
|
||||||
|
// Test for RootNode2 - should only show SharedSubNode with source RootNode2
|
||||||
|
const rootNode2Tree = getTreeNodeData('RootNode2', workflowWithSharedSubNode, aiData);
|
||||||
|
expect(rootNode2Tree[0].children.length).toBe(1);
|
||||||
|
expect(rootNode2Tree[0].children[0].node).toBe('SharedSubNode');
|
||||||
|
expect(rootNode2Tree[0].children[0].runIndex).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter node executions based on source run index', () => {
|
||||||
|
const workflow = createTestWorkflowObject({
|
||||||
|
nodes: [createTestNode({ name: 'RootNode' }), createTestNode({ name: 'SubNode' })],
|
||||||
|
connections: {
|
||||||
|
SubNode: {
|
||||||
|
ai_tool: [[{ node: 'RootNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create test AI data with source information
|
||||||
|
const subNodeData1 = {
|
||||||
|
node: 'SubNode',
|
||||||
|
runIndex: 0,
|
||||||
|
data: {
|
||||||
|
data: [{ json: { result: 'from RootNode' } }],
|
||||||
|
inOut: 'output' as const,
|
||||||
|
type: NodeConnectionTypes.AiTool,
|
||||||
|
source: [{ previousNode: 'RootNode', previousNodeRun: 0 }],
|
||||||
|
metadata: {
|
||||||
|
executionTime: 100,
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:01.000Z'),
|
||||||
|
subExecution: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const subNodeData2 = {
|
||||||
|
node: 'SubNode',
|
||||||
|
runIndex: 1,
|
||||||
|
data: {
|
||||||
|
data: [{ json: { result: 'from RootNode' } }],
|
||||||
|
inOut: 'output' as const,
|
||||||
|
type: NodeConnectionTypes.AiTool,
|
||||||
|
source: [{ previousNode: 'RootNode', previousNodeRun: 1 }],
|
||||||
|
metadata: {
|
||||||
|
executionTime: 100,
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:02.000Z'),
|
||||||
|
subExecution: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create test AI data array
|
||||||
|
const aiData = [subNodeData1, subNodeData2];
|
||||||
|
|
||||||
|
// Test for run #1 of RootNode - should only show SubNode with source run index 0
|
||||||
|
const rootNode1Tree = getTreeNodeData('RootNode', workflow, aiData, 0);
|
||||||
|
expect(rootNode1Tree[0].children.length).toBe(1);
|
||||||
|
expect(rootNode1Tree[0].children[0].node).toBe('SubNode');
|
||||||
|
expect(rootNode1Tree[0].children[0].runIndex).toBe(0);
|
||||||
|
|
||||||
|
// Test for run #2 of RootNode - should only show SubNode with source run index 1
|
||||||
|
const rootNode2Tree = getTreeNodeData('RootNode', workflow, aiData, 1);
|
||||||
|
expect(rootNode2Tree[0].children.length).toBe(1);
|
||||||
|
expect(rootNode2Tree[0].children[0].node).toBe('SubNode');
|
||||||
|
expect(rootNode2Tree[0].children[0].runIndex).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include nodes without source information', () => {
|
||||||
|
const workflow = createTestWorkflowObject({
|
||||||
|
nodes: [createTestNode({ name: 'RootNode' }), createTestNode({ name: 'SubNode' })],
|
||||||
|
connections: {
|
||||||
|
SubNode: {
|
||||||
|
ai_tool: [[{ node: 'RootNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create test AI data with a node that has no source information
|
||||||
|
const subNodeData = {
|
||||||
|
node: 'SubNode',
|
||||||
|
runIndex: 0,
|
||||||
|
data: {
|
||||||
|
data: [{ json: { result: 'from RootNode' } }],
|
||||||
|
inOut: 'output' as const,
|
||||||
|
type: NodeConnectionTypes.AiTool,
|
||||||
|
// No source field intentionally
|
||||||
|
metadata: {
|
||||||
|
executionTime: 100,
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:01.000Z'),
|
||||||
|
subExecution: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create test AI data array
|
||||||
|
const aiData = [subNodeData];
|
||||||
|
|
||||||
|
// Test for RootNode - should still show SubNode even without source info
|
||||||
|
const rootNodeTree = getTreeNodeData('RootNode', workflow, aiData);
|
||||||
|
expect(rootNodeTree[0].children.length).toBe(1);
|
||||||
|
expect(rootNodeTree[0].children[0].node).toBe('SubNode');
|
||||||
|
expect(rootNodeTree[0].children[0].runIndex).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include nodes with empty source array', () => {
|
||||||
|
const workflow = createTestWorkflowObject({
|
||||||
|
nodes: [createTestNode({ name: 'RootNode' }), createTestNode({ name: 'SubNode' })],
|
||||||
|
connections: {
|
||||||
|
SubNode: {
|
||||||
|
ai_tool: [[{ node: 'RootNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create test AI data with a node that has empty source array
|
||||||
|
const subNodeData = {
|
||||||
|
node: 'SubNode',
|
||||||
|
runIndex: 0,
|
||||||
|
data: {
|
||||||
|
data: [{ json: { result: 'from RootNode' } }],
|
||||||
|
inOut: 'output' as const,
|
||||||
|
type: NodeConnectionTypes.AiTool,
|
||||||
|
source: [], // Empty array
|
||||||
|
metadata: {
|
||||||
|
executionTime: 100,
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:01.000Z'),
|
||||||
|
subExecution: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create test AI data array
|
||||||
|
const aiData = [subNodeData];
|
||||||
|
|
||||||
|
// Test for RootNode - should still show SubNode even with empty source array
|
||||||
|
const rootNodeTree = getTreeNodeData('RootNode', workflow, aiData);
|
||||||
|
expect(rootNodeTree[0].children.length).toBe(1);
|
||||||
|
expect(rootNodeTree[0].children[0].node).toBe('SubNode');
|
||||||
|
expect(rootNodeTree[0].children[0].runIndex).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter deeper nested nodes based on source', () => {
|
||||||
|
const workflow = createTestWorkflowObject({
|
||||||
|
nodes: [
|
||||||
|
createTestNode({ name: 'RootNode1' }),
|
||||||
|
createTestNode({ name: 'RootNode2' }),
|
||||||
|
createTestNode({ name: 'SharedSubNode' }),
|
||||||
|
createTestNode({ name: 'DeepSubNode' }),
|
||||||
|
],
|
||||||
|
connections: {
|
||||||
|
SharedSubNode: {
|
||||||
|
ai_tool: [
|
||||||
|
[{ node: 'RootNode1', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||||
|
[{ node: 'RootNode2', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
DeepSubNode: {
|
||||||
|
ai_tool: [[{ node: 'SharedSubNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create test AI data with source information
|
||||||
|
const sharedSubNodeData1 = {
|
||||||
|
node: 'SharedSubNode',
|
||||||
|
runIndex: 0,
|
||||||
|
data: {
|
||||||
|
data: [{ json: { result: 'from RootNode1' } }],
|
||||||
|
inOut: 'output' as const,
|
||||||
|
type: NodeConnectionTypes.AiTool,
|
||||||
|
source: [{ previousNode: 'RootNode1', previousNodeRun: 0 }],
|
||||||
|
metadata: {
|
||||||
|
executionTime: 100,
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:01.000Z'),
|
||||||
|
subExecution: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const sharedSubNodeData2 = {
|
||||||
|
node: 'SharedSubNode',
|
||||||
|
runIndex: 1,
|
||||||
|
data: {
|
||||||
|
data: [{ json: { result: 'from RootNode2' } }],
|
||||||
|
inOut: 'output' as const,
|
||||||
|
type: NodeConnectionTypes.AiTool,
|
||||||
|
source: [{ previousNode: 'RootNode2', previousNodeRun: 0 }],
|
||||||
|
metadata: {
|
||||||
|
executionTime: 100,
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:02.000Z'),
|
||||||
|
subExecution: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const deepSubNodeData1 = {
|
||||||
|
node: 'DeepSubNode',
|
||||||
|
runIndex: 0,
|
||||||
|
data: {
|
||||||
|
data: [{ json: { result: 'from SharedSubNode run 0' } }],
|
||||||
|
inOut: 'output' as const,
|
||||||
|
type: NodeConnectionTypes.AiTool,
|
||||||
|
source: [{ previousNode: 'SharedSubNode', previousNodeRun: 0 }],
|
||||||
|
metadata: {
|
||||||
|
executionTime: 100,
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:03.000Z'),
|
||||||
|
subExecution: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const deepSubNodeData2 = {
|
||||||
|
node: 'DeepSubNode',
|
||||||
|
runIndex: 1,
|
||||||
|
data: {
|
||||||
|
data: [{ json: { result: 'from SharedSubNode run 1' } }],
|
||||||
|
inOut: 'output' as const,
|
||||||
|
type: NodeConnectionTypes.AiTool,
|
||||||
|
source: [{ previousNode: 'SharedSubNode', previousNodeRun: 1 }],
|
||||||
|
metadata: {
|
||||||
|
executionTime: 100,
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:04.000Z'),
|
||||||
|
subExecution: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create test AI data array
|
||||||
|
const aiData = [sharedSubNodeData1, sharedSubNodeData2, deepSubNodeData1, deepSubNodeData2];
|
||||||
|
|
||||||
|
// Test filtering for RootNode1
|
||||||
|
const rootNode1Tree = getTreeNodeData('RootNode1', workflow, aiData);
|
||||||
|
expect(rootNode1Tree[0].children.length).toBe(1);
|
||||||
|
expect(rootNode1Tree[0].children[0].node).toBe('SharedSubNode');
|
||||||
|
expect(rootNode1Tree[0].children[0].runIndex).toBe(0);
|
||||||
|
expect(rootNode1Tree[0].children[0].children.length).toBe(1);
|
||||||
|
expect(rootNode1Tree[0].children[0].children[0].node).toBe('DeepSubNode');
|
||||||
|
expect(rootNode1Tree[0].children[0].children[0].runIndex).toBe(0);
|
||||||
|
|
||||||
|
// Test filtering for RootNode2
|
||||||
|
const rootNode2Tree = getTreeNodeData('RootNode2', workflow, aiData);
|
||||||
|
expect(rootNode2Tree[0].children.length).toBe(1);
|
||||||
|
expect(rootNode2Tree[0].children[0].node).toBe('SharedSubNode');
|
||||||
|
expect(rootNode2Tree[0].children[0].runIndex).toBe(1);
|
||||||
|
expect(rootNode2Tree[0].children[0].children.length).toBe(1);
|
||||||
|
expect(rootNode2Tree[0].children[0].children[0].node).toBe('DeepSubNode');
|
||||||
|
expect(rootNode2Tree[0].children[0].children[0].runIndex).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(getTreeNodeDataV2, () => {
|
describe(getTreeNodeDataV2, () => {
|
||||||
@@ -321,6 +628,358 @@ describe(getTreeNodeDataV2, () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should filter node executions based on source node', () => {
|
||||||
|
const workflow = createTestWorkflowObject({
|
||||||
|
nodes: [
|
||||||
|
createTestNode({ name: 'RootNode1' }),
|
||||||
|
createTestNode({ name: 'RootNode2' }),
|
||||||
|
createTestNode({ name: 'SharedSubNode' }),
|
||||||
|
],
|
||||||
|
connections: {
|
||||||
|
SharedSubNode: {
|
||||||
|
ai_tool: [
|
||||||
|
[{ node: 'RootNode1', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||||
|
[{ node: 'RootNode2', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create test run data with source information
|
||||||
|
const runData = {
|
||||||
|
RootNode1: [
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:00.000Z'),
|
||||||
|
executionIndex: 0,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
RootNode2: [
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:01.000Z'),
|
||||||
|
executionIndex: 1,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
SharedSubNode: [
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:02.000Z'),
|
||||||
|
executionIndex: 2,
|
||||||
|
source: [{ previousNode: 'RootNode1', previousNodeRun: 0 }],
|
||||||
|
data: { main: [[{ json: { result: 'from RootNode1' } }]] },
|
||||||
|
}),
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:03.000Z'),
|
||||||
|
executionIndex: 3,
|
||||||
|
source: [{ previousNode: 'RootNode2', previousNodeRun: 1 }],
|
||||||
|
data: { main: [[{ json: { result: 'from RootNode2' } }]] },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test for RootNode1 - should only show SharedSubNode with source RootNode1
|
||||||
|
const rootNode1Tree = getTreeNodeDataV2('RootNode1', runData.RootNode1[0], workflow, runData);
|
||||||
|
expect(rootNode1Tree[0].children.length).toBe(1);
|
||||||
|
expect(rootNode1Tree[0].children[0].node.name).toBe('SharedSubNode');
|
||||||
|
expect(rootNode1Tree[0].children[0].runIndex).toBe(0);
|
||||||
|
|
||||||
|
// Test for RootNode2 - should only show SharedSubNode with source RootNode2
|
||||||
|
const rootNode2Tree = getTreeNodeDataV2('RootNode2', runData.RootNode2[0], workflow, runData);
|
||||||
|
expect(rootNode2Tree[0].children.length).toBe(1);
|
||||||
|
expect(rootNode2Tree[0].children[0].node.name).toBe('SharedSubNode');
|
||||||
|
expect(rootNode2Tree[0].children[0].runIndex).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter node executions based on source run index', () => {
|
||||||
|
const workflow = createTestWorkflowObject({
|
||||||
|
nodes: [createTestNode({ name: 'RootNode' }), createTestNode({ name: 'SubNode' })],
|
||||||
|
connections: {
|
||||||
|
SubNode: {
|
||||||
|
ai_tool: [[{ node: 'RootNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create test run data with source information
|
||||||
|
const runData = {
|
||||||
|
RootNode: [
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:00.000Z'),
|
||||||
|
executionIndex: 0,
|
||||||
|
}),
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:02.000Z'),
|
||||||
|
executionIndex: 2,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
SubNode: [
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:01.000Z'),
|
||||||
|
executionIndex: 1,
|
||||||
|
}),
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:03.000Z'),
|
||||||
|
executionIndex: 3,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test for run #1 of RootNode - should only show SubNode with source run index 0
|
||||||
|
const rootNode1Tree = getTreeNodeDataV2('RootNode', runData.RootNode[0], workflow, runData, 0);
|
||||||
|
expect(rootNode1Tree[0].children.length).toBe(1);
|
||||||
|
expect(rootNode1Tree[0].children[0].node.name).toBe('SubNode');
|
||||||
|
expect(rootNode1Tree[0].children[0].runIndex).toBe(0);
|
||||||
|
|
||||||
|
// Test for run #2 of RootNode - should only show SubNode with source run index 1
|
||||||
|
const rootNode2Tree = getTreeNodeDataV2('RootNode', runData.RootNode[1], workflow, runData, 1);
|
||||||
|
expect(rootNode2Tree[0].children.length).toBe(1);
|
||||||
|
expect(rootNode2Tree[0].children[0].node.name).toBe('SubNode');
|
||||||
|
expect(rootNode2Tree[0].children[0].runIndex).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include nodes without source information (v2)', () => {
|
||||||
|
const workflow = createTestWorkflowObject({
|
||||||
|
nodes: [createTestNode({ name: 'RootNode' }), createTestNode({ name: 'SubNode' })],
|
||||||
|
connections: {
|
||||||
|
SubNode: {
|
||||||
|
ai_tool: [[{ node: 'RootNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create test run data with a node that has no source field
|
||||||
|
const runData = {
|
||||||
|
RootNode: [
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:00.000Z'),
|
||||||
|
executionIndex: 0,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
SubNode: [
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:01.000Z'),
|
||||||
|
executionIndex: 1,
|
||||||
|
// No source field
|
||||||
|
data: { main: [[{ json: { result: 'from RootNode' } }]] },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test for RootNode - should still show SubNode even without source info
|
||||||
|
const rootNodeTree = getTreeNodeDataV2('RootNode', runData.RootNode[0], workflow, runData);
|
||||||
|
expect(rootNodeTree[0].children.length).toBe(1);
|
||||||
|
expect(rootNodeTree[0].children[0].node.name).toBe('SubNode');
|
||||||
|
expect(rootNodeTree[0].children[0].runIndex).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include nodes with empty source array (v2)', () => {
|
||||||
|
const workflow = createTestWorkflowObject({
|
||||||
|
nodes: [createTestNode({ name: 'RootNode' }), createTestNode({ name: 'SubNode' })],
|
||||||
|
connections: {
|
||||||
|
SubNode: {
|
||||||
|
ai_tool: [[{ node: 'RootNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create test run data with a node that has empty source array
|
||||||
|
const runData = {
|
||||||
|
RootNode: [
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:00.000Z'),
|
||||||
|
executionIndex: 0,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
SubNode: [
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:01.000Z'),
|
||||||
|
executionIndex: 1,
|
||||||
|
source: [], // Empty array
|
||||||
|
data: { main: [[{ json: { result: 'from RootNode' } }]] },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test for RootNode - should still show SubNode even with empty source array
|
||||||
|
const rootNodeTree = getTreeNodeDataV2('RootNode', runData.RootNode[0], workflow, runData);
|
||||||
|
expect(rootNodeTree[0].children.length).toBe(1);
|
||||||
|
expect(rootNodeTree[0].children[0].node.name).toBe('SubNode');
|
||||||
|
expect(rootNodeTree[0].children[0].runIndex).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter deeper nested nodes based on source (v2)', () => {
|
||||||
|
const workflow = createTestWorkflowObject({
|
||||||
|
nodes: [
|
||||||
|
createTestNode({ name: 'RootNode1' }),
|
||||||
|
createTestNode({ name: 'RootNode2' }),
|
||||||
|
createTestNode({ name: 'SharedSubNode' }),
|
||||||
|
createTestNode({ name: 'DeepSubNode' }),
|
||||||
|
],
|
||||||
|
connections: {
|
||||||
|
SharedSubNode: {
|
||||||
|
ai_tool: [
|
||||||
|
[{ node: 'RootNode1', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||||
|
[{ node: 'RootNode2', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
DeepSubNode: {
|
||||||
|
ai_tool: [[{ node: 'SharedSubNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create test run data with source information
|
||||||
|
const runData = {
|
||||||
|
RootNode1: [
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:00.000Z'),
|
||||||
|
executionIndex: 0,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
RootNode2: [
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:01.000Z'),
|
||||||
|
executionIndex: 1,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
SharedSubNode: [
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:02.000Z'),
|
||||||
|
executionIndex: 2,
|
||||||
|
source: [{ previousNode: 'RootNode1' }],
|
||||||
|
data: { main: [[{ json: { result: 'from RootNode1' } }]] },
|
||||||
|
}),
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:03.000Z'),
|
||||||
|
executionIndex: 3,
|
||||||
|
source: [{ previousNode: 'RootNode2' }],
|
||||||
|
data: { main: [[{ json: { result: 'from RootNode2' } }]] },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
DeepSubNode: [
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:04.000Z'),
|
||||||
|
executionIndex: 4,
|
||||||
|
source: [{ previousNode: 'SharedSubNode' }],
|
||||||
|
data: { main: [[{ json: { result: 'from SharedSubNode run 0' } }]] },
|
||||||
|
}),
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:05.000Z'),
|
||||||
|
executionIndex: 5,
|
||||||
|
source: [{ previousNode: 'SharedSubNode' }],
|
||||||
|
data: { main: [[{ json: { result: 'from SharedSubNode run 1' } }]] },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test filtering for RootNode1
|
||||||
|
const rootNode1Tree = getTreeNodeDataV2('RootNode1', runData.RootNode1[0], workflow, runData);
|
||||||
|
expect(rootNode1Tree[0].children.length).toBe(1);
|
||||||
|
expect(rootNode1Tree[0].children[0].node.name).toBe('SharedSubNode');
|
||||||
|
expect(rootNode1Tree[0].children[0].runIndex).toBe(0);
|
||||||
|
expect(rootNode1Tree[0].children[0].children.length).toBe(1);
|
||||||
|
expect(rootNode1Tree[0].children[0].children[0].node.name).toBe('DeepSubNode');
|
||||||
|
expect(rootNode1Tree[0].children[0].children[0].runIndex).toBe(0);
|
||||||
|
|
||||||
|
// Test filtering for RootNode2
|
||||||
|
const rootNode2Tree = getTreeNodeDataV2('RootNode2', runData.RootNode2[0], workflow, runData);
|
||||||
|
expect(rootNode2Tree[0].children.length).toBe(1);
|
||||||
|
expect(rootNode2Tree[0].children[0].node.name).toBe('SharedSubNode');
|
||||||
|
expect(rootNode2Tree[0].children[0].runIndex).toBe(1);
|
||||||
|
expect(rootNode2Tree[0].children[0].children.length).toBe(1);
|
||||||
|
expect(rootNode2Tree[0].children[0].children[0].node.name).toBe('DeepSubNode');
|
||||||
|
expect(rootNode2Tree[0].children[0].children[0].runIndex).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex tree with multiple branches and filters correctly', () => {
|
||||||
|
const workflow = createTestWorkflowObject({
|
||||||
|
nodes: [
|
||||||
|
createTestNode({ name: 'RootNode1' }),
|
||||||
|
createTestNode({ name: 'RootNode2' }),
|
||||||
|
createTestNode({ name: 'SubNodeA' }),
|
||||||
|
createTestNode({ name: 'SubNodeB' }),
|
||||||
|
createTestNode({ name: 'DeepNode' }),
|
||||||
|
],
|
||||||
|
connections: {
|
||||||
|
SubNodeA: {
|
||||||
|
ai_tool: [[{ node: 'RootNode1', type: NodeConnectionTypes.AiTool, index: 0 }]],
|
||||||
|
},
|
||||||
|
SubNodeB: {
|
||||||
|
ai_tool: [[{ node: 'RootNode2', type: NodeConnectionTypes.AiTool, index: 0 }]],
|
||||||
|
},
|
||||||
|
DeepNode: {
|
||||||
|
ai_tool: [
|
||||||
|
[{ node: 'SubNodeA', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||||
|
[{ node: 'SubNodeB', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create test run data with source information
|
||||||
|
const runData = {
|
||||||
|
RootNode1: [
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:00.000Z'),
|
||||||
|
executionIndex: 0,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
RootNode2: [
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:01.000Z'),
|
||||||
|
executionIndex: 1,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
SubNodeA: [
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:02.000Z'),
|
||||||
|
executionIndex: 2,
|
||||||
|
source: [{ previousNode: 'RootNode1' }],
|
||||||
|
data: { main: [[{ json: { result: 'from RootNode1' } }]] },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
SubNodeB: [
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:03.000Z'),
|
||||||
|
executionIndex: 3,
|
||||||
|
source: [{ previousNode: 'RootNode2' }],
|
||||||
|
data: { main: [[{ json: { result: 'from RootNode2' } }]] },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
DeepNode: [
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:04.000Z'),
|
||||||
|
executionIndex: 4,
|
||||||
|
source: [{ previousNode: 'SubNodeA' }],
|
||||||
|
data: { main: [[{ json: { result: 'from SubNodeA' } }]] },
|
||||||
|
}),
|
||||||
|
createTestTaskData({
|
||||||
|
startTime: Date.parse('2025-02-26T00:00:05.000Z'),
|
||||||
|
executionIndex: 5,
|
||||||
|
source: [{ previousNode: 'SubNodeB' }],
|
||||||
|
data: { main: [[{ json: { result: 'from SubNodeB' } }]] },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test filtering for RootNode1 -> SubNodeA -> DeepNode
|
||||||
|
const rootNode1Tree = getTreeNodeDataV2('RootNode1', runData.RootNode1[0], workflow, runData);
|
||||||
|
expect(rootNode1Tree[0].children.length).toBe(1);
|
||||||
|
expect(rootNode1Tree[0].children[0].node.name).toBe('SubNodeA');
|
||||||
|
expect(rootNode1Tree[0].children[0].children.length).toBe(1);
|
||||||
|
expect(rootNode1Tree[0].children[0].children[0].node.name).toBe('DeepNode');
|
||||||
|
expect(rootNode1Tree[0].children[0].children[0].runIndex).toBe(0); // First DeepNode execution
|
||||||
|
|
||||||
|
// Test filtering for RootNode2 -> SubNodeB -> DeepNode
|
||||||
|
const rootNode2Tree = getTreeNodeDataV2('RootNode2', runData.RootNode2[0], workflow, runData);
|
||||||
|
|
||||||
|
expect(rootNode2Tree[0].children.length).toBe(1);
|
||||||
|
expect(rootNode2Tree[0].children[0].node.name).toBe('SubNodeB');
|
||||||
|
expect(rootNode2Tree[0].children[0].children.length).toBe(1);
|
||||||
|
expect(rootNode2Tree[0].children[0].children[0].node.name).toBe('DeepNode');
|
||||||
|
|
||||||
|
const deepNodeRunIndex = rootNode2Tree[0].children[0].children[0].runIndex;
|
||||||
|
expect(typeof deepNodeRunIndex).toBe('number');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(findSelectedLogEntry, () => {
|
describe(findSelectedLogEntry, () => {
|
||||||
|
|||||||
@@ -80,13 +80,31 @@ function getTreeNodeDataRec(
|
|||||||
return resultData.map((d) => createNode(parent, nodeName, currentDepth, d.runIndex, d));
|
return resultData.map((d) => createNode(parent, nodeName, currentDepth, d.runIndex, d));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When at root depth, filter AI data to only show executions that were triggered by this node
|
||||||
|
// This prevents duplicate entries in logs when a sub-node is connected to multiple root nodes
|
||||||
|
// Nodes without source info or with empty source arrays are always included
|
||||||
|
const filteredAiData =
|
||||||
|
currentDepth === 0
|
||||||
|
? aiData?.filter(({ data }) => {
|
||||||
|
if (!data?.source || data.source.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.source.some(
|
||||||
|
(source) =>
|
||||||
|
source?.previousNode === nodeName &&
|
||||||
|
(runIndex === undefined || source.previousNodeRun === runIndex),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: aiData;
|
||||||
|
|
||||||
// Get the first level of children
|
// Get the first level of children
|
||||||
const connectedSubNodes = workflow.getParentNodes(nodeName, 'ALL_NON_MAIN', 1);
|
const connectedSubNodes = workflow.getParentNodes(nodeName, 'ALL_NON_MAIN', 1);
|
||||||
|
|
||||||
const treeNode = createNode(parent, nodeName, currentDepth, runIndex ?? 0);
|
const treeNode = createNode(parent, nodeName, currentDepth, runIndex ?? 0);
|
||||||
|
|
||||||
// Only include sub-nodes which have data
|
// Only include sub-nodes which have data
|
||||||
const children = (aiData ?? []).flatMap((data) =>
|
const children = (filteredAiData ?? []).flatMap((data) =>
|
||||||
connectedSubNodes.includes(data.node) && (runIndex === undefined || data.runIndex === runIndex)
|
connectedSubNodes.includes(data.node) && (runIndex === undefined || data.runIndex === runIndex)
|
||||||
? getTreeNodeDataRec(treeNode, data.node, currentDepth + 1, workflow, aiData, data.runIndex)
|
? getTreeNodeDataRec(treeNode, data.node, currentDepth + 1, workflow, aiData, data.runIndex)
|
||||||
: [],
|
: [],
|
||||||
@@ -152,6 +170,9 @@ export function getReferencedData(
|
|||||||
data: data[type][0],
|
data: data[type][0],
|
||||||
inOut,
|
inOut,
|
||||||
type: type as NodeConnectionType,
|
type: type as NodeConnectionType,
|
||||||
|
// Include source information in AI content to track which node triggered the execution
|
||||||
|
// This enables filtering in the UI to show only relevant executions
|
||||||
|
source: taskData.source,
|
||||||
metadata: {
|
metadata: {
|
||||||
executionTime: taskData.executionTime,
|
executionTime: taskData.executionTime,
|
||||||
startTime: taskData.startTime,
|
startTime: taskData.startTime,
|
||||||
@@ -310,10 +331,23 @@ function getTreeNodeDataRecV2(
|
|||||||
// Get the first level of children
|
// Get the first level of children
|
||||||
const connectedSubNodes = workflow.getParentNodes(node.name, 'ALL_NON_MAIN', 1);
|
const connectedSubNodes = workflow.getParentNodes(node.name, 'ALL_NON_MAIN', 1);
|
||||||
const treeNode = createNodeV2(parent, node, currentDepth, runIndex ?? 0, runData);
|
const treeNode = createNodeV2(parent, node, currentDepth, runIndex ?? 0, runData);
|
||||||
|
|
||||||
const children = connectedSubNodes
|
const children = connectedSubNodes
|
||||||
.flatMap((subNodeName) =>
|
.flatMap((subNodeName) =>
|
||||||
(data[subNodeName] ?? []).flatMap((t, index) => {
|
(data[subNodeName] ?? []).flatMap((t, index) => {
|
||||||
if (runIndex !== undefined && index !== runIndex) {
|
// At root depth, filter out node executions that weren't triggered by this node
|
||||||
|
// This prevents showing duplicate executions when a sub-node is connected to multiple parents
|
||||||
|
// Only filter nodes that have source information with valid previousNode references
|
||||||
|
const isMatched =
|
||||||
|
currentDepth === 0 && t.source?.length > 0
|
||||||
|
? t.source.some(
|
||||||
|
(source) =>
|
||||||
|
source?.previousNode === node.name &&
|
||||||
|
(runIndex === undefined || source.previousNodeRun === runIndex),
|
||||||
|
)
|
||||||
|
: runIndex === undefined || index === runIndex;
|
||||||
|
|
||||||
|
if (!isMatched) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,7 +421,12 @@ export function createLogEntries(workflow: Workflow, runData: IRunData) {
|
|||||||
workflow.getChildNodes(nodeName, 'ALL_NON_MAIN').length > 0 ||
|
workflow.getChildNodes(nodeName, 'ALL_NON_MAIN').length > 0 ||
|
||||||
workflow.getNode(nodeName)?.disabled
|
workflow.getNode(nodeName)?.disabled
|
||||||
? [] // skip sub nodes and disabled nodes
|
? [] // skip sub nodes and disabled nodes
|
||||||
: taskData.map((task, runIndex) => ({ nodeName, task, runIndex })),
|
: taskData.map((task, runIndex) => ({
|
||||||
|
nodeName,
|
||||||
|
task,
|
||||||
|
runIndex,
|
||||||
|
nodeHasMultipleRuns: taskData.length > 1,
|
||||||
|
})),
|
||||||
)
|
)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (a.task.executionIndex !== undefined && b.task.executionIndex !== undefined) {
|
if (a.task.executionIndex !== undefined && b.task.executionIndex !== undefined) {
|
||||||
@@ -399,13 +438,15 @@ export function createLogEntries(workflow: Workflow, runData: IRunData) {
|
|||||||
: a.task.startTime - b.task.startTime;
|
: a.task.startTime - b.task.startTime;
|
||||||
});
|
});
|
||||||
|
|
||||||
return runs.flatMap(({ nodeName, runIndex, task }) => {
|
return runs.flatMap(({ nodeName, runIndex, task, nodeHasMultipleRuns }) =>
|
||||||
if (workflow.getParentNodes(nodeName, 'ALL_NON_MAIN').length > 0) {
|
getTreeNodeDataV2(
|
||||||
return getTreeNodeDataV2(nodeName, task, workflow, runData, undefined);
|
nodeName,
|
||||||
}
|
task,
|
||||||
|
workflow,
|
||||||
return getTreeNodeDataV2(nodeName, task, workflow, runData, runIndex);
|
runData,
|
||||||
});
|
nodeHasMultipleRuns ? runIndex : undefined,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function includesLogEntry(log: LogEntry, logs: LogEntry[]): boolean {
|
export function includesLogEntry(log: LogEntry, logs: LogEntry[]): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user