mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix(core): Bring back execution data on the executionFinished push message (#11821)
This commit is contained in:
committed by
GitHub
parent
13cc5abb7f
commit
03135702f1
@@ -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');
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user