feat(editor): Always keep at least one executing node indicator in the workflow (#12829)

This commit is contained in:
Alex Grozav 2025-01-29 13:38:24 +02:00 committed by GitHub
parent 8da4f351e1
commit c25c613a04
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 166 additions and 10 deletions

View file

@ -0,0 +1,109 @@
import { useExecutingNode } from '@/composables/useExecutingNode';
describe('useExecutingNode', () => {
it('should always have at least one executing node during execution', () => {
const { executingNode, executingNodeCompletionQueue, addExecutingNode, removeExecutingNode } =
useExecutingNode();
addExecutingNode('node1');
expect(executingNode.value).toEqual(['node1']);
expect(executingNodeCompletionQueue.value).toEqual([]);
addExecutingNode('node2');
expect(executingNode.value).toEqual(['node1', 'node2']);
expect(executingNodeCompletionQueue.value).toEqual([]);
addExecutingNode('node3');
expect(executingNode.value).toEqual(['node1', 'node2', 'node3']);
expect(executingNodeCompletionQueue.value).toEqual([]);
removeExecutingNode('node1');
expect(executingNode.value).toEqual(['node2', 'node3']);
expect(executingNodeCompletionQueue.value).toEqual([]);
removeExecutingNode('node2');
expect(executingNode.value).toEqual(['node3']);
expect(executingNodeCompletionQueue.value).toEqual([]);
removeExecutingNode('node3');
expect(executingNode.value).toEqual(['node3']);
expect(executingNodeCompletionQueue.value).toEqual(['node3']);
addExecutingNode('node4');
expect(executingNode.value).toEqual(['node4']);
expect(executingNodeCompletionQueue.value).toEqual([]);
});
describe('resolveNodeExecutionQueue', () => {
it('should clear all nodes from the execution queue', () => {
const { executingNode, executingNodeCompletionQueue, resolveNodeExecutionQueue } =
useExecutingNode();
executingNode.value = ['node1', 'node2'];
executingNodeCompletionQueue.value = ['node1', 'node2'];
resolveNodeExecutionQueue();
expect(executingNode.value).toEqual([]);
expect(executingNodeCompletionQueue.value).toEqual([]);
});
it('should keep the last executing node if keepLastInQueue is true and only one node is executing', () => {
const { executingNode, executingNodeCompletionQueue, resolveNodeExecutionQueue } =
useExecutingNode();
executingNode.value = ['node1'];
executingNodeCompletionQueue.value = ['node1'];
resolveNodeExecutionQueue(true);
expect(executingNode.value).toEqual(['node1']);
expect(executingNodeCompletionQueue.value).toEqual(['node1']);
});
it('should remove all nodes except the last one if keepLastInQueue is true and more than one node is executing', () => {
const { executingNode, executingNodeCompletionQueue, resolveNodeExecutionQueue } =
useExecutingNode();
executingNode.value = ['node1', 'node2'];
executingNodeCompletionQueue.value = ['node1', 'node2'];
resolveNodeExecutionQueue(true);
expect(executingNode.value).toEqual(['node2']);
expect(executingNodeCompletionQueue.value).toEqual(['node2']);
});
it('should clear all nodes if keepLastInQueue is false', () => {
const { executingNode, executingNodeCompletionQueue, resolveNodeExecutionQueue } =
useExecutingNode();
executingNode.value = ['node1', 'node2'];
executingNodeCompletionQueue.value = ['node1', 'node2'];
resolveNodeExecutionQueue(false);
expect(executingNode.value).toEqual([]);
expect(executingNodeCompletionQueue.value).toEqual([]);
});
it('should handle empty execution queue gracefully', () => {
const { executingNode, executingNodeCompletionQueue, resolveNodeExecutionQueue } =
useExecutingNode();
executingNode.value = [];
executingNodeCompletionQueue.value = [];
resolveNodeExecutionQueue();
expect(executingNode.value).toEqual([]);
expect(executingNodeCompletionQueue.value).toEqual([]);
});
});
});

View file

@ -0,0 +1,52 @@
import { ref } from 'vue';
/**
* Composable to keep track of the currently executing node.
* The queue is used to keep track of the order in which nodes are completed and
* to ensure that there's always at least one node in the executing queue.
*
* The completion queue serves as a workaround for the fact that the execution status of a node
* is not updated in real-time when dealing with large amounts of data, meaning we can end up in a
* state where no node is actively executing, even though the workflow execution is not completed.
*/
export function useExecutingNode() {
const executingNode = ref<string[]>([]);
const executingNodeCompletionQueue = ref<string[]>([]);
function addExecutingNode(nodeName: string) {
resolveNodeExecutionQueue();
executingNode.value.push(nodeName);
}
function removeExecutingNode(nodeName: string) {
executingNodeCompletionQueue.value.push(nodeName);
resolveNodeExecutionQueue(
executingNode.value.length <= executingNodeCompletionQueue.value.length,
);
}
function resolveNodeExecutionQueue(keepLastInQueue = false) {
const lastExecutingNode = executingNodeCompletionQueue.value.at(-1);
const nodesToRemove = keepLastInQueue
? executingNodeCompletionQueue.value.slice(0, -1)
: executingNodeCompletionQueue.value;
executingNode.value = executingNode.value.filter((name) => !nodesToRemove.includes(name));
executingNodeCompletionQueue.value =
keepLastInQueue && lastExecutingNode ? [lastExecutingNode] : [];
}
function clearNodeExecutionQueue() {
executingNode.value = [];
executingNodeCompletionQueue.value = [];
}
return {
executingNode,
executingNodeCompletionQueue,
addExecutingNode,
removeExecutingNode,
resolveNodeExecutionQueue,
clearNodeExecutionQueue,
};
}

View file

@ -89,6 +89,7 @@ import { clearPopupWindowState, openFormPopupWindow } from '@/utils/executionUti
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useUsersStore } from '@/stores/users.store';
import { updateCurrentUserSettings } from '@/api/users';
import { useExecutingNode } from '@/composables/useExecutingNode';
const defaults: Omit<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = {
name: '',
@ -140,7 +141,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const activeExecutionId = ref<string | null>(null);
const subWorkflowExecutionError = ref<Error | null>(null);
const executionWaitingForWebhook = ref(false);
const executingNode = ref<string[]>([]);
const workflowsById = ref<Record<string, IWorkflowDb>>({});
const nodeMetadata = ref<NodeMetadataMap>({});
const isInDebugMode = ref(false);
@ -149,6 +149,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const isChatPanelOpen = ref(false);
const isLogsPanelOpen = ref(false);
const { executingNode, addExecutingNode, removeExecutingNode, clearNodeExecutionQueue } =
useExecutingNode();
const workflowName = computed(() => workflow.value.name);
const workflowId = computed(() => workflow.value.id);
@ -552,14 +555,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
executionWaitingForWebhook.value = false;
}
function addExecutingNode(nodeName: string) {
executingNode.value.push(nodeName);
}
function removeExecutingNode(nodeName: string) {
executingNode.value = executingNode.value.filter((name) => name !== nodeName);
}
function setWorkflowId(id?: string) {
workflow.value.id = !id || id === 'new' ? PLACEHOLDER_EMPTY_WORKFLOW_ID : id;
}
@ -1604,7 +1599,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
function markExecutionAsStopped() {
activeExecutionId.value = null;
executingNode.value.length = 0;
clearNodeExecutionQueue();
executionWaitingForWebhook.value = false;
uiStore.removeActiveAction('workflowRunning');
workflowHelpers.setDocumentTitle(workflowName.value, 'IDLE');