fix(editor): Add visual-only waitingForNext execution state for slow networks (#16143)

This commit is contained in:
Alex Grozav
2025-06-10 09:32:08 +02:00
committed by GitHub
parent 00083d5ed6
commit 5deab75c7d
9 changed files with 120 additions and 8 deletions

View File

@@ -30,6 +30,7 @@ const {
hasPinnedData, hasPinnedData,
executionStatus, executionStatus,
executionWaiting, executionWaiting,
executionWaitingForNext,
executionRunning, executionRunning,
hasRunData, hasRunData,
hasIssues, hasIssues,
@@ -61,7 +62,7 @@ const classes = computed(() => {
[$style.error]: hasIssues.value, [$style.error]: hasIssues.value,
[$style.pinned]: hasPinnedData.value, [$style.pinned]: hasPinnedData.value,
[$style.waiting]: executionWaiting.value ?? executionStatus.value === 'waiting', [$style.waiting]: executionWaiting.value ?? executionStatus.value === 'waiting',
[$style.running]: executionRunning.value, [$style.running]: executionRunning.value || executionWaitingForNext.value,
[$style.configurable]: renderOptions.value.configurable, [$style.configurable]: renderOptions.value.configurable,
[$style.configuration]: renderOptions.value.configuration, [$style.configuration]: renderOptions.value.configuration,
[$style.trigger]: renderOptions.value.trigger, [$style.trigger]: renderOptions.value.trigger,

View File

@@ -16,6 +16,7 @@ const {
hasIssues, hasIssues,
executionStatus, executionStatus,
executionWaiting, executionWaiting,
executionWaitingForNext,
executionRunning, executionRunning,
hasRunData, hasRunData,
runDataIterations, runDataIterations,
@@ -59,7 +60,7 @@ const dirtiness = computed(() =>
<!-- Do nothing, unknown means the node never executed --> <!-- Do nothing, unknown means the node never executed -->
</div> </div>
<div <div
v-else-if="executionRunning || executionStatus === 'running'" v-else-if="executionRunning || executionWaitingForNext || executionStatus === 'running'"
data-test-id="canvas-node-status-running" data-test-id="canvas-node-status-running"
:class="[$style.status, $style.running]" :class="[$style.status, $style.running]"
> >

View File

@@ -112,6 +112,7 @@ describe('useCanvasMapping', () => {
status: 'new', status: 'new',
running: false, running: false,
waiting: undefined, waiting: undefined,
waitingForNext: false,
}, },
issues: { issues: {
items: [], 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<Workflow>,
});
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<Workflow>,
});
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<Workflow>,
});
expect(nodeExecutionWaitingForNextById.value[node1.id]).toBe(false);
expect(nodeExecutionWaitingForNextById.value[node2.id]).toBe(false);
});
});
describe('connections', () => { describe('connections', () => {
it('should map connections to canvas connections', () => { it('should map connections to canvas connections', () => {
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2); const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);

View File

@@ -327,6 +327,17 @@ export function useCanvasMapping({
}, {}), }, {}),
); );
const nodeExecutionWaitingForNextById = computed(() =>
nodes.value.reduce<Record<string, boolean>>((acc, node) => {
acc[node.id] =
node.name === workflowsStore.lastAddedExecutingNode &&
workflowsStore.executingNode.length === 0 &&
workflowsStore.isWorkflowRunning;
return acc;
}, {}),
);
const nodeExecutionStatusById = computed(() => const nodeExecutionStatusById = computed(() =>
nodes.value.reduce<Record<string, ExecutionStatus>>((acc, node) => { nodes.value.reduce<Record<string, ExecutionStatus>>((acc, node) => {
const tasks = workflowsStore.getWorkflowRunData?.[node.name] ?? []; const tasks = workflowsStore.getWorkflowRunData?.[node.name] ?? [];
@@ -589,6 +600,7 @@ export function useCanvasMapping({
execution: { execution: {
status: nodeExecutionStatusById.value[node.id], status: nodeExecutionStatusById.value[node.id],
waiting: nodeExecutionWaitingById.value[node.id], waiting: nodeExecutionWaitingById.value[node.id],
waitingForNext: nodeExecutionWaitingForNextById.value[node.id],
running: nodeExecutionRunningById.value[node.id], running: nodeExecutionRunningById.value[node.id],
}, },
runData: { runData: {
@@ -704,6 +716,7 @@ export function useCanvasMapping({
return { return {
additionalNodePropertiesById, additionalNodePropertiesById,
nodeExecutionRunDataOutputMapById, nodeExecutionRunDataOutputMapById,
nodeExecutionWaitingForNextById,
nodeIssuesById, nodeIssuesById,
nodeHasIssuesById, nodeHasIssuesById,
connections: mappedConnections, connections: mappedConnections,

View File

@@ -52,6 +52,7 @@ export function useCanvasNode() {
const executionStatus = computed(() => data.value.execution.status); const executionStatus = computed(() => data.value.execution.status);
const executionWaiting = computed(() => data.value.execution.waiting); const executionWaiting = computed(() => data.value.execution.waiting);
const executionWaitingForNext = computed(() => data.value.execution.waitingForNext);
const executionRunning = computed(() => data.value.execution.running); const executionRunning = computed(() => data.value.execution.running);
const runDataOutputMap = computed(() => data.value.runData.outputMap); const runDataOutputMap = computed(() => data.value.runData.outputMap);
@@ -83,6 +84,7 @@ export function useCanvasNode() {
hasIssues, hasIssues,
executionStatus, executionStatus,
executionWaiting, executionWaiting,
executionWaitingForNext,
executionRunning, executionRunning,
render, render,
eventBus, eventBus,

View File

@@ -14,9 +14,11 @@ import { ref } from 'vue';
*/ */
export function useExecutingNode() { export function useExecutingNode() {
const executingNode = ref<string[]>([]); const executingNode = ref<string[]>([]);
const lastAddedExecutingNode = ref<string | null>(null);
function addExecutingNode(nodeName: string) { function addExecutingNode(nodeName: string) {
executingNode.value.push(nodeName); executingNode.value.push(nodeName);
lastAddedExecutingNode.value = nodeName;
} }
function removeExecutingNode(nodeName: string) { function removeExecutingNode(nodeName: string) {
@@ -30,6 +32,7 @@ export function useExecutingNode() {
function clearNodeExecutionQueue() { function clearNodeExecutionQueue() {
executingNode.value = []; executingNode.value = [];
lastAddedExecutingNode.value = null;
} }
function isNodeExecuting(nodeName: string): boolean { function isNodeExecuting(nodeName: string): boolean {
@@ -38,6 +41,7 @@ export function useExecutingNode() {
return { return {
executingNode, executingNode,
lastAddedExecutingNode,
addExecutingNode, addExecutingNode,
removeExecutingNode, removeExecutingNode,
isNodeExecuting, isNodeExecuting,

View File

@@ -27,12 +27,7 @@ export async function nodeExecuteAfter({ data: pushData }: NodeExecuteAfter) {
} }
workflowsStore.updateNodeExecutionData(pushData); workflowsStore.updateNodeExecutionData(pushData);
workflowsStore.removeExecutingNode(pushData.nodeName);
// 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);
void assistantStore.onNodeExecution(pushData); void assistantStore.onNodeExecution(pushData);
void schemaPreviewStore.trackSchemaPreviewExecution(pushData); void schemaPreviewStore.trackSchemaPreviewExecution(pushData);

View File

@@ -163,6 +163,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const { const {
executingNode, executingNode,
lastAddedExecutingNode,
addExecutingNode, addExecutingNode,
removeExecutingNode, removeExecutingNode,
isNodeExecuting, isNodeExecuting,
@@ -1938,6 +1939,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
subWorkflowExecutionError, subWorkflowExecutionError,
executionWaitingForWebhook, executionWaitingForWebhook,
executingNode, executingNode,
lastAddedExecutingNode,
workflowsById, workflowsById,
nodeMetadata, nodeMetadata,
isInDebugMode, isInDebugMode,

View File

@@ -123,6 +123,7 @@ export interface CanvasNodeData {
status?: ExecutionStatus; status?: ExecutionStatus;
waiting?: string; waiting?: string;
running: boolean; running: boolean;
waitingForNext?: boolean;
}; };
runData: { runData: {
outputMap: ExecutionOutputMap; outputMap: ExecutionOutputMap;