mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-20 19:32:15 +00:00
fix(editor): Show error toast for failed executions (#15388)
This commit is contained in:
committed by
GitHub
parent
954b66218f
commit
e68149bbc7
@@ -53,6 +53,7 @@ import { nextTick } from 'vue';
|
|||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import type { CanvasLayoutEvent } from './useCanvasLayout';
|
import type { CanvasLayoutEvent } from './useCanvasLayout';
|
||||||
import { useTelemetry } from './useTelemetry';
|
import { useTelemetry } from './useTelemetry';
|
||||||
|
import { useToast } from '@/composables/useToast';
|
||||||
|
|
||||||
vi.mock('vue-router', async (importOriginal) => {
|
vi.mock('vue-router', async (importOriginal) => {
|
||||||
const actual = await importOriginal<{}>();
|
const actual = await importOriginal<{}>();
|
||||||
@@ -88,6 +89,21 @@ vi.mock('@/composables/useTelemetry', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock('@/composables/useToast', () => {
|
||||||
|
const showMessage = vi.fn();
|
||||||
|
const showError = vi.fn();
|
||||||
|
const showToast = vi.fn();
|
||||||
|
return {
|
||||||
|
useToast: () => {
|
||||||
|
return {
|
||||||
|
showMessage,
|
||||||
|
showError,
|
||||||
|
showToast,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('useCanvasOperations', () => {
|
describe('useCanvasOperations', () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -2726,6 +2742,39 @@ describe('useCanvasOperations', () => {
|
|||||||
|
|
||||||
expect(workflowsStore.setWorkflowPinData).toHaveBeenCalledWith({});
|
expect(workflowsStore.setWorkflowPinData).toHaveBeenCalledWith({});
|
||||||
});
|
});
|
||||||
|
it('should show an error notification for failed executions', async () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const { openExecution } = useCanvasOperations({ router });
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const executionId = '123';
|
||||||
|
const executionData: IExecutionResponse = {
|
||||||
|
id: executionId,
|
||||||
|
finished: true,
|
||||||
|
status: 'error',
|
||||||
|
startedAt: new Date(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
workflowData: createTestWorkflow(),
|
||||||
|
mode: 'manual',
|
||||||
|
data: {
|
||||||
|
resultData: {
|
||||||
|
error: { message: 'Crashed', node: { name: 'Step1' } },
|
||||||
|
lastNodeExecuted: 'Last Node',
|
||||||
|
},
|
||||||
|
} as IExecutionResponse['data'],
|
||||||
|
};
|
||||||
|
|
||||||
|
workflowsStore.getExecution.mockResolvedValue(executionData);
|
||||||
|
|
||||||
|
await openExecution(executionId);
|
||||||
|
|
||||||
|
expect(toast.showMessage).toHaveBeenCalledWith({
|
||||||
|
duration: 0,
|
||||||
|
message: 'Crashed',
|
||||||
|
title: 'Problem in node ‘Last Node‘',
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('connectAdjacentNodes', () => {
|
describe('connectAdjacentNodes', () => {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { type PinDataSource, usePinnedData } from '@/composables/usePinnedData';
|
|||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
|
import { getExecutionErrorToastConfiguration } from '@/utils/executionUtils';
|
||||||
import {
|
import {
|
||||||
EnterpriseEditionFeature,
|
EnterpriseEditionFeature,
|
||||||
FORM_TRIGGER_NODE_TYPE,
|
FORM_TRIGGER_NODE_TYPE,
|
||||||
@@ -2009,6 +2010,14 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
|||||||
throw new Error(`Execution with id "${executionId}" could not be found!`);
|
throw new Error(`Execution with id "${executionId}" could not be found!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.status === 'error' && data.data?.resultData.error) {
|
||||||
|
const { title, message } = getExecutionErrorToastConfiguration({
|
||||||
|
error: data.data.resultData.error,
|
||||||
|
lastNodeExecuted: data.data.resultData.lastNodeExecuted,
|
||||||
|
});
|
||||||
|
toast.showMessage({ title, message, type: 'error', duration: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
initializeWorkspace(data.workflowData);
|
initializeWorkspace(data.workflowData);
|
||||||
|
|
||||||
workflowsStore.setWorkflowExecutionData(data);
|
workflowsStore.setWorkflowExecutionData(data);
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ import { useUIStore } from '@/stores/ui.store';
|
|||||||
import type { IExecutionResponse } from '@/Interface';
|
import type { IExecutionResponse } from '@/Interface';
|
||||||
import { WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants';
|
import { WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants';
|
||||||
import { getEasyAiWorkflowJson } from '@/utils/easyAiWorkflowUtils';
|
import { getEasyAiWorkflowJson } from '@/utils/easyAiWorkflowUtils';
|
||||||
import { clearPopupWindowState, hasTrimmedData, hasTrimmedItem } from '@/utils/executionUtils';
|
import {
|
||||||
|
clearPopupWindowState,
|
||||||
|
hasTrimmedData,
|
||||||
|
hasTrimmedItem,
|
||||||
|
getExecutionErrorToastConfiguration,
|
||||||
|
getExecutionErrorMessage,
|
||||||
|
} from '@/utils/executionUtils';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
@@ -13,18 +19,8 @@ import { useToast } from '@/composables/useToast';
|
|||||||
import type { useRouter } from 'vue-router';
|
import type { useRouter } from 'vue-router';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { TelemetryHelpers } from 'n8n-workflow';
|
import { TelemetryHelpers } from 'n8n-workflow';
|
||||||
import type {
|
import type { IWorkflowBase, ExpressionError, IDataObject, IRunExecutionData } from 'n8n-workflow';
|
||||||
IWorkflowBase,
|
|
||||||
NodeError,
|
|
||||||
NodeOperationError,
|
|
||||||
SubworkflowOperationError,
|
|
||||||
ExpressionError,
|
|
||||||
IDataObject,
|
|
||||||
IRunExecutionData,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import { codeNodeEditorEventBus, globalLinkActionsEventBus } from '@/event-bus';
|
import { codeNodeEditorEventBus, globalLinkActionsEventBus } from '@/event-bus';
|
||||||
import { h } from 'vue';
|
|
||||||
import NodeExecutionErrorMessage from '@/components/NodeExecutionErrorMessage.vue';
|
|
||||||
import { getTriggerNodeServiceName } from '@/utils/nodeTypesUtils';
|
import { getTriggerNodeServiceName } from '@/utils/nodeTypesUtils';
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
@@ -164,45 +160,6 @@ export function getRunExecutionData(execution: SimplifiedExecution): IRunExecuti
|
|||||||
return runExecutionData;
|
return runExecutionData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the error message from the execution object if it exists,
|
|
||||||
* or a fallback error message otherwise
|
|
||||||
*/
|
|
||||||
export function getExecutionError(execution: SimplifiedExecution): string {
|
|
||||||
const error = execution.data?.resultData.error;
|
|
||||||
const i18n = useI18n();
|
|
||||||
|
|
||||||
let errorMessage: string;
|
|
||||||
|
|
||||||
if (execution.data?.resultData.lastNodeExecuted && error) {
|
|
||||||
errorMessage = error.message ?? error.description ?? '';
|
|
||||||
} else {
|
|
||||||
errorMessage = i18n.baseText('pushConnection.executionError', {
|
|
||||||
interpolate: { error: '!' },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error?.message) {
|
|
||||||
let nodeName: string | undefined;
|
|
||||||
if ('node' in error) {
|
|
||||||
nodeName = typeof error.node === 'string' ? error.node : error.node!.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
const receivedError = nodeName ? `${nodeName}: ${error.message}` : error.message;
|
|
||||||
errorMessage = i18n.baseText('pushConnection.executionError', {
|
|
||||||
interpolate: {
|
|
||||||
error: `.${i18n.baseText('pushConnection.executionError.details', {
|
|
||||||
interpolate: {
|
|
||||||
details: receivedError,
|
|
||||||
},
|
|
||||||
})}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errorMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the error message for the execution run data if the execution status is crashed or canceled,
|
* Returns the error message for the execution run data if the execution status is crashed or canceled,
|
||||||
* or a fallback error message otherwise
|
* or a fallback error message otherwise
|
||||||
@@ -220,7 +177,10 @@ export function getRunDataExecutedErrorMessage(execution: SimplifiedExecution) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return getExecutionError(execution);
|
return getExecutionErrorMessage({
|
||||||
|
error: execution.data?.resultData.error,
|
||||||
|
lastNodeExecuted: execution.data?.resultData.lastNodeExecuted,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -269,7 +229,6 @@ export function handleExecutionFinishedWithErrorOrCanceled(
|
|||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const workflowHelpers = useWorkflowHelpers(options);
|
const workflowHelpers = useWorkflowHelpers(options);
|
||||||
const workflowObject = workflowsStore.getCurrentWorkflow();
|
const workflowObject = workflowsStore.getCurrentWorkflow();
|
||||||
const runDataExecutedErrorMessage = getRunDataExecutedErrorMessage(execution);
|
|
||||||
|
|
||||||
workflowHelpers.setDocumentTitle(workflowObject.name as string, 'ERROR');
|
workflowHelpers.setDocumentTitle(workflowObject.name as string, 'ERROR');
|
||||||
|
|
||||||
@@ -315,62 +274,19 @@ export function handleExecutionFinishedWithErrorOrCanceled(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (runExecutionData.resultData.error?.name === 'SubworkflowOperationError') {
|
if (execution.status === 'canceled') {
|
||||||
const error = runExecutionData.resultData.error as SubworkflowOperationError;
|
|
||||||
|
|
||||||
workflowsStore.subWorkflowExecutionError = error;
|
|
||||||
|
|
||||||
toast.showMessage({
|
|
||||||
title: error.message,
|
|
||||||
message: error.description,
|
|
||||||
type: 'error',
|
|
||||||
duration: 0,
|
|
||||||
});
|
|
||||||
} else if (
|
|
||||||
(runExecutionData.resultData.error?.name === 'NodeOperationError' ||
|
|
||||||
runExecutionData.resultData.error?.name === 'NodeApiError') &&
|
|
||||||
(runExecutionData.resultData.error as NodeError).functionality === 'configuration-node'
|
|
||||||
) {
|
|
||||||
// If the error is a configuration error of the node itself doesn't get executed so we can't use lastNodeExecuted for the title
|
|
||||||
let title: string;
|
|
||||||
const nodeError = runExecutionData.resultData.error as NodeOperationError;
|
|
||||||
if (nodeError.node.name) {
|
|
||||||
title = `Error in sub-node ‘${nodeError.node.name}‘`;
|
|
||||||
} else {
|
|
||||||
title = 'Problem executing workflow';
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.showMessage({
|
|
||||||
title,
|
|
||||||
message: h(NodeExecutionErrorMessage, {
|
|
||||||
errorMessage: nodeError?.description ?? runDataExecutedErrorMessage,
|
|
||||||
nodeName: nodeError.node.name,
|
|
||||||
}),
|
|
||||||
type: 'error',
|
|
||||||
duration: 0,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Do not show the error message if the workflow got canceled
|
// Do not show the error message if the workflow got canceled
|
||||||
if (execution.status === 'canceled') {
|
toast.showMessage({
|
||||||
toast.showMessage({
|
title: i18n.baseText('nodeView.showMessage.stopExecutionTry.title'),
|
||||||
title: i18n.baseText('nodeView.showMessage.stopExecutionTry.title'),
|
type: 'success',
|
||||||
type: 'success',
|
});
|
||||||
});
|
} else if (execution.data?.resultData.error) {
|
||||||
} else {
|
const { message, title } = getExecutionErrorToastConfiguration({
|
||||||
let title: string;
|
error: execution.data.resultData.error,
|
||||||
if (runExecutionData.resultData.lastNodeExecuted) {
|
lastNodeExecuted: execution.data?.resultData.lastNodeExecuted,
|
||||||
title = `Problem in node ‘${runExecutionData.resultData.lastNodeExecuted}‘`;
|
});
|
||||||
} else {
|
|
||||||
title = 'Problem executing workflow';
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.showMessage({
|
toast.showMessage({ title, message, type: 'error', duration: 0 });
|
||||||
title,
|
|
||||||
message: runDataExecutedErrorMessage,
|
|
||||||
type: 'error',
|
|
||||||
duration: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { displayForm, executionFilterToQueryFilter, waitingNodeTooltip } from './executionUtils';
|
import {
|
||||||
import type { INode, IRunData, IPinData } from 'n8n-workflow';
|
displayForm,
|
||||||
|
executionFilterToQueryFilter,
|
||||||
|
waitingNodeTooltip,
|
||||||
|
getExecutionErrorMessage,
|
||||||
|
getExecutionErrorToastConfiguration,
|
||||||
|
} from './executionUtils';
|
||||||
|
import type { INode, IRunData, IPinData, ExecutionError } from 'n8n-workflow';
|
||||||
import { type INodeUi } from '../Interface';
|
import { type INodeUi } from '../Interface';
|
||||||
import { CHAT_TRIGGER_NODE_TYPE, FORM_TRIGGER_NODE_TYPE, GITHUB_NODE_TYPE } from '@/constants';
|
import { CHAT_TRIGGER_NODE_TYPE, FORM_TRIGGER_NODE_TYPE, GITHUB_NODE_TYPE } from '@/constants';
|
||||||
import { createTestNode } from '@/__tests__/mocks';
|
import { createTestNode } from '@/__tests__/mocks';
|
||||||
|
import type { VNode } from 'vue';
|
||||||
|
|
||||||
const WAIT_NODE_TYPE = 'waitNode';
|
const WAIT_NODE_TYPE = 'waitNode';
|
||||||
|
|
||||||
@@ -24,13 +31,15 @@ vi.mock('@/stores/workflows.store', () => ({
|
|||||||
|
|
||||||
vi.mock('@/plugins/i18n', () => ({
|
vi.mock('@/plugins/i18n', () => ({
|
||||||
i18n: {
|
i18n: {
|
||||||
baseText: (key: string) => {
|
baseText: (key: string, options?: { interpolate?: { error?: string; details?: string } }) => {
|
||||||
const texts: { [key: string]: string } = {
|
const texts: { [key: string]: string } = {
|
||||||
'ndv.output.waitNodeWaiting': 'Waiting for execution to resume...',
|
'ndv.output.waitNodeWaiting': 'Waiting for execution to resume...',
|
||||||
'ndv.output.waitNodeWaitingForFormSubmission': 'Waiting for form submission: ',
|
'ndv.output.waitNodeWaitingForFormSubmission': 'Waiting for form submission: ',
|
||||||
'ndv.output.waitNodeWaitingForWebhook': 'Waiting for webhook call: ',
|
'ndv.output.waitNodeWaitingForWebhook': 'Waiting for webhook call: ',
|
||||||
'ndv.output.githubNodeWaitingForWebhook': 'Waiting for webhook call: ',
|
'ndv.output.githubNodeWaitingForWebhook': 'Waiting for webhook call: ',
|
||||||
'ndv.output.sendAndWaitWaitingApproval': 'Waiting for approval...',
|
'ndv.output.sendAndWaitWaitingApproval': 'Waiting for approval...',
|
||||||
|
'pushConnection.executionError': `Execution error${options?.interpolate?.error}`,
|
||||||
|
'pushConnection.executionError.details': `Details: ${options?.interpolate?.details}`,
|
||||||
};
|
};
|
||||||
return texts[key] || key;
|
return texts[key] || key;
|
||||||
},
|
},
|
||||||
@@ -323,3 +332,123 @@ describe('waitingNodeTooltip', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const executionErrorFactory = (error: Record<string, unknown>) =>
|
||||||
|
error as unknown as ExecutionError;
|
||||||
|
|
||||||
|
describe('getExecutionErrorMessage', () => {
|
||||||
|
it('returns error.message when lastNodeExecuted and error are present', () => {
|
||||||
|
const result = getExecutionErrorMessage({
|
||||||
|
error: executionErrorFactory({ message: 'Node failed' }),
|
||||||
|
lastNodeExecuted: 'Node1',
|
||||||
|
});
|
||||||
|
expect(result).toBe('Node failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses fallback translation when only error.message is provided', () => {
|
||||||
|
const result = getExecutionErrorMessage({
|
||||||
|
error: executionErrorFactory({ message: 'Something went wrong' }),
|
||||||
|
});
|
||||||
|
expect(result).toBe('Execution error.Details: Something went wrong');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes node name if error.node is a string', () => {
|
||||||
|
const result = getExecutionErrorMessage({
|
||||||
|
error: executionErrorFactory({ message: 'Failed', node: 'MyNode' }),
|
||||||
|
});
|
||||||
|
expect(result).toBe('Execution error.Details: MyNode: Failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes node.name if error.node is an object', () => {
|
||||||
|
const result = getExecutionErrorMessage({
|
||||||
|
error: executionErrorFactory({ message: 'Crashed', node: { name: 'Step1' } }),
|
||||||
|
});
|
||||||
|
expect(result).toBe('Execution error.Details: Step1: Crashed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses default fallback when no error or lastNodeExecuted', () => {
|
||||||
|
const result = getExecutionErrorMessage({});
|
||||||
|
expect(result).toBe('Execution error!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getExecutionErrorToastConfiguration', () => {
|
||||||
|
it('returns config for SubworkflowOperationError', () => {
|
||||||
|
const result = getExecutionErrorToastConfiguration({
|
||||||
|
error: executionErrorFactory({
|
||||||
|
name: 'SubworkflowOperationError',
|
||||||
|
message: 'Subworkflow failed',
|
||||||
|
description: 'Workflow XYZ failed',
|
||||||
|
}),
|
||||||
|
lastNodeExecuted: 'NodeA',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
title: 'Subworkflow failed',
|
||||||
|
message: 'Workflow XYZ failed',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns config for configuration-node error with node name', () => {
|
||||||
|
const result = getExecutionErrorToastConfiguration({
|
||||||
|
error: executionErrorFactory({
|
||||||
|
name: 'NodeOperationError',
|
||||||
|
message: 'Node failed',
|
||||||
|
description: 'Bad configuration',
|
||||||
|
functionality: 'configuration-node',
|
||||||
|
node: { name: 'TestNode' },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(result.title).toBe('Error in sub-node ‘TestNode‘');
|
||||||
|
expect((result.message as VNode).props).toEqual({
|
||||||
|
errorMessage: 'Bad configuration',
|
||||||
|
nodeName: 'TestNode',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns config for configuration-node error without node name', () => {
|
||||||
|
const result = getExecutionErrorToastConfiguration({
|
||||||
|
error: executionErrorFactory({
|
||||||
|
name: 'NodeApiError',
|
||||||
|
message: 'API failed',
|
||||||
|
description: 'Missing credentials',
|
||||||
|
functionality: 'configuration-node',
|
||||||
|
node: {},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.title).toBe('Problem executing workflow');
|
||||||
|
expect((result.message as VNode).props).toEqual({
|
||||||
|
errorMessage: 'Missing credentials',
|
||||||
|
nodeName: '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns generic config when error type is not special', () => {
|
||||||
|
const result = getExecutionErrorToastConfiguration({
|
||||||
|
error: executionErrorFactory({
|
||||||
|
name: 'UnknownError',
|
||||||
|
message: 'Something broke',
|
||||||
|
}),
|
||||||
|
lastNodeExecuted: 'NodeX',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
title: 'Problem in node ‘NodeX‘',
|
||||||
|
message: 'Something broke',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns generic config without lastNodeExecuted', () => {
|
||||||
|
const result = getExecutionErrorToastConfiguration({
|
||||||
|
error: executionErrorFactory({
|
||||||
|
name: 'UnknownError',
|
||||||
|
message: 'Something broke',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
title: 'Problem executing workflow',
|
||||||
|
message: 'Execution error.Details: Something broke',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
INode,
|
INode,
|
||||||
IPinData,
|
IPinData,
|
||||||
IRunData,
|
IRunData,
|
||||||
|
ExecutionError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import type { ExecutionFilterType, ExecutionsQueryFilter, INodeUi } from '@/Interface';
|
import type { ExecutionFilterType, ExecutionsQueryFilter, INodeUi } from '@/Interface';
|
||||||
import { isEmpty } from '@/utils/typesUtils';
|
import { isEmpty } from '@/utils/typesUtils';
|
||||||
@@ -13,6 +14,8 @@ import { FORM_NODE_TYPE, FORM_TRIGGER_NODE_TYPE, GITHUB_NODE_TYPE } from '../con
|
|||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useRootStore } from '@/stores/root.store';
|
import { useRootStore } from '@/stores/root.store';
|
||||||
import { i18n } from '@/plugins/i18n';
|
import { i18n } from '@/plugins/i18n';
|
||||||
|
import { h } from 'vue';
|
||||||
|
import NodeExecutionErrorMessage from '@/components/NodeExecutionErrorMessage.vue';
|
||||||
|
|
||||||
export function getDefaultExecutionFilters(): ExecutionFilterType {
|
export function getDefaultExecutionFilters(): ExecutionFilterType {
|
||||||
return {
|
return {
|
||||||
@@ -266,3 +269,76 @@ export function executionRetryMessage(executionStatus: ExecutionStatus):
|
|||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the error message from the execution object if it exists,
|
||||||
|
* or a fallback error message otherwise
|
||||||
|
*/
|
||||||
|
export function getExecutionErrorMessage({
|
||||||
|
error,
|
||||||
|
lastNodeExecuted,
|
||||||
|
}: { error?: ExecutionError; lastNodeExecuted?: string }): string {
|
||||||
|
let errorMessage: string;
|
||||||
|
|
||||||
|
if (lastNodeExecuted && error) {
|
||||||
|
errorMessage = error.message ?? error.description ?? '';
|
||||||
|
} else {
|
||||||
|
errorMessage = i18n.baseText('pushConnection.executionError', {
|
||||||
|
interpolate: { error: '!' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error?.message) {
|
||||||
|
let nodeName: string | undefined;
|
||||||
|
if ('node' in error) {
|
||||||
|
nodeName = typeof error.node === 'string' ? error.node : error.node?.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const receivedError = nodeName ? `${nodeName}: ${error.message}` : error.message;
|
||||||
|
errorMessage = i18n.baseText('pushConnection.executionError', {
|
||||||
|
interpolate: {
|
||||||
|
error: `.${i18n.baseText('pushConnection.executionError.details', {
|
||||||
|
interpolate: {
|
||||||
|
details: receivedError,
|
||||||
|
},
|
||||||
|
})}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExecutionErrorToastConfiguration({
|
||||||
|
error,
|
||||||
|
lastNodeExecuted,
|
||||||
|
}: { error: ExecutionError; lastNodeExecuted?: string }) {
|
||||||
|
const message = getExecutionErrorMessage({ error, lastNodeExecuted });
|
||||||
|
|
||||||
|
if (error.name === 'SubworkflowOperationError') {
|
||||||
|
return { title: error.message, message: error.description ?? '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(error.name === 'NodeOperationError' || error.name === 'NodeApiError') &&
|
||||||
|
error.functionality === 'configuration-node'
|
||||||
|
) {
|
||||||
|
// If the error is a configuration error of the node itself doesn't get executed so we can't use lastNodeExecuted for the title
|
||||||
|
const nodeErrorName = 'node' in error && error.node?.name ? error.node.name : '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: nodeErrorName ? `Error in sub-node ‘${nodeErrorName}‘` : 'Problem executing workflow',
|
||||||
|
message: h(NodeExecutionErrorMessage, {
|
||||||
|
errorMessage: error.description ?? message,
|
||||||
|
nodeName: nodeErrorName,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: lastNodeExecuted
|
||||||
|
? `Problem in node ‘${lastNodeExecuted}‘`
|
||||||
|
: 'Problem executing workflow',
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user