diff --git a/cypress/utils/executions.ts b/cypress/utils/executions.ts index be9a9f6d7d..96a6794489 100644 --- a/cypress/utils/executions.ts +++ b/cypress/utils/executions.ts @@ -1,5 +1,11 @@ import { stringify } from 'flatted'; -import type { IDataObject, ITaskData, ITaskDataConnections } from 'n8n-workflow'; +import type { + IDataObject, + IRunData, + IRunExecutionData, + ITaskData, + ITaskDataConnections, +} from 'n8n-workflow'; import { nanoid } from 'nanoid'; import { clickExecuteWorkflowButton } from '../composables/workflow'; @@ -53,6 +59,28 @@ export function runMockWorkflowExecution({ const workflowId = nanoid(); const executionId = Math.floor(Math.random() * 1_000_000).toString(); + const resolvedRunData = runData.reduce((acc, nodeExecution) => { + const nodeName = Object.keys(nodeExecution)[0]; + acc[nodeName] = [nodeExecution[nodeName]]; + return acc; + }, {}); + + const executionData: IRunExecutionData = { + startData: {}, + resultData: { + runData: resolvedRunData, + pinData: {}, + lastNodeExecuted, + }, + executionData: { + contextData: {}, + nodeExecutionStack: [], + metadata: {}, + waitingExecution: {}, + waitingExecutionSource: {}, + }, + }; + cy.intercept('POST', '/rest/workflows/**/run?**', { statusCode: 201, body: { @@ -70,7 +98,15 @@ export function runMockWorkflowExecution({ cy.wait('@runWorkflow'); - const resolvedRunData: Record = {}; + cy.push('executionStarted', { + workflowId, + executionId, + mode: 'manual', + startedAt: new Date(), + workflowName: '', + flattedRunData: '', + }); + runData.forEach((nodeExecution) => { const nodeName = Object.keys(nodeExecution)[0]; const nodeRunData = nodeExecution[nodeName]; @@ -85,28 +121,12 @@ export function runMockWorkflowExecution({ nodeName, data: nodeRunData, }); - - resolvedRunData[nodeName] = nodeExecution[nodeName]; }); cy.push('executionFinished', { executionId, workflowId, status: 'success', - rawData: stringify({ - startData: {}, - resultData: { - runData, - pinData: {}, - lastNodeExecuted, - }, - executionData: { - contextData: {}, - nodeExecutionStack: [], - metadata: {}, - waitingExecution: {}, - waitingExecutionSource: {}, - }, - }), + rawData: stringify(executionData), }); } diff --git a/packages/@n8n/api-types/src/push/collaboration.ts b/packages/@n8n/api-types/src/push/collaboration.ts index f2ec6fcc6c..c8d1f12a13 100644 --- a/packages/@n8n/api-types/src/push/collaboration.ts +++ b/packages/@n8n/api-types/src/push/collaboration.ts @@ -6,7 +6,7 @@ export type Collaborator = { lastSeen: Iso8601DateTimeString; }; -type CollaboratorsChanged = { +export type CollaboratorsChanged = { type: 'collaboratorsChanged'; data: { workflowId: string; diff --git a/packages/@n8n/api-types/src/push/debug.ts b/packages/@n8n/api-types/src/push/debug.ts index 99c8dc447f..030ca3b5ae 100644 --- a/packages/@n8n/api-types/src/push/debug.ts +++ b/packages/@n8n/api-types/src/push/debug.ts @@ -1,4 +1,4 @@ -type SendConsoleMessage = { +export type SendConsoleMessage = { type: 'sendConsoleMessage'; data: { source: string; diff --git a/packages/@n8n/api-types/src/push/execution.ts b/packages/@n8n/api-types/src/push/execution.ts index b16b73aeff..5476dbcab5 100644 --- a/packages/@n8n/api-types/src/push/execution.ts +++ b/packages/@n8n/api-types/src/push/execution.ts @@ -5,7 +5,7 @@ import type { WorkflowExecuteMode, } from 'n8n-workflow'; -type ExecutionStarted = { +export type ExecutionStarted = { type: 'executionStarted'; data: { executionId: string; @@ -18,14 +18,14 @@ type ExecutionStarted = { }; }; -type ExecutionWaiting = { +export type ExecutionWaiting = { type: 'executionWaiting'; data: { executionId: string; }; }; -type ExecutionFinished = { +export type ExecutionFinished = { type: 'executionFinished'; data: { executionId: string; @@ -36,14 +36,14 @@ type ExecutionFinished = { }; }; -type ExecutionRecovered = { +export type ExecutionRecovered = { type: 'executionRecovered'; data: { executionId: string; }; }; -type NodeExecuteBefore = { +export type NodeExecuteBefore = { type: 'nodeExecuteBefore'; data: { executionId: string; @@ -52,7 +52,7 @@ type NodeExecuteBefore = { }; }; -type NodeExecuteAfter = { +export type NodeExecuteAfter = { type: 'nodeExecuteAfter'; data: { executionId: string; diff --git a/packages/@n8n/api-types/src/push/hot-reload.ts b/packages/@n8n/api-types/src/push/hot-reload.ts index e8f9eba316..f752f798d6 100644 --- a/packages/@n8n/api-types/src/push/hot-reload.ts +++ b/packages/@n8n/api-types/src/push/hot-reload.ts @@ -1,19 +1,19 @@ -type NodeTypeData = { +export type NodeTypeData = { name: string; version: number; }; -type ReloadNodeType = { +export type ReloadNodeType = { type: 'reloadNodeType'; data: NodeTypeData; }; -type RemoveNodeType = { +export type RemoveNodeType = { type: 'removeNodeType'; data: NodeTypeData; }; -type NodeDescriptionUpdated = { +export type NodeDescriptionUpdated = { type: 'nodeDescriptionUpdated'; data: {}; }; diff --git a/packages/@n8n/api-types/src/push/webhook.ts b/packages/@n8n/api-types/src/push/webhook.ts index 31e2d9919f..bd796cc22d 100644 --- a/packages/@n8n/api-types/src/push/webhook.ts +++ b/packages/@n8n/api-types/src/push/webhook.ts @@ -1,4 +1,4 @@ -type TestWebhookDeleted = { +export type TestWebhookDeleted = { type: 'testWebhookDeleted'; data: { executionId?: string; @@ -6,7 +6,7 @@ type TestWebhookDeleted = { }; }; -type TestWebhookReceived = { +export type TestWebhookReceived = { type: 'testWebhookReceived'; data: { executionId: string; diff --git a/packages/@n8n/api-types/src/push/workflow.ts b/packages/@n8n/api-types/src/push/workflow.ts index 0dcf9ad78d..36d9da857e 100644 --- a/packages/@n8n/api-types/src/push/workflow.ts +++ b/packages/@n8n/api-types/src/push/workflow.ts @@ -1,11 +1,11 @@ -type WorkflowActivated = { +export type WorkflowActivated = { type: 'workflowActivated'; data: { workflowId: string; }; }; -type WorkflowFailedToActivate = { +export type WorkflowFailedToActivate = { type: 'workflowFailedToActivate'; data: { workflowId: string; @@ -13,7 +13,7 @@ type WorkflowFailedToActivate = { }; }; -type WorkflowDeactivated = { +export type WorkflowDeactivated = { type: 'workflowDeactivated'; data: { workflowId: string; diff --git a/packages/@n8n/utils/src/event-queue.test.ts b/packages/@n8n/utils/src/event-queue.test.ts new file mode 100644 index 0000000000..a7c351a74f --- /dev/null +++ b/packages/@n8n/utils/src/event-queue.test.ts @@ -0,0 +1,100 @@ +import { createEventQueue } from './event-queue'; + +describe('createEventQueue', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('should process events in order', async () => { + const processedEvents: string[] = []; + + // Create an async handler that pushes events into the processedEvents array. + const processEvent = vi.fn(async (event: string) => { + processedEvents.push(event); + // Simulate asynchronous delay of 10ms. + await new Promise((resolve) => setTimeout(resolve, 10)); + }); + + // Create the event queue. + const { enqueue } = createEventQueue(processEvent); + + // Enqueue events in a specific order. + enqueue('Event 1'); + enqueue('Event 2'); + enqueue('Event 3'); + + // Advance the timers enough to process all events. + // runAllTimersAsync() will run all pending timers and wait for any pending promise resolution. + await vi.runAllTimersAsync(); + + expect(processEvent).toHaveBeenCalledTimes(3); + expect(processedEvents).toEqual(['Event 1', 'Event 2', 'Event 3']); + }); + + it('should handle errors and continue processing', async () => { + const processedEvents: string[] = []; + const processEvent = vi.fn(async (event: string) => { + if (event === 'fail') { + throw new Error('Processing error'); // eslint-disable-line n8n-local-rules/no-plain-errors + } + processedEvents.push(event); + await new Promise((resolve) => setTimeout(resolve, 10)); + }); + const { enqueue } = createEventQueue(processEvent); + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + enqueue('Event A'); + enqueue('fail'); + enqueue('Event B'); + + await vi.runAllTimersAsync(); + + expect(processEvent).toHaveBeenCalledTimes(3); + // 'fail' should cause an error but processing continues. + expect(processedEvents).toEqual(['Event A', 'Event B']); + expect(consoleSpy).toHaveBeenCalledWith('Error processing event:', expect.any(Error)); + + consoleSpy.mockRestore(); + }); + + it('should not process any events if none are enqueued', async () => { + const processEvent = vi.fn(async (_event: string) => { + await new Promise((resolve) => setTimeout(resolve, 10)); + }); + + createEventQueue(processEvent); + + await vi.runAllTimersAsync(); + + // Did not enqueue any event. + expect(processEvent).not.toHaveBeenCalled(); + }); + + it('should ensure no concurrent processing of events', async () => { + let processingCounter = 0; + let maxConcurrent = 0; + + const processEvent = vi.fn(async (_event: string) => { + processingCounter++; + maxConcurrent = Math.max(maxConcurrent, processingCounter); + // Simulate asynchronous delay. + await new Promise((resolve) => setTimeout(resolve, 20)); + processingCounter--; + }); + + const { enqueue } = createEventQueue(processEvent); + + enqueue('A'); + enqueue('B'); + enqueue('C'); + + await vi.runAllTimersAsync(); + + // Throughout processing, maxConcurrent should remain 1. + expect(maxConcurrent).toEqual(1); + }); +}); diff --git a/packages/@n8n/utils/src/event-queue.ts b/packages/@n8n/utils/src/event-queue.ts new file mode 100644 index 0000000000..b8405c7480 --- /dev/null +++ b/packages/@n8n/utils/src/event-queue.ts @@ -0,0 +1,50 @@ +/** + * Create an event queue that processes events sequentially. + * + * @param processEvent - Async function that processes a single event. + * @returns A function that enqueues events for processing. + */ +export function createEventQueue(processEvent: (event: T) => Promise) { + // The internal queue holding events. + const queue: T[] = []; + + // Flag to indicate whether an event is currently being processed. + let processing = false; + + /** + * Process the next event in the queue (if not already processing). + */ + async function processNext(): Promise { + if (processing || queue.length === 0) { + return; + } + + processing = true; + const currentEvent = queue.shift(); + + if (currentEvent !== undefined) { + try { + await processEvent(currentEvent); + } catch (error) { + console.error('Error processing event:', error); + } + } + + processing = false; + + // Recursively process the next event. + await processNext(); + } + + /** + * Enqueue an event and trigger processing. + * + * @param event - The event to enqueue. + */ + function enqueue(event: T): void { + queue.push(event); + void processNext(); + } + + return { enqueue }; +} diff --git a/packages/@n8n/utils/src/retry.test.ts b/packages/@n8n/utils/src/retry.test.ts new file mode 100644 index 0000000000..38452db0c6 --- /dev/null +++ b/packages/@n8n/utils/src/retry.test.ts @@ -0,0 +1,122 @@ +import { retry } from './retry'; + +describe('retry', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.clearAllTimers(); + }); + + it('should resolve true when the function eventually returns true', async () => { + let callCount = 0; + const fn = vi.fn(async () => { + callCount++; + // Return true on the second attempt. + return callCount === 2; + }); + + const promise = retry(fn, 1000, 2, null); + + // The first call happens immediately. + expect(fn).toHaveBeenCalledTimes(1); + + // Advance timers by 1000ms asynchronously to allow the waiting period to complete. + await vi.advanceTimersByTimeAsync(1000); + + // After advancing, the second attempt should have occurred. + expect(fn).toHaveBeenCalledTimes(2); + + // The promise should now resolve with true. + const result = await promise; + expect(result).toBe(true); + }); + + it('should resolve false if maximum retries are reached with no success', async () => { + let callCount = 0; + const fn = vi.fn(async () => { + callCount++; + return false; + }); + + const promise = retry(fn, 1000, 3, null); + + // The first attempt fires immediately. + expect(fn).toHaveBeenCalledTimes(1); + + // Advance timers for the delay after the first attempt. + await vi.advanceTimersByTimeAsync(1000); + expect(fn).toHaveBeenCalledTimes(2); + + // Advance timers for the delay after the second attempt. + await vi.advanceTimersByTimeAsync(1000); + expect(fn).toHaveBeenCalledTimes(3); + + // With maxRetries reached (3 calls), promise should resolve to false. + const result = await promise; + expect(result).toBe(false); + }); + + it('should reject if the function throws an error', async () => { + const fn = vi.fn(async () => { + throw new Error('Test error'); // eslint-disable-line n8n-local-rules/no-plain-errors + }); + + // Since the error is thrown on the first call, no timer advancement is needed. + await expect(retry(fn, 1000, 3, null)).rejects.toThrow('Test error'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('should use linear backoff strategy', async () => { + let callCount = 0; + const fn = vi.fn(async () => { + callCount++; + return callCount === 4; // Return true on the fourth attempt. + }); + + const promise = retry(fn, 1000, 4, 'linear'); + + expect(fn).toHaveBeenCalledTimes(1); + + await vi.advanceTimersByTimeAsync(1000); // First backoff + expect(fn).toHaveBeenCalledTimes(2); + + await vi.advanceTimersByTimeAsync(2000); // Second backoff + expect(fn).toHaveBeenCalledTimes(3); + + await vi.advanceTimersByTimeAsync(3000); // Third backoff + expect(fn).toHaveBeenCalledTimes(4); + + const result = await promise; + expect(result).toBe(true); + }); + + it('should use exponential backoff strategy', async () => { + let callCount = 0; + const fn = vi.fn(async () => { + callCount++; + return callCount === 5; // Return true on the fifth attempt. + }); + + const promise = retry(fn, 1000, 5, 'exponential'); + + expect(fn).toHaveBeenCalledTimes(1); + + await vi.advanceTimersByTimeAsync(1000); // First backoff + expect(fn).toHaveBeenCalledTimes(2); + + await vi.advanceTimersByTimeAsync(2000); // Second backoff + expect(fn).toHaveBeenCalledTimes(3); + + await vi.advanceTimersByTimeAsync(4000); // Third backoff + expect(fn).toHaveBeenCalledTimes(4); + + await vi.advanceTimersByTimeAsync(8000); // Fourth backoff + expect(fn).toHaveBeenCalledTimes(5); + + const result = await promise; + expect(result).toBe(true); + }); +}); diff --git a/packages/@n8n/utils/src/retry.ts b/packages/@n8n/utils/src/retry.ts new file mode 100644 index 0000000000..a943b023f8 --- /dev/null +++ b/packages/@n8n/utils/src/retry.ts @@ -0,0 +1,51 @@ +type RetryFn = () => boolean | Promise; + +/** + * A utility that retries a function every `interval` milliseconds + * until the function returns true or the maximum number of retries is reached. + * + * @param fn - A function that returns a boolean or a Promise resolving to a boolean. + * @param interval - The time interval (in milliseconds) between each retry. Defaults to 1000. + * @param maxRetries - The maximum number of retry attempts. Defaults to 3. + * @param backoff - The backoff strategy to use: 'linear', 'exponential', or null. + * @returns {Promise} - A promise that resolves to: + * - true: If the function returns true before reaching maxRetries. + * - false: If the function never returns true or if an error occurs. + */ +export async function retry( + fn: RetryFn, + interval: number = 1000, + maxRetries: number = 3, + backoff: 'exponential' | 'linear' | null = 'linear', +): Promise { + let attempt = 0; + + while (attempt < maxRetries) { + attempt++; + try { + const result = await fn(); + if (result) { + return true; + } + } catch (error) { + console.error('Error during retry:', error); + throw error; + } + + // Wait for the specified interval before the next attempt, if any attempts remain. + if (attempt < maxRetries) { + let computedInterval = interval; + + if (backoff === 'linear') { + computedInterval = interval * attempt; + } else if (backoff === 'exponential') { + computedInterval = Math.pow(2, attempt - 1) * interval; + computedInterval = Math.min(computedInterval, 30000); // Cap the maximum interval to 30 seconds + } + + await new Promise((resolve) => setTimeout(resolve, computedInterval)); + } + } + + return false; +} diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts index f35a6e8ffb..398257cac0 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts +++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts @@ -221,7 +221,7 @@ describe('LogsPanel', () => { expect(rendered.getByText('Running')).toBeInTheDocument(); expect(rendered.queryByText('AI Agent')).not.toBeInTheDocument(); - workflowsStore.setNodeExecuting({ + workflowsStore.addNodeExecutionData({ nodeName: 'AI Agent', executionId: '567', data: { executionIndex: 0, startTime: Date.parse('2025-04-20T12:34:51.000Z'), source: [] }, @@ -243,7 +243,6 @@ describe('LogsPanel', () => { executionStatus: 'success', }, }); - expect(await treeItem.findByText('AI Agent')).toBeInTheDocument(); expect(treeItem.getByText('Success in 33ms')).toBeInTheDocument(); diff --git a/packages/frontend/editor-ui/src/components/InputPanel.vue b/packages/frontend/editor-ui/src/components/InputPanel.vue index b1cb3b00c0..0f2813c9ae 100644 --- a/packages/frontend/editor-ui/src/components/InputPanel.vue +++ b/packages/frontend/editor-ui/src/components/InputPanel.vue @@ -8,8 +8,8 @@ import { START_NODE_TYPE, } from '@/constants'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; -import { useUIStore } from '@/stores/ui.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; +import { useNDVStore } from '@/stores/ndv.store'; import { waitingNodeTooltip } from '@/utils/executionUtils'; import { uniqBy } from 'lodash-es'; import { N8nIcon, N8nRadioButtons, N8nText, N8nTooltip } from '@n8n/design-system'; @@ -23,7 +23,6 @@ import { } from 'n8n-workflow'; import { storeToRefs } from 'pinia'; import { computed, ref, watch } from 'vue'; -import { useNDVStore } from '../stores/ndv.store'; import InputNodeSelect from './InputNodeSelect.vue'; import NodeExecuteButton from './NodeExecuteButton.vue'; import RunData from './RunData.vue'; @@ -90,7 +89,6 @@ const inputModes = [ const nodeTypesStore = useNodeTypesStore(); const ndvStore = useNDVStore(); const workflowsStore = useWorkflowsStore(); -const uiStore = useUIStore(); const { activeNode, @@ -166,7 +164,7 @@ const isMappingEnabled = computed(() => { return true; }); const isExecutingPrevious = computed(() => { - if (!workflowRunning.value) { + if (!workflowsStore.isWorkflowRunning) { return false; } const triggeredNode = workflowsStore.executedNode; @@ -187,7 +185,6 @@ const isExecutingPrevious = computed(() => { } return false; }); -const workflowRunning = computed(() => uiStore.isActionActive.workflowRunning); const rootNodesParents = computed(() => { if (!rootNode.value) return []; diff --git a/packages/frontend/editor-ui/src/components/NodeDetailsView.vue b/packages/frontend/editor-ui/src/components/NodeDetailsView.vue index 5b31150d81..21ea79ea1f 100644 --- a/packages/frontend/editor-ui/src/components/NodeDetailsView.vue +++ b/packages/frontend/editor-ui/src/components/NodeDetailsView.vue @@ -31,7 +31,6 @@ import { dataPinningEventBus, ndvEventBus } from '@/event-bus'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { useNDVStore } from '@/stores/ndv.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; -import { useUIStore } from '@/stores/ui.store'; import { useSettingsStore } from '@/stores/settings.store'; import { useDeviceSupport } from '@n8n/composables/useDeviceSupport'; import { useNodeHelpers } from '@/composables/useNodeHelpers'; @@ -72,7 +71,6 @@ const { activeNode } = storeToRefs(ndvStore); const pinnedData = usePinnedData(activeNode); const workflowActivate = useWorkflowActivate(); const nodeTypesStore = useNodeTypesStore(); -const uiStore = useUIStore(); const workflowsStore = useWorkflowsStore(); const settingsStore = useSettingsStore(); const deviceSupport = useDeviceSupport(); @@ -108,14 +106,12 @@ const activeNodeType = computed(() => { return null; }); -const workflowRunning = computed(() => uiStore.isActionActive.workflowRunning); - const showTriggerWaitingWarning = computed( () => triggerWaitingWarningEnabled.value && !!activeNodeType.value && !activeNodeType.value.group.includes('trigger') && - workflowRunning.value && + workflowsStore.isWorkflowRunning && workflowsStore.executionWaitingForWebhook, ); @@ -327,11 +323,11 @@ const featureRequestUrl = computed(() => { const outputPanelEditMode = computed(() => ndvStore.outputPanelEditMode); -const isWorkflowRunning = computed(() => uiStore.isActionActive.workflowRunning); - const isExecutionWaitingForWebhook = computed(() => workflowsStore.executionWaitingForWebhook); -const blockUi = computed(() => isWorkflowRunning.value || isExecutionWaitingForWebhook.value); +const blockUi = computed( + () => workflowsStore.isWorkflowRunning || isExecutionWaitingForWebhook.value, +); const foreignCredentials = computed(() => { const credentials = activeNode.value?.credentials; @@ -470,7 +466,7 @@ const onUnlinkRun = (pane: string) => { const onNodeExecute = () => { setTimeout(() => { - if (!activeNode.value || !workflowRunning.value) { + if (!activeNode.value || !workflowsStore.isWorkflowRunning) { return; } triggerWaitingWarningEnabled.value = true; diff --git a/packages/frontend/editor-ui/src/components/NodeExecuteButton.test.ts b/packages/frontend/editor-ui/src/components/NodeExecuteButton.test.ts index 4632dac484..c22f4b63a1 100644 --- a/packages/frontend/editor-ui/src/components/NodeExecuteButton.test.ts +++ b/packages/frontend/editor-ui/src/components/NodeExecuteButton.test.ts @@ -15,7 +15,6 @@ import { } from '@/constants'; import NodeExecuteButton from '@/components/NodeExecuteButton.vue'; import { useWorkflowsStore } from '@/stores/workflows.store'; -import { useUIStore } from '@/stores/ui.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNDVStore } from '@/stores/ndv.store'; import { useRunWorkflow } from '@/composables/useRunWorkflow'; @@ -84,7 +83,6 @@ vi.mock('@/composables/useMessage', () => { let renderComponent: ReturnType; let workflowsStore: MockedStore; -let uiStore: MockedStore; let nodeTypesStore: MockedStore; let ndvStore: MockedStore; @@ -109,7 +107,6 @@ describe('NodeExecuteButton', () => { }); workflowsStore = mockedStore(useWorkflowsStore); - uiStore = mockedStore(useUIStore); nodeTypesStore = mockedStore(useNodeTypesStore); ndvStore = mockedStore(useNDVStore); @@ -193,7 +190,7 @@ describe('NodeExecuteButton', () => { workflowsStore.getNodeByName.mockReturnValue(node); workflowsStore.isNodeExecuting = vi.fn(() => true); nodeTypesStore.isTriggerNode = () => true; - uiStore.isActionActive.workflowRunning = true; + workflowsStore.isWorkflowRunning = true; const { getByRole } = renderComponent(); expect(getByRole('button').textContent).toBe('Stop Listening'); @@ -203,7 +200,7 @@ describe('NodeExecuteButton', () => { const node = mockNode({ name: 'test-node', type: SET_NODE_TYPE }); workflowsStore.getNodeByName.mockReturnValue(node); workflowsStore.isNodeExecuting = vi.fn(() => true); - uiStore.isActionActive.workflowRunning = true; + workflowsStore.isWorkflowRunning = true; const { getByRole } = renderComponent(); expect(getByRole('button').querySelector('.n8n-spinner')).toBeVisible(); @@ -227,7 +224,7 @@ describe('NodeExecuteButton', () => { }); it('should be disabled when workflow is running but node is not executing', async () => { - uiStore.isActionActive.workflowRunning = true; + workflowsStore.isWorkflowRunning = true; workflowsStore.isNodeExecuting.mockReturnValue(false); workflowsStore.getNodeByName.mockReturnValue( mockNode({ name: 'test-node', type: SET_NODE_TYPE }), @@ -277,7 +274,7 @@ describe('NodeExecuteButton', () => { }); it('stops execution when clicking button while workflow is running', async () => { - uiStore.isActionActive.workflowRunning = true; + workflowsStore.isWorkflowRunning = true; nodeTypesStore.isTriggerNode = () => true; workflowsStore.setActiveExecutionId('test-execution-id'); workflowsStore.isNodeExecuting.mockReturnValue(true); diff --git a/packages/frontend/editor-ui/src/components/NodeExecuteButton.vue b/packages/frontend/editor-ui/src/components/NodeExecuteButton.vue index dd31507cff..607e9a4af2 100644 --- a/packages/frontend/editor-ui/src/components/NodeExecuteButton.vue +++ b/packages/frontend/editor-ui/src/components/NodeExecuteButton.vue @@ -22,7 +22,6 @@ import { useExternalHooks } from '@/composables/useExternalHooks'; import { nodeViewEventBus } from '@/event-bus'; import { usePinnedData } from '@/composables/usePinnedData'; import { useRunWorkflow } from '@/composables/useRunWorkflow'; -import { useUIStore } from '@/stores/ui.store'; import { useRouter } from 'vue-router'; import { useI18n } from '@/composables/useI18n'; import { useTelemetry } from '@/composables/useTelemetry'; @@ -72,7 +71,6 @@ const externalHooks = useExternalHooks(); const toast = useToast(); const ndvStore = useNDVStore(); const nodeTypesStore = useNodeTypesStore(); -const uiStore = useUIStore(); const i18n = useI18n(); const message = useMessage(); const telemetry = useTelemetry(); @@ -85,7 +83,7 @@ const nodeType = computed((): INodeTypeDescription | null => { }); const isNodeRunning = computed(() => { - if (!uiStore.isActionActive.workflowRunning || codeGenerationInProgress.value) return false; + if (!workflowsStore.isWorkflowRunning || codeGenerationInProgress.value) return false; const triggeredNode = workflowsStore.executedNode; return ( workflowsStore.isNodeExecuting(node.value?.name ?? '') || triggeredNode === node.value?.name @@ -96,8 +94,6 @@ const isTriggerNode = computed(() => { return node.value ? nodeTypesStore.isTriggerNode(node.value.type) : false; }); -const isWorkflowRunning = computed(() => uiStore.isActionActive.workflowRunning); - const isManualTriggerNode = computed(() => nodeType.value ? nodeType.value.name === MANUAL_TRIGGER_NODE_TYPE : false, ); @@ -168,7 +164,7 @@ const disabledHint = computed(() => { return i18n.baseText('ndv.execute.requiredFieldsMissing'); } - if (isWorkflowRunning.value && !isNodeRunning.value) { + if (workflowsStore.isWorkflowRunning && !isNodeRunning.value) { return i18n.baseText('ndv.execute.workflowAlreadyRunning'); } diff --git a/packages/frontend/editor-ui/src/components/OutputPanel.vue b/packages/frontend/editor-ui/src/components/OutputPanel.vue index 78d5813842..c1704bec9e 100644 --- a/packages/frontend/editor-ui/src/components/OutputPanel.vue +++ b/packages/frontend/editor-ui/src/components/OutputPanel.vue @@ -9,7 +9,6 @@ import { import RunData from './RunData.vue'; import RunInfo from './RunInfo.vue'; import { storeToRefs } from 'pinia'; -import { useUIStore } from '@/stores/ui.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { useNDVStore } from '@/stores/ndv.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; @@ -77,7 +76,6 @@ const emit = defineEmits<{ const ndvStore = useNDVStore(); const nodeTypesStore = useNodeTypesStore(); const workflowsStore = useWorkflowsStore(); -const uiStore = useUIStore(); const telemetry = useTelemetry(); const i18n = useI18n(); const { activeNode } = storeToRefs(ndvStore); @@ -144,7 +142,7 @@ const isNodeRunning = computed(() => { return workflowRunning.value && !!node.value && workflowsStore.isNodeExecuting(node.value.name); }); -const workflowRunning = computed(() => uiStore.isActionActive.workflowRunning); +const workflowRunning = computed(() => workflowsStore.isWorkflowRunning); const workflowExecution = computed(() => { return workflowsStore.getWorkflowExecution; diff --git a/packages/frontend/editor-ui/src/components/TriggerPanel.vue b/packages/frontend/editor-ui/src/components/TriggerPanel.vue index 888fbe291c..ab0f804188 100644 --- a/packages/frontend/editor-ui/src/components/TriggerPanel.vue +++ b/packages/frontend/editor-ui/src/components/TriggerPanel.vue @@ -162,9 +162,7 @@ const isListeningForEvents = computed(() => { ); }); -const workflowRunning = computed(() => { - return uiStore.isActionActive.workflowRunning; -}); +const workflowRunning = computed(() => workflowsStore.isWorkflowRunning); const isActivelyPolling = computed(() => { const triggeredNode = workflowsStore.executedNode; 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 5e10afee58..03d35003a8 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,7 +16,7 @@ const { hasIssues, executionStatus, executionWaiting, - executionRunningThrottled, + executionRunning, hasRunData, runDataIterations, isDisabled, @@ -55,6 +55,16 @@ const dirtiness = computed(() => +
+ +
+
+ +
>
-
- -
-
- -