From 5deab75c7ddbc818e5d0cee4e2b85352ab682538 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Tue, 10 Jun 2025 09:32:08 +0200 Subject: [PATCH] fix(editor): Add visual-only `waitingForNext` execution state for slow networks (#16143) --- .../nodes/render-types/CanvasNodeDefault.vue | 3 +- .../parts/CanvasNodeStatusIcons.vue | 3 +- .../src/composables/useCanvasMapping.test.ts | 93 +++++++++++++++++++ .../src/composables/useCanvasMapping.ts | 13 +++ .../src/composables/useCanvasNode.ts | 2 + .../src/composables/useExecutingNode.ts | 4 + .../handlers/nodeExecuteAfter.ts | 7 +- .../editor-ui/src/stores/workflows.store.ts | 2 + .../frontend/editor-ui/src/types/canvas.ts | 1 + 9 files changed, 120 insertions(+), 8 deletions(-) diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue index 1011c87da9..9cc934ce2b 100644 --- a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue +++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue @@ -30,6 +30,7 @@ const { hasPinnedData, executionStatus, executionWaiting, + executionWaitingForNext, executionRunning, hasRunData, hasIssues, @@ -61,7 +62,7 @@ const classes = computed(() => { [$style.error]: hasIssues.value, [$style.pinned]: hasPinnedData.value, [$style.waiting]: executionWaiting.value ?? executionStatus.value === 'waiting', - [$style.running]: executionRunning.value, + [$style.running]: executionRunning.value || executionWaitingForNext.value, [$style.configurable]: renderOptions.value.configurable, [$style.configuration]: renderOptions.value.configuration, [$style.trigger]: renderOptions.value.trigger, diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue index 0f45dab127..ecb1499906 100644 --- a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue +++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue @@ -16,6 +16,7 @@ const { hasIssues, executionStatus, executionWaiting, + executionWaitingForNext, executionRunning, hasRunData, runDataIterations, @@ -59,7 +60,7 @@ const dirtiness = computed(() =>
diff --git a/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts b/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts index 75ee5be9a9..72a08e8232 100644 --- a/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts +++ b/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts @@ -112,6 +112,7 @@ describe('useCanvasMapping', () => { status: 'new', running: false, waiting: undefined, + waitingForNext: false, }, issues: { items: [], @@ -1202,6 +1203,98 @@ describe('useCanvasMapping', () => { }); }); + describe('nodeExecutionWaitingForNextById', () => { + it('should be true when already executed node is waiting for next', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const node1 = createTestNode({ + name: 'Node 1', + }); + const node2 = createTestNode({ + name: 'Node 2', + }); + const nodes = [node1, node2]; + const connections = {}; + + const workflowObject = createTestWorkflowObject({ + nodes, + connections, + }); + + workflowsStore.executingNode = []; + workflowsStore.lastAddedExecutingNode = node1.name; + workflowsStore.isWorkflowRunning = true; + + const { nodeExecutionWaitingForNextById } = useCanvasMapping({ + nodes: ref(nodes), + connections: ref(connections), + workflowObject: ref(workflowObject) as Ref, + }); + + expect(nodeExecutionWaitingForNextById.value[node1.id]).toBe(true); + expect(nodeExecutionWaitingForNextById.value[node2.id]).toBe(false); + }); + + it('should be false when workflow is not executing', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const node1 = createTestNode({ + name: 'Node 1', + }); + const node2 = createTestNode({ + name: 'Node 2', + }); + const nodes = [node1, node2]; + const connections = {}; + + const workflowObject = createTestWorkflowObject({ + nodes, + connections, + }); + + workflowsStore.executingNode = []; + workflowsStore.lastAddedExecutingNode = node1.name; + workflowsStore.isWorkflowRunning = false; + + const { nodeExecutionWaitingForNextById } = useCanvasMapping({ + nodes: ref(nodes), + connections: ref(connections), + workflowObject: ref(workflowObject) as Ref, + }); + + expect(nodeExecutionWaitingForNextById.value[node1.id]).toBe(false); + expect(nodeExecutionWaitingForNextById.value[node2.id]).toBe(false); + }); + + it('should be false when there are nodes that are executing', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const node1 = createTestNode({ + name: 'Node 1', + }); + const node2 = createTestNode({ + name: 'Node 2', + }); + const nodes = [node1, node2]; + const connections = {}; + + const workflowObject = createTestWorkflowObject({ + nodes, + connections, + }); + + workflowsStore.executingNode = [node2.name]; + workflowsStore.lastAddedExecutingNode = node1.name; + workflowsStore.isWorkflowRunning = false; + + const { nodeExecutionWaitingForNextById } = useCanvasMapping({ + nodes: ref(nodes), + connections: ref(connections), + workflowObject: ref(workflowObject) as Ref, + }); + + expect(nodeExecutionWaitingForNextById.value[node1.id]).toBe(false); + expect(nodeExecutionWaitingForNextById.value[node2.id]).toBe(false); + }); + }); + describe('connections', () => { it('should map connections to canvas connections', () => { const [manualTriggerNode, setNode] = mockNodes.slice(0, 2); diff --git a/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts b/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts index b7d98e8b92..24358bf063 100644 --- a/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts +++ b/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts @@ -327,6 +327,17 @@ export function useCanvasMapping({ }, {}), ); + const nodeExecutionWaitingForNextById = computed(() => + nodes.value.reduce>((acc, node) => { + acc[node.id] = + node.name === workflowsStore.lastAddedExecutingNode && + workflowsStore.executingNode.length === 0 && + workflowsStore.isWorkflowRunning; + + return acc; + }, {}), + ); + const nodeExecutionStatusById = computed(() => nodes.value.reduce>((acc, node) => { const tasks = workflowsStore.getWorkflowRunData?.[node.name] ?? []; @@ -589,6 +600,7 @@ export function useCanvasMapping({ execution: { status: nodeExecutionStatusById.value[node.id], waiting: nodeExecutionWaitingById.value[node.id], + waitingForNext: nodeExecutionWaitingForNextById.value[node.id], running: nodeExecutionRunningById.value[node.id], }, runData: { @@ -704,6 +716,7 @@ export function useCanvasMapping({ return { additionalNodePropertiesById, nodeExecutionRunDataOutputMapById, + nodeExecutionWaitingForNextById, nodeIssuesById, nodeHasIssuesById, connections: mappedConnections, diff --git a/packages/frontend/editor-ui/src/composables/useCanvasNode.ts b/packages/frontend/editor-ui/src/composables/useCanvasNode.ts index d62279a8bb..f35aa578e8 100644 --- a/packages/frontend/editor-ui/src/composables/useCanvasNode.ts +++ b/packages/frontend/editor-ui/src/composables/useCanvasNode.ts @@ -52,6 +52,7 @@ export function useCanvasNode() { const executionStatus = computed(() => data.value.execution.status); const executionWaiting = computed(() => data.value.execution.waiting); + const executionWaitingForNext = computed(() => data.value.execution.waitingForNext); const executionRunning = computed(() => data.value.execution.running); const runDataOutputMap = computed(() => data.value.runData.outputMap); @@ -83,6 +84,7 @@ export function useCanvasNode() { hasIssues, executionStatus, executionWaiting, + executionWaitingForNext, executionRunning, render, eventBus, diff --git a/packages/frontend/editor-ui/src/composables/useExecutingNode.ts b/packages/frontend/editor-ui/src/composables/useExecutingNode.ts index ed6bfa109a..641aebdac1 100644 --- a/packages/frontend/editor-ui/src/composables/useExecutingNode.ts +++ b/packages/frontend/editor-ui/src/composables/useExecutingNode.ts @@ -14,9 +14,11 @@ import { ref } from 'vue'; */ export function useExecutingNode() { const executingNode = ref([]); + const lastAddedExecutingNode = ref(null); function addExecutingNode(nodeName: string) { executingNode.value.push(nodeName); + lastAddedExecutingNode.value = nodeName; } function removeExecutingNode(nodeName: string) { @@ -30,6 +32,7 @@ export function useExecutingNode() { function clearNodeExecutionQueue() { executingNode.value = []; + lastAddedExecutingNode.value = null; } function isNodeExecuting(nodeName: string): boolean { @@ -38,6 +41,7 @@ export function useExecutingNode() { return { executingNode, + lastAddedExecutingNode, addExecutingNode, removeExecutingNode, isNodeExecuting, diff --git a/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/nodeExecuteAfter.ts b/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/nodeExecuteAfter.ts index 32d116a179..39f575f8fb 100644 --- a/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/nodeExecuteAfter.ts +++ b/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/nodeExecuteAfter.ts @@ -27,12 +27,7 @@ export async function nodeExecuteAfter({ data: pushData }: NodeExecuteAfter) { } workflowsStore.updateNodeExecutionData(pushData); - - // Remove the node from the executing queue after a short delay - // To allow the running spinner to show for at least 50ms - setTimeout(() => { - workflowsStore.removeExecutingNode(pushData.nodeName); - }, 50); + workflowsStore.removeExecutingNode(pushData.nodeName); void assistantStore.onNodeExecution(pushData); void schemaPreviewStore.trackSchemaPreviewExecution(pushData); diff --git a/packages/frontend/editor-ui/src/stores/workflows.store.ts b/packages/frontend/editor-ui/src/stores/workflows.store.ts index b122fd1eb6..95dba1f305 100644 --- a/packages/frontend/editor-ui/src/stores/workflows.store.ts +++ b/packages/frontend/editor-ui/src/stores/workflows.store.ts @@ -163,6 +163,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { const { executingNode, + lastAddedExecutingNode, addExecutingNode, removeExecutingNode, isNodeExecuting, @@ -1938,6 +1939,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { subWorkflowExecutionError, executionWaitingForWebhook, executingNode, + lastAddedExecutingNode, workflowsById, nodeMetadata, isInDebugMode, diff --git a/packages/frontend/editor-ui/src/types/canvas.ts b/packages/frontend/editor-ui/src/types/canvas.ts index c0d7665aea..66dbb91ad3 100644 --- a/packages/frontend/editor-ui/src/types/canvas.ts +++ b/packages/frontend/editor-ui/src/types/canvas.ts @@ -123,6 +123,7 @@ export interface CanvasNodeData { status?: ExecutionStatus; waiting?: string; running: boolean; + waitingForNext?: boolean; }; runData: { outputMap: ExecutionOutputMap;