fix(core): Bring back execution data on the executionFinished push message (#11821)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2024-11-22 19:09:48 +01:00
committed by GitHub
parent 13cc5abb7f
commit 03135702f1
5 changed files with 79 additions and 96 deletions

View File

@@ -1,5 +1,6 @@
import { stringify } from 'flatted'; import { stringify } from 'flatted';
import type { IDataObject, IPinData, ITaskData, ITaskDataConnections } from 'n8n-workflow'; import type { IDataObject, ITaskData, ITaskDataConnections } from 'n8n-workflow';
import { nanoid } from 'nanoid';
import { clickExecuteWorkflowButton } from '../composables/workflow'; import { clickExecuteWorkflowButton } from '../composables/workflow';
@@ -39,38 +40,6 @@ export function createMockNodeExecutionData(
}; };
} }
function createMockWorkflowExecutionData({
runData,
lastNodeExecuted,
}: {
runData: Record<string, ITaskData | ITaskData[]>;
pinData?: IPinData;
lastNodeExecuted: string;
}) {
return {
data: stringify({
startData: {},
resultData: {
runData,
pinData: {},
lastNodeExecuted,
},
executionData: {
contextData: {},
nodeExecutionStack: [],
metadata: {},
waitingExecution: {},
waitingExecutionSource: {},
},
}),
mode: 'manual',
startedAt: new Date().toISOString(),
stoppedAt: new Date().toISOString(),
status: 'success',
finished: true,
};
}
export function runMockWorkflowExecution({ export function runMockWorkflowExecution({
trigger, trigger,
lastNodeExecuted, lastNodeExecuted,
@@ -80,6 +49,7 @@ export function runMockWorkflowExecution({
lastNodeExecuted: string; lastNodeExecuted: string;
runData: Array<ReturnType<typeof createMockNodeExecutionData>>; runData: Array<ReturnType<typeof createMockNodeExecutionData>>;
}) { }) {
const workflowId = nanoid();
const executionId = Math.floor(Math.random() * 1_000_000).toString(); const executionId = Math.floor(Math.random() * 1_000_000).toString();
cy.intercept('POST', '/rest/workflows/**/run?**', { cy.intercept('POST', '/rest/workflows/**/run?**', {
@@ -117,17 +87,24 @@ export function runMockWorkflowExecution({
resolvedRunData[nodeName] = nodeExecution[nodeName]; resolvedRunData[nodeName] = nodeExecution[nodeName];
}); });
cy.intercept('GET', `/rest/executions/${executionId}`, { cy.push('executionFinished', {
statusCode: 200, executionId,
body: { workflowId,
data: createMockWorkflowExecutionData({ status: 'success',
rawData: stringify({
startData: {},
resultData: {
runData,
pinData: {},
lastNodeExecuted, lastNodeExecuted,
runData: resolvedRunData, },
}), executionData: {
}, contextData: {},
}).as('getExecution'); nodeExecutionStack: [],
metadata: {},
cy.push('executionFinished', { executionId }); waitingExecution: {},
waitingExecutionSource: {},
cy.wait('@getExecution'); },
}),
});
} }

View File

@@ -1,4 +1,4 @@
import type { ITaskData, WorkflowExecuteMode } from 'n8n-workflow'; import type { ExecutionStatus, ITaskData, WorkflowExecuteMode } from 'n8n-workflow';
type ExecutionStarted = { type ExecutionStarted = {
type: 'executionStarted'; type: 'executionStarted';
@@ -23,6 +23,10 @@ type ExecutionFinished = {
type: 'executionFinished'; type: 'executionFinished';
data: { data: {
executionId: string; executionId: string;
workflowId: string;
status: ExecutionStatus;
/** @deprecated: Please construct execution data in the frontend from the data pushed in previous messages, instead of depending on this additional payload serialization */
rawData?: string;
}; };
}; };

View File

@@ -5,6 +5,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
import type { PushType } from '@n8n/api-types'; import type { PushType } from '@n8n/api-types';
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
import { stringify } from 'flatted';
import { WorkflowExecute } from 'n8n-core'; import { WorkflowExecute } from 'n8n-core';
import { import {
ApplicationError, ApplicationError,
@@ -318,9 +319,17 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
workflowId, workflowId,
}); });
const pushType = const { status } = fullRunData;
fullRunData.status === 'waiting' ? 'executionWaiting' : 'executionFinished'; if (status === 'waiting') {
pushInstance.send(pushType, { executionId }, pushRef); pushInstance.send('executionWaiting', { executionId }, pushRef);
} else {
const rawData = stringify(fullRunData.data);
pushInstance.send(
'executionFinished',
{ executionId, workflowId, status, rawData },
pushRef,
);
}
}, },
], ],
}; };

View File

@@ -2,7 +2,6 @@ import { stringify } from 'flatted';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { createPinia, setActivePinia } from 'pinia'; import { createPinia, setActivePinia } from 'pinia';
import type { PushMessage, PushPayload } from '@n8n/api-types'; import type { PushMessage, PushPayload } from '@n8n/api-types';
import { mock } from 'vitest-mock-extended';
import type { ITaskData, WorkflowOperationError } from 'n8n-workflow'; import type { ITaskData, WorkflowOperationError } from 'n8n-workflow';
import { usePushConnection } from '@/composables/usePushConnection'; import { usePushConnection } from '@/composables/usePushConnection';
@@ -11,7 +10,6 @@ import { useOrchestrationStore } from '@/stores/orchestration.store';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import type { IExecutionResponse } from '@/Interface';
vi.mock('vue-router', () => { vi.mock('vue-router', () => {
return { return {
@@ -140,10 +138,7 @@ describe('usePushConnection()', () => {
describe('executionFinished', () => { describe('executionFinished', () => {
const executionId = '1'; const executionId = '1';
const event: PushMessage = { const workflowId = 'abc';
type: 'executionFinished',
data: { executionId: '1' },
};
beforeEach(() => { beforeEach(() => {
workflowsStore.activeExecutionId = executionId; workflowsStore.activeExecutionId = executionId;
@@ -151,28 +146,23 @@ describe('usePushConnection()', () => {
}); });
it('should handle executionFinished event correctly', async () => { it('should handle executionFinished event correctly', async () => {
const spy = vi.spyOn(workflowsStore, 'fetchExecutionDataById').mockResolvedValue( const result = await pushConnection.pushMessageReceived({
mock<IExecutionResponse>({ type: 'executionFinished',
id: executionId, data: {
data: stringify({ executionId,
workflowId,
status: 'success',
rawData: stringify({
resultData: { resultData: {
runData: {}, runData: {},
}, },
}) as unknown as IExecutionResponse['data'], }),
finished: true, },
mode: 'manual', });
startedAt: new Date(),
stoppedAt: new Date(),
status: 'success',
}),
);
const result = await pushConnection.pushMessageReceived(event);
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(workflowsStore.workflowExecutionData).toBeDefined(); expect(workflowsStore.workflowExecutionData).toBeDefined();
expect(uiStore.isActionActive['workflowRunning']).toBeTruthy(); expect(uiStore.isActionActive['workflowRunning']).toBeTruthy();
expect(spy).toHaveBeenCalledWith(executionId);
expect(toast.showMessage).toHaveBeenCalledWith({ expect(toast.showMessage).toHaveBeenCalledWith({
title: 'Workflow executed successfully', title: 'Workflow executed successfully',
@@ -181,10 +171,13 @@ describe('usePushConnection()', () => {
}); });
it('should handle isManualExecutionCancelled correctly', async () => { it('should handle isManualExecutionCancelled correctly', async () => {
const spy = vi.spyOn(workflowsStore, 'fetchExecutionDataById').mockResolvedValue( const result = await pushConnection.pushMessageReceived({
mock<IExecutionResponse>({ type: 'executionFinished',
id: executionId, data: {
data: stringify({ executionId,
workflowId,
status: 'error',
rawData: stringify({
startData: {}, startData: {},
resultData: { resultData: {
runData: { runData: {
@@ -198,14 +191,9 @@ describe('usePushConnection()', () => {
node: 'Last Node', node: 'Last Node',
} as unknown as WorkflowOperationError, } as unknown as WorkflowOperationError,
}, },
}) as unknown as IExecutionResponse['data'], }),
mode: 'manual', },
startedAt: new Date(), });
status: 'running',
}),
);
const result = await pushConnection.pushMessageReceived(event);
expect(useToast().showMessage).toHaveBeenCalledWith({ expect(useToast().showMessage).toHaveBeenCalledWith({
message: message:
@@ -219,7 +207,6 @@ describe('usePushConnection()', () => {
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(workflowsStore.workflowExecutionData).toBeDefined(); expect(workflowsStore.workflowExecutionData).toBeDefined();
expect(uiStore.isActionActive.workflowRunning).toBeTruthy(); expect(uiStore.isActionActive.workflowRunning).toBeTruthy();
expect(spy).toHaveBeenCalledWith(executionId);
}); });
}); });

View File

@@ -35,6 +35,7 @@ import { useTelemetry } from '@/composables/useTelemetry';
import type { PushMessageQueueItem } from '@/types'; import type { PushMessageQueueItem } from '@/types';
import { useAssistantStore } from '@/stores/assistant.store'; import { useAssistantStore } from '@/stores/assistant.store';
import NodeExecutionErrorMessage from '@/components/NodeExecutionErrorMessage.vue'; import NodeExecutionErrorMessage from '@/components/NodeExecutionErrorMessage.vue';
import type { IExecutionResponse } from '@/Interface';
export function usePushConnection({ router }: { router: ReturnType<typeof useRouter> }) { export function usePushConnection({ router }: { router: ReturnType<typeof useRouter> }) {
const workflowHelpers = useWorkflowHelpers({ router }); const workflowHelpers = useWorkflowHelpers({ router });
@@ -205,11 +206,19 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
return false; return false;
} }
// pull execution data for the execution from the server let executionData: Pick<IExecutionResponse, 'workflowId' | 'data' | 'status'>;
const executionData = await workflowsStore.fetchExecutionDataById(executionId); if (receivedData.type === 'executionFinished' && receivedData.data.rawData) {
if (!executionData?.data) return false; const { workflowId, status, rawData } = receivedData.data;
// data comes in as 'flatten' object, so we need to parse it executionData = { workflowId, data: parse(rawData), status };
executionData.data = parse(executionData.data as unknown as string) as IRunExecutionData; } else {
const execution = await workflowsStore.fetchExecutionDataById(executionId);
if (!execution?.data) return false;
executionData = {
workflowId: execution.workflowId,
data: parse(execution.data as unknown as string),
status: execution.status,
};
}
const iRunExecutionData: IRunExecutionData = { const iRunExecutionData: IRunExecutionData = {
startData: executionData.data?.startData, startData: executionData.data?.startData,
@@ -265,7 +274,7 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
// Workflow did start but had been put to wait // Workflow did start but had been put to wait
workflowHelpers.setDocumentTitle(workflow.name as string, 'IDLE'); workflowHelpers.setDocumentTitle(workflow.name as string, 'IDLE');
} else if (executionData.finished !== true) { } else if (executionData.status === 'error' || executionData.status === 'canceled') {
workflowHelpers.setDocumentTitle(workflow.name as string, 'ERROR'); workflowHelpers.setDocumentTitle(workflow.name as string, 'ERROR');
if ( if (
@@ -347,17 +356,14 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
duration: 0, duration: 0,
}); });
} else { } else {
let title: string; // Do not show the error message if the workflow got canceled
const isManualExecutionCancelled = if (executionData.status === 'canceled') {
executionData.mode === 'manual' && executionData.status === 'canceled';
// Do not show the error message if the workflow got canceled manually
if (isManualExecutionCancelled) {
toast.showMessage({ toast.showMessage({
title: i18n.baseText('nodeView.showMessage.stopExecutionTry.title'), title: i18n.baseText('nodeView.showMessage.stopExecutionTry.title'),
type: 'success', type: 'success',
}); });
} else { } else {
let title: string;
if (iRunExecutionData.resultData.lastNodeExecuted) { if (iRunExecutionData.resultData.lastNodeExecuted) {
title = `Problem in node ${iRunExecutionData.resultData.lastNodeExecuted}`; title = `Problem in node ${iRunExecutionData.resultData.lastNodeExecuted}`;
} else { } else {
@@ -421,7 +427,7 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
} }
workflowsStore.executingNode.length = 0; workflowsStore.executingNode.length = 0;
workflowsStore.setWorkflowExecutionData(executionData); workflowsStore.setWorkflowExecutionData(executionData as IExecutionResponse);
uiStore.removeActiveAction('workflowRunning'); uiStore.removeActiveAction('workflowRunning');
// Set the node execution issues on all the nodes which produced an error so that // Set the node execution issues on all the nodes which produced an error so that