Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import NodePlay from './custom/node-play.svg';
import NodePower from './custom/node-power.svg';
import NodeSuccess from './custom/node-success.svg';
import NodeTrash from './custom/node-trash.svg';
import NodeValidationError from './custom/node-validation-error.svg';
import PopOut from './custom/pop-out.svg';
import Retry from './custom/retry.svg';
import RunOnce from './custom/run-once.svg';
Expand Down Expand Up @@ -435,6 +436,7 @@ export const updatedIconSet = {
'node-dirty': NodeDirty,
'node-ellipsis': NodeEllipsis,
'node-error': NodeError,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename this to node-execution-error so people don't misuse this for validation errors?

'node-validation-error': NodeValidationError,
'node-pin': NodePin,
'node-play': NodePlay,
'node-power': NodePower,
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/editor-ui/src/__tests__/data/canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function createCanvasNodeData({
outputs = [],
connections = { [CanvasConnectionMode.Input]: {}, [CanvasConnectionMode.Output]: {} },
execution = { running: false },
issues = { items: [], visible: false },
issues = { execution: [], validation: [], visible: false },
pinnedData = { count: 0, visible: false },
runData = { outputMap: {}, iterations: 0, visible: false },
render = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const {
executionWaitingForNext,
executionRunning,
hasRunData,
hasIssues,
hasExecutionErrors,
render,
} = useCanvasNode();
const { mainOutputs, mainOutputConnections, mainInputs, mainInputConnections, nonMainInputs } =
Expand All @@ -54,7 +54,7 @@ const classes = computed(() => {
[$style.selected]: isSelected.value,
[$style.disabled]: isDisabled.value,
[$style.success]: hasRunData.value,
[$style.error]: hasIssues.value,
[$style.error]: hasExecutionErrors.value,
[$style.pinned]: hasPinnedData.value,
[$style.waiting]: executionWaiting.value ?? executionStatus.value === 'waiting',
[$style.running]: executionRunning.value || executionWaitingForNext.value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const $style = useCssModule();
const {
hasPinnedData,
issues,
hasIssues,
hasExecutionErrors,
hasValidationErrors,
executionStatus,
executionWaiting,
executionWaitingForNext,
Expand Down Expand Up @@ -66,12 +67,6 @@ const commonClasses = computed(() => [
<N8nIcon icon="clock" :size="size" />
</N8nTooltip>
</div>
<div
v-if="spinnerLayout === 'absolute'"
:class="[...commonClasses, $style['node-waiting-spinner']]"
>
<N8nIcon icon="refresh-cw" spin />
</div>
</div>
<div
v-else-if="isNodeExecuting"
Expand All @@ -84,7 +79,7 @@ const commonClasses = computed(() => [
<N8nIcon icon="power" :size="size" />
</div>
<div
v-else-if="hasIssues && !hideNodeIssues"
v-else-if="hasExecutionErrors && !hideNodeIssues"
:class="[...commonClasses, $style.issues]"
data-test-id="node-issues"
>
Expand All @@ -95,6 +90,18 @@ const commonClasses = computed(() => [
<N8nIcon icon="node-error" :size="size" />
</N8nTooltip>
</div>
<div
v-else-if="hasValidationErrors && !hideNodeIssues"
:class="[...commonClasses, $style.issues]"
data-test-id="node-issues"
>
<N8nTooltip :show-after="500" placement="bottom">
<template #content>
<TitledList :title="`${i18n.baseText('node.issues')}:`" :items="issues" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should items be executionErrors (and validationErrors for the above)?

</template>
<N8nIcon icon="node-validation-error" :size="size" />
</N8nTooltip>
</div>
<div v-else-if="executionStatus === 'unknown'">
<!-- Do nothing, unknown means the node never executed -->
</div>
Expand Down Expand Up @@ -152,7 +159,6 @@ const commonClasses = computed(() => [
color: var(--color-secondary);
}

.node-waiting-spinner,
.running {
color: hsl(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ describe('useCanvasMapping', () => {
waitingForNext: false,
},
issues: {
items: [],
execution: [],
validation: [],
visible: false,
},
pinnedData: {
Expand Down Expand Up @@ -721,7 +722,7 @@ describe('useCanvasMapping', () => {
workflowObject: ref(workflowObject) as Ref<Workflow>,
});

expect(nodeIssuesById.value[node.id]).toEqual([]);
expect(nodeIssuesById.value[node.id]).toEqual({ execution: [], validation: [] });
});

it('should handle execution errors', () => {
Expand Down Expand Up @@ -754,7 +755,10 @@ describe('useCanvasMapping', () => {
workflowObject: ref(workflowObject) as Ref<Workflow>,
});

expect(nodeIssuesById.value[node.id]).toEqual([`${errorMessage} (${errorDescription})`]);
expect(nodeIssuesById.value[node.id]).toEqual({
execution: [`${errorMessage} (${errorDescription})`],
validation: [],
});
});

it('should handle execution error without description', () => {
Expand Down Expand Up @@ -786,7 +790,10 @@ describe('useCanvasMapping', () => {
workflowObject: ref(workflowObject) as Ref<Workflow>,
});

expect(nodeIssuesById.value[node.id]).toEqual([errorMessage]);
expect(nodeIssuesById.value[node.id]).toEqual({
execution: [errorMessage],
validation: [],
});
});

it('should handle multiple execution errors', () => {
Expand Down Expand Up @@ -827,10 +834,10 @@ describe('useCanvasMapping', () => {
workflowObject: ref(workflowObject) as Ref<Workflow>,
});

expect(nodeIssuesById.value[node.id]).toEqual([
'Error 1 (Description 1)',
'Error 2 (Description 2)',
]);
expect(nodeIssuesById.value[node.id]).toEqual({
execution: ['Error 1 (Description 1)', 'Error 2 (Description 2)'],
validation: [],
});
});

it('should handle node issues', () => {
Expand All @@ -853,9 +860,10 @@ describe('useCanvasMapping', () => {
workflowObject: ref(workflowObject) as Ref<Workflow>,
});

expect(nodeIssuesById.value[node.id]).toEqual([
'Node Type "n8n-nodes-base.set" is not known.',
]);
expect(nodeIssuesById.value[node.id]).toEqual({
execution: [],
validation: ['Node Type "n8n-nodes-base.set" is not known.'],
});
});

it('should combine execution errors and node issues', () => {
Expand Down Expand Up @@ -891,10 +899,10 @@ describe('useCanvasMapping', () => {
workflowObject: ref(workflowObject) as Ref<Workflow>,
});

expect(nodeIssuesById.value[node.id]).toEqual([
'Execution error (Error description)',
'Node Type "n8n-nodes-base.set" is not known.',
]);
expect(nodeIssuesById.value[node.id]).toEqual({
execution: ['Execution error (Error description)'],
validation: ['Node Type "n8n-nodes-base.set" is not known.'],
});
});

it('should handle multiple nodes with different issues', () => {
Expand Down Expand Up @@ -931,10 +939,14 @@ describe('useCanvasMapping', () => {
workflowObject: ref(workflowObject) as Ref<Workflow>,
});

expect(nodeIssuesById.value[node1.id]).toEqual([
'Node Type "n8n-nodes-base.set" is not known.',
]);
expect(nodeIssuesById.value[node2.id]).toEqual(['Execution error (Error description)']);
expect(nodeIssuesById.value[node1.id]).toEqual({
execution: [],
validation: ['Node Type "n8n-nodes-base.set" is not known.'],
});
expect(nodeIssuesById.value[node2.id]).toEqual({
execution: ['Execution error (Error description)'],
validation: [],
});
});
});

Expand Down
45 changes: 37 additions & 8 deletions packages/frontend/editor-ui/src/composables/useCanvasMapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,41 +395,69 @@ export function useCanvasMapping({
{ throttle: CANVAS_EXECUTION_DATA_THROTTLE_DURATION, immediate: true },
);

const nodeIssuesById = computed(() =>
const nodeExecutionErrorsById = computed(() =>
nodes.value.reduce<Record<string, string[]>>((acc, node) => {
const issues: string[] = [];
const executionErrors: string[] = [];
const nodeExecutionRunData = workflowsStore.getWorkflowRunData?.[node.name];
if (nodeExecutionRunData) {
nodeExecutionRunData.forEach((executionRunData) => {
if (executionRunData?.error) {
const { message, description } = executionRunData.error;
const issue = `${message}${description ? ` (${description})` : ''}`;
issues.push(sanitizeHtml(issue));
executionErrors.push(sanitizeHtml(issue));
}
});
}

acc[node.id] = executionErrors;

return acc;
}, {}),
);

const nodeValidationErrorsById = computed(() =>
nodes.value.reduce<Record<string, string[]>>((acc, node) => {
const validationErrors: string[] = [];

if (node?.issues !== undefined) {
issues.push(...nodeHelpers.nodeIssuesToString(node.issues, node));
validationErrors.push(...nodeHelpers.nodeIssuesToString(node.issues, node));
}

acc[node.id] = issues;
acc[node.id] = validationErrors;

return acc;
}, {}),
);

const nodeIssuesById = computed(() =>
nodes.value.reduce<Record<string, { execution: string[]; validation: string[] }>>(
(acc, node) => {
acc[node.id] = {
execution: nodeExecutionErrorsById.value[node.id] ?? [],
validation: nodeValidationErrorsById.value[node.id] ?? [],
};

return acc;
},
{},
),
);

const nodeHasIssuesById = computed(() =>
nodes.value.reduce<Record<string, boolean>>((acc, node) => {
const hasExecutionErrors = nodeExecutionErrorsById.value[node.id]?.length > 0;
const hasValidationErrors = nodeValidationErrorsById.value[node.id]?.length > 0;

if (['crashed', 'error'].includes(nodeExecutionStatusById.value[node.id])) {
acc[node.id] = true;
} else if (nodePinnedDataById.value[node.id]) {
acc[node.id] = false;
} else if (node.issues && nodeHelpers.nodeIssuesToString(node.issues, node).length) {
} else if (hasValidationErrors) {
acc[node.id] = true;
} else if (hasExecutionErrors) {
acc[node.id] = true;
} else {
const tasks = workflowsStore.getWorkflowRunData?.[node.name] ?? [];

acc[node.id] = Boolean(tasks.at(-1)?.error);
}

Expand Down Expand Up @@ -605,7 +633,8 @@ export function useCanvasMapping({
[CanvasConnectionMode.Output]: outputConnections,
},
issues: {
items: nodeIssuesById.value[node.id],
execution: nodeIssuesById.value[node.id].execution,
validation: nodeIssuesById.value[node.id].validation,
Comment on lines +636 to +637
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be like below? So we don't have to calculate nodeIssuesById?

Suggested change
execution: nodeIssuesById.value[node.id].execution,
validation: nodeIssuesById.value[node.id].validation,
execution: nodeExecutionErrorsById.value[node.id],
validation: nodeValidationErrorsById.value[node.id],

visible: nodeHasIssuesById.value[node.id],
},
pinnedData: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ describe('useCanvasNode', () => {
[CanvasConnectionMode.Input]: { '0': [] },
[CanvasConnectionMode.Output]: {},
},
issues: { items: ['issue1'], visible: true },
issues: {
execution: ['execution_error1'],
validation: ['validation_error1'],
visible: true,
},
execution: { status: 'running', waiting: 'waiting', running: true },
runData: { outputMap: {}, iterations: 1, visible: true },
pinnedData: { count: 1, visible: true },
Expand Down Expand Up @@ -90,7 +94,7 @@ describe('useCanvasNode', () => {
expect(result.runDataOutputMap.value).toEqual({});
expect(result.runDataIterations.value).toBe(1);
expect(result.hasRunData.value).toBe(true);
expect(result.issues.value).toEqual(['issue1']);
expect(result.issues.value).toEqual(['execution_error1', 'validation_error1']);
expect(result.hasIssues.value).toBe(true);
expect(result.executionStatus.value).toBe('running');
expect(result.executionWaiting.value).toBe('waiting');
Expand Down
12 changes: 10 additions & 2 deletions packages/frontend/editor-ui/src/composables/useCanvasNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function useCanvasNode() {
inputs: [],
outputs: [],
connections: { [CanvasConnectionMode.Input]: {}, [CanvasConnectionMode.Output]: {} },
issues: { items: [], visible: false },
issues: { execution: [], validation: [], visible: false },
pinnedData: { count: 0, visible: false },
execution: {
running: false,
Expand Down Expand Up @@ -47,8 +47,12 @@ export function useCanvasNode() {
const pinnedDataCount = computed(() => data.value.pinnedData.count);
const hasPinnedData = computed(() => data.value.pinnedData.count > 0);

const issues = computed(() => data.value.issues.items ?? []);
const issues = computed(() => [...data.value.issues.execution, ...data.value.issues.validation]);
const executionErrors = computed(() => data.value.issues.execution ?? []);
const validationErrors = computed(() => data.value.issues.validation ?? []);
const hasIssues = computed(() => data.value.issues.visible);
const hasExecutionErrors = computed(() => data.value.issues.execution.length > 0);
const hasValidationErrors = computed(() => data.value.issues.validation.length > 0);

const executionStatus = computed(() => data.value.execution.status);
const executionWaiting = computed(() => data.value.execution.waiting);
Expand Down Expand Up @@ -81,7 +85,11 @@ export function useCanvasNode() {
runDataOutputMap,
hasRunData,
issues,
executionErrors,
validationErrors,
hasIssues,
hasExecutionErrors,
hasValidationErrors,
executionStatus,
executionWaiting,
executionWaitingForNext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ vi.mock('@/composables/useCanvasMapping', () => ({
additionalNodePropertiesById: computed(() => ({})),
nodeExecutionRunDataOutputMapById: computed(() => ({})),
nodeExecutionWaitingForNextById: computed(() => ({})),
nodeIssuesById: computed(() => ({})),
nodeIssuesById: computed(
() => ({}) as Record<string, { execution: string[]; validation: string[] }>,
),
nodeHasIssuesById: computed(() => ({})),
nodes: computed(() => []),
connections: computed(() => []),
Expand Down Expand Up @@ -401,7 +403,9 @@ describe('useWorkflowDiff', () => {
additionalNodePropertiesById: computed(() => ({}) as Record<string, Partial<CanvasNode>>),
nodeExecutionRunDataOutputMapById: computed(() => ({}) as Record<string, ExecutionOutputMap>),
nodeExecutionWaitingForNextById: computed(() => ({}) as Record<string, boolean>),
nodeIssuesById: computed(() => ({}) as Record<string, string[]>),
nodeIssuesById: computed(
() => ({}) as Record<string, { execution: string[]; validation: string[] }>,
),
nodeHasIssuesById: computed(() => ({}) as Record<string, boolean>),
nodes: computed(() => nodes as CanvasNode[]),
connections: computed(() => connections as CanvasConnection[]),
Expand Down
3 changes: 2 additions & 1 deletion packages/frontend/editor-ui/src/types/canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ export interface CanvasNodeData {
[CanvasConnectionMode.Output]: INodeConnections;
};
issues: {
items: string[];
execution: string[];
validation: string[];
visible: boolean;
};
pinnedData: {
Expand Down
Loading