mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix(editor): Waiting executions broken - Chat, Form, Wait (no-changelog) (#15343)
This commit is contained in:
@@ -224,7 +224,7 @@ describe('LogsPanel', () => {
|
||||
expect(rendered.getByText('Running')).toBeInTheDocument();
|
||||
expect(rendered.queryByText('AI Agent')).not.toBeInTheDocument();
|
||||
|
||||
workflowsStore.addNodeExecutionData({
|
||||
workflowsStore.addNodeExecutionStartedData({
|
||||
nodeName: 'AI Agent',
|
||||
executionId: '567',
|
||||
data: { executionIndex: 0, startTime: Date.parse('2025-04-20T12:34:51.000Z'), source: [] },
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useThrottleFn } from '@vueuse/core';
|
||||
import {
|
||||
createLogTree,
|
||||
deepToRaw,
|
||||
mergeStartData,
|
||||
type LatestNodeInfo,
|
||||
type LogEntry,
|
||||
} from '@/components/RunDataAi/utils';
|
||||
@@ -106,11 +107,19 @@ export function useExecutionData() {
|
||||
() => workflowsStore.workflowExecutionData?.workflowData.id,
|
||||
() => workflowsStore.workflowExecutionData?.status,
|
||||
() => workflowsStore.workflowExecutionResultDataLastUpdate,
|
||||
() => workflowsStore.workflowExecutionStartedData,
|
||||
],
|
||||
useThrottleFn(
|
||||
([executionId], [previousExecutionId]) => {
|
||||
// Create deep copy to disable reactivity
|
||||
execData.value = deepToRaw(workflowsStore.workflowExecutionData ?? undefined);
|
||||
execData.value =
|
||||
workflowsStore.workflowExecutionData === null
|
||||
? undefined
|
||||
: deepToRaw(
|
||||
mergeStartData(
|
||||
workflowsStore.workflowExecutionStartedData?.[1] ?? {},
|
||||
workflowsStore.workflowExecutionData,
|
||||
),
|
||||
); // Create deep copy to disable reactivity
|
||||
|
||||
if (executionId !== previousExecutionId) {
|
||||
// Reset sub workflow data when top-level execution changes
|
||||
|
||||
@@ -14,11 +14,13 @@ import {
|
||||
getDefaultCollapsedEntries,
|
||||
getTreeNodeData,
|
||||
getTreeNodeDataV2,
|
||||
mergeStartData,
|
||||
} from '@/components/RunDataAi/utils';
|
||||
import {
|
||||
AGENT_LANGCHAIN_NODE_TYPE,
|
||||
type ExecutionError,
|
||||
type ITaskData,
|
||||
type ITaskStartedData,
|
||||
NodeConnectionTypes,
|
||||
} from 'n8n-workflow';
|
||||
import { type LogEntrySelection } from '../CanvasChat/types/logs';
|
||||
@@ -1259,7 +1261,7 @@ describe(createLogTree, () => {
|
||||
executionIndex: 3,
|
||||
}),
|
||||
createTestTaskData({
|
||||
startTime: Date.parse('2025-04-04T00:00:03.000Z'),
|
||||
startTime: Date.parse('2025-04-04T00:00:02.000Z'),
|
||||
executionIndex: 2,
|
||||
}),
|
||||
],
|
||||
@@ -1318,7 +1320,7 @@ describe(createLogTree, () => {
|
||||
executionIndex: 3,
|
||||
}),
|
||||
createTestTaskData({
|
||||
startTime: Date.parse('2025-04-04T00:00:03.000Z'),
|
||||
startTime: Date.parse('2025-04-04T00:00:02.000Z'),
|
||||
executionIndex: 2,
|
||||
}),
|
||||
],
|
||||
@@ -1457,6 +1459,90 @@ describe(deepToRaw, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe(mergeStartData, () => {
|
||||
it('should return unchanged execution response if start data is empty', () => {
|
||||
const response = createTestWorkflowExecutionResponse({
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {
|
||||
A: [createTestTaskData()],
|
||||
B: [createTestTaskData(), createTestTaskData()],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(mergeStartData({}, response)).toEqual(response);
|
||||
});
|
||||
|
||||
it('should add runs in start data to the execution response as running state', () => {
|
||||
const response = createTestWorkflowExecutionResponse({
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {
|
||||
A: [createTestTaskData({ startTime: 0, executionIndex: 0 })],
|
||||
B: [
|
||||
createTestTaskData({ startTime: 1, executionIndex: 1 }),
|
||||
createTestTaskData({ startTime: 2, executionIndex: 2 }),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const startData: { [nodeName: string]: ITaskStartedData[] } = {
|
||||
B: [{ startTime: 3, executionIndex: 3, source: [] }],
|
||||
C: [{ startTime: 4, executionIndex: 4, source: [] }],
|
||||
};
|
||||
const merged = mergeStartData(startData, response);
|
||||
|
||||
expect(merged.data?.resultData.runData.A).toEqual(response.data?.resultData.runData.A);
|
||||
expect(merged.data?.resultData.runData.B).toEqual([
|
||||
response.data!.resultData.runData.B[0],
|
||||
response.data!.resultData.runData.B[1],
|
||||
{ ...startData.B[0], executionStatus: 'running', executionTime: 0 },
|
||||
]);
|
||||
expect(merged.data?.resultData.runData.C).toEqual([
|
||||
{ ...startData.C[0], executionStatus: 'running', executionTime: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not add runs in start data if a run with the same executionIndex already exists in response', () => {
|
||||
const response = createTestWorkflowExecutionResponse({
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {
|
||||
A: [createTestTaskData({ executionIndex: 0 })],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const startData = {
|
||||
A: [createTestTaskData({ executionIndex: 0 })],
|
||||
};
|
||||
const merged = mergeStartData(startData, response);
|
||||
|
||||
expect(merged.data?.resultData.runData.A).toEqual(response.data?.resultData.runData.A);
|
||||
});
|
||||
|
||||
it('should not add runs in start data if a run for the same node with larger start time already exists in response', () => {
|
||||
const response = createTestWorkflowExecutionResponse({
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {
|
||||
A: [createTestTaskData({ startTime: 1, executionIndex: 1 })],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const startData = {
|
||||
A: [createTestTaskData({ startTime: 0, executionIndex: 0 })],
|
||||
};
|
||||
const merged = mergeStartData(startData, response);
|
||||
|
||||
expect(merged.data?.resultData.runData.A).toEqual(response.data?.resultData.runData.A);
|
||||
});
|
||||
});
|
||||
|
||||
describe(getDefaultCollapsedEntries, () => {
|
||||
it('should recursively find logs for runs with a sub execution and has no child logs', () => {
|
||||
const entries = [
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type ITaskDataConnections,
|
||||
type NodeConnectionType,
|
||||
type Workflow,
|
||||
type ITaskStartedData,
|
||||
type IRunExecutionData,
|
||||
} from 'n8n-workflow';
|
||||
import { type LogEntrySelection } from '../CanvasChat/types/logs';
|
||||
@@ -397,17 +398,7 @@ function getTreeNodeDataRecV2(
|
||||
runIndex: number | undefined,
|
||||
): LogEntry[] {
|
||||
const treeNode = createNodeV2(node, context, runIndex ?? 0, runData);
|
||||
const children = getChildNodes(treeNode, node, runIndex, context).sort((a, b) => {
|
||||
// Sort the data by execution index or start time
|
||||
if (a.runData.executionIndex !== undefined && b.runData.executionIndex !== undefined) {
|
||||
return a.runData.executionIndex - b.runData.executionIndex;
|
||||
}
|
||||
|
||||
const aTime = a.runData.startTime ?? 0;
|
||||
const bTime = b.runData.startTime ?? 0;
|
||||
|
||||
return aTime - bTime;
|
||||
});
|
||||
const children = getChildNodes(treeNode, node, runIndex, context).sort(sortLogEntries);
|
||||
|
||||
treeNode.children = children;
|
||||
|
||||
@@ -483,23 +474,15 @@ function createLogTreeRec(context: LogTreeCreationContext) {
|
||||
? [] // skip sub nodes and disabled nodes
|
||||
: taskData.map((task, runIndex) => ({
|
||||
nodeName,
|
||||
task,
|
||||
runData: task,
|
||||
runIndex,
|
||||
nodeHasMultipleRuns: taskData.length > 1,
|
||||
})),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (a.task.executionIndex !== undefined && b.task.executionIndex !== undefined) {
|
||||
return a.task.executionIndex - b.task.executionIndex;
|
||||
}
|
||||
.sort(sortLogEntries);
|
||||
|
||||
return a.nodeName === b.nodeName
|
||||
? a.runIndex - b.runIndex
|
||||
: a.task.startTime - b.task.startTime;
|
||||
});
|
||||
|
||||
return runs.flatMap(({ nodeName, runIndex, task, nodeHasMultipleRuns }) =>
|
||||
getTreeNodeDataV2(nodeName, task, nodeHasMultipleRuns ? runIndex : undefined, context),
|
||||
return runs.flatMap(({ nodeName, runIndex, runData, nodeHasMultipleRuns }) =>
|
||||
getTreeNodeDataV2(nodeName, runData, nodeHasMultipleRuns ? runIndex : undefined, context),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -595,6 +578,65 @@ export function flattenLogEntries(
|
||||
return ret;
|
||||
}
|
||||
|
||||
function sortLogEntries<T extends { runData: ITaskData }>(a: T, b: T) {
|
||||
// We rely on execution index only when startTime is different
|
||||
// Because it is reset to 0 when execution is waited, and therefore not necessarily unique
|
||||
if (a.runData.startTime === b.runData.startTime) {
|
||||
return a.runData.executionIndex - b.runData.executionIndex;
|
||||
}
|
||||
|
||||
return a.runData.startTime - b.runData.startTime;
|
||||
}
|
||||
|
||||
export function mergeStartData(
|
||||
startData: { [nodeName: string]: ITaskStartedData[] },
|
||||
response: IExecutionResponse,
|
||||
): IExecutionResponse {
|
||||
if (!response.data) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const nodeNames = [
|
||||
...new Set(
|
||||
Object.keys(startData).concat(Object.keys(response.data.resultData.runData)),
|
||||
).values(),
|
||||
];
|
||||
const runData = Object.fromEntries(
|
||||
nodeNames.map<[string, ITaskData[]]>((nodeName) => {
|
||||
const tasks = response.data?.resultData.runData[nodeName] ?? [];
|
||||
const mergedTasks = tasks.concat(
|
||||
(startData[nodeName] ?? [])
|
||||
.filter((task) =>
|
||||
// To remove duplicate runs, we check start time in addition to execution index
|
||||
// because nodes such as Wait and Form emits multiple websocket events with
|
||||
// different execution index for a single run
|
||||
tasks.every(
|
||||
(t) => t.startTime < task.startTime && t.executionIndex !== task.executionIndex,
|
||||
),
|
||||
)
|
||||
.map<ITaskData>((task) => ({
|
||||
...task,
|
||||
executionTime: 0,
|
||||
executionStatus: 'running',
|
||||
})),
|
||||
);
|
||||
|
||||
return [nodeName, mergedTasks];
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
...response,
|
||||
data: {
|
||||
...response.data,
|
||||
resultData: {
|
||||
...response.data.resultData,
|
||||
runData,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function hasSubExecution(entry: LogEntry): boolean {
|
||||
return !!entry.runData.metadata?.subExecution;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user