mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix: Show correct "canceled" node status for chat model nodes (#19366)
This commit is contained in:
@@ -1883,6 +1883,7 @@
|
||||
"runData.editValue": "Edit Value",
|
||||
"runData.executionStatus.success": "Executed successfully",
|
||||
"runData.executionStatus.failed": "Execution failed",
|
||||
"runData.executionStatus.canceled": "Execution canceled",
|
||||
"runData.downloadBinaryData": "Download",
|
||||
"runData.executeNode": "Test Node",
|
||||
"runData.executionTime": "Execution Time",
|
||||
|
||||
155
packages/frontend/editor-ui/src/components/RunInfo.test.ts
Normal file
155
packages/frontend/editor-ui/src/components/RunInfo.test.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import type { ITaskData } from 'n8n-workflow';
|
||||
import RunInfo from './RunInfo.vue';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
vi.mock('@/utils/formatters/dateFormatter', () => ({
|
||||
convertToDisplayDateComponents: vi.fn(() => ({
|
||||
date: 'Jan 15',
|
||||
time: '10:30:00',
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@n8n/i18n', async (importOriginal) => {
|
||||
return {
|
||||
...(await importOriginal()),
|
||||
useI18n: () => ({
|
||||
baseText: vi.fn((key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'runData.executionStatus.success': 'Success',
|
||||
'runData.executionStatus.canceled': 'Canceled',
|
||||
'runData.executionStatus.failed': 'Failed',
|
||||
'runData.startTime': 'Start time',
|
||||
'runData.executionTime': 'Execution time',
|
||||
'runData.ms': 'ms',
|
||||
};
|
||||
return translations[key] || key;
|
||||
}),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const renderComponent = createComponentRenderer(RunInfo);
|
||||
|
||||
describe('RunInfo', () => {
|
||||
it('should display success status when execution status is success', () => {
|
||||
const successTaskData: ITaskData = mock<ITaskData>({
|
||||
startTime: Date.now(),
|
||||
executionTime: 1500,
|
||||
executionStatus: 'success',
|
||||
data: {},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
const { getByTestId, container } = renderComponent({
|
||||
props: {
|
||||
taskData: successTaskData,
|
||||
hasStaleData: false,
|
||||
hasPinData: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('node-run-status-success')).toBeInTheDocument();
|
||||
expect(getByTestId('node-run-info')).toBeInTheDocument();
|
||||
|
||||
const tooltipDiv = container.querySelector('.tooltipRow');
|
||||
expect(tooltipDiv).toBeInTheDocument();
|
||||
|
||||
const statusIcon = getByTestId('node-run-status-success');
|
||||
expect(statusIcon).toHaveClass('success');
|
||||
|
||||
const infoIcon = getByTestId('node-run-info');
|
||||
expect(infoIcon).toBeInTheDocument();
|
||||
|
||||
// Verify the component renders with success status
|
||||
expect(statusIcon).toHaveAttribute('data-test-id', 'node-run-status-success');
|
||||
|
||||
// Check tooltip content exists in the DOM (even if hidden)
|
||||
expect(document.body).toHaveTextContent('Success');
|
||||
expect(document.body).toHaveTextContent('Start time:');
|
||||
expect(document.body).toHaveTextContent('Jan 15 at 10:30:00');
|
||||
expect(document.body).toHaveTextContent('Execution time:');
|
||||
expect(document.body).toHaveTextContent('1500 ms');
|
||||
});
|
||||
|
||||
it('should display cancelled status when execution status is canceled', () => {
|
||||
const cancelledTaskData: ITaskData = mock<ITaskData>({
|
||||
startTime: 1757506978099,
|
||||
executionTime: 800,
|
||||
executionStatus: 'canceled',
|
||||
data: {},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
const { getByTestId, container, queryByTestId } = renderComponent({
|
||||
props: {
|
||||
taskData: cancelledTaskData,
|
||||
hasStaleData: false,
|
||||
hasPinData: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(queryByTestId('node-run-status-success')).not.toBeInTheDocument();
|
||||
expect(getByTestId('node-run-info')).toBeInTheDocument();
|
||||
|
||||
const tooltipDiv = container.querySelector('.tooltipRow');
|
||||
expect(tooltipDiv).toBeInTheDocument();
|
||||
|
||||
const infoIcon = getByTestId('node-run-info');
|
||||
expect(infoIcon).toBeInTheDocument();
|
||||
|
||||
// For cancelled status, only info tooltip is shown (no status icon)
|
||||
expect(infoIcon).toHaveAttribute('data-test-id', 'node-run-info');
|
||||
|
||||
// Check tooltip content exists in the DOM (even if hidden)
|
||||
expect(document.body).toHaveTextContent('Canceled');
|
||||
expect(document.body).toHaveTextContent('Start time:');
|
||||
expect(document.body).toHaveTextContent('Jan 15 at 10:30:00');
|
||||
expect(document.body).toHaveTextContent('Execution time:');
|
||||
expect(document.body).toHaveTextContent('800 ms');
|
||||
});
|
||||
|
||||
it('should display error status when there is an error', () => {
|
||||
const errorTaskData: ITaskData = mock<ITaskData>({
|
||||
startTime: 1757506978099,
|
||||
executionTime: 1200,
|
||||
executionStatus: 'success',
|
||||
data: {},
|
||||
error: {
|
||||
message: 'Something went wrong',
|
||||
name: 'Error',
|
||||
},
|
||||
});
|
||||
|
||||
const { getByTestId, container } = renderComponent({
|
||||
props: {
|
||||
taskData: errorTaskData,
|
||||
hasStaleData: false,
|
||||
hasPinData: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('node-run-status-danger')).toBeInTheDocument();
|
||||
expect(getByTestId('node-run-info')).toBeInTheDocument();
|
||||
|
||||
const tooltipDiv = container.querySelector('.tooltipRow');
|
||||
expect(tooltipDiv).toBeInTheDocument();
|
||||
|
||||
const statusIcon = getByTestId('node-run-status-danger');
|
||||
expect(statusIcon).toHaveClass('danger');
|
||||
|
||||
const infoIcon = getByTestId('node-run-info');
|
||||
expect(infoIcon).toBeInTheDocument();
|
||||
|
||||
// Verify the component renders with error status
|
||||
expect(statusIcon).toHaveAttribute('data-test-id', 'node-run-status-danger');
|
||||
|
||||
// Check tooltip content exists in the DOM (even if hidden)
|
||||
expect(document.body).toHaveTextContent('Failed');
|
||||
expect(document.body).toHaveTextContent('Start time:');
|
||||
expect(document.body).toHaveTextContent('Jan 15 at 10:30:00');
|
||||
expect(document.body).toHaveTextContent('Execution time:');
|
||||
expect(document.body).toHaveTextContent('1200 ms');
|
||||
});
|
||||
});
|
||||
@@ -53,6 +53,7 @@ const runMetadata = computed(() => {
|
||||
</N8nInfoTip>
|
||||
<div v-else-if="runMetadata" :class="$style.tooltipRow">
|
||||
<N8nInfoTip
|
||||
v-if="taskData?.executionStatus !== 'canceled'"
|
||||
type="note"
|
||||
:theme="theme"
|
||||
:data-test-id="`node-run-status-${theme}`"
|
||||
@@ -69,7 +70,9 @@ const runMetadata = computed(() => {
|
||||
>{{
|
||||
runTaskData?.error
|
||||
? i18n.baseText('runData.executionStatus.failed')
|
||||
: i18n.baseText('runData.executionStatus.success')
|
||||
: runTaskData?.executionStatus === 'canceled'
|
||||
? i18n.baseText('runData.executionStatus.canceled')
|
||||
: i18n.baseText('runData.executionStatus.success')
|
||||
}} </n8n-text
|
||||
><br />
|
||||
<n8n-text :bold="true" size="small">{{
|
||||
|
||||
@@ -53,7 +53,7 @@ const classes = computed(() => {
|
||||
[$style.node]: true,
|
||||
[$style.selected]: isSelected.value,
|
||||
[$style.disabled]: isDisabled.value,
|
||||
[$style.success]: hasRunData.value,
|
||||
[$style.success]: hasRunData.value && executionStatus.value === 'success',
|
||||
[$style.error]: hasExecutionErrors.value,
|
||||
[$style.pinned]: hasPinnedData.value,
|
||||
[$style.waiting]: executionWaiting.value ?? executionStatus.value === 'waiting',
|
||||
|
||||
@@ -75,7 +75,10 @@ describe('CanvasNodeStatusIcons', () => {
|
||||
provide: {
|
||||
...createCanvasProvide(),
|
||||
...createCanvasNodeProvide({
|
||||
data: { runData: { outputMap: {}, iterations: 15, visible: true } },
|
||||
data: {
|
||||
execution: { status: 'success', running: false },
|
||||
runData: { outputMap: {}, iterations: 15, visible: true },
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -84,6 +87,24 @@ describe('CanvasNodeStatusIcons', () => {
|
||||
expect(getByTestId('canvas-node-status-success')).toHaveTextContent('15');
|
||||
});
|
||||
|
||||
it('should not render success icon for a node that was canceled', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasProvide(),
|
||||
...createCanvasNodeProvide({
|
||||
data: {
|
||||
execution: { status: 'canceled', running: false },
|
||||
runData: { outputMap: {}, iterations: 15, visible: true },
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(queryByTestId('canvas-node-status-success')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render correctly for a dirty node that has run successfully', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
global: {
|
||||
|
||||
@@ -131,7 +131,7 @@ const commonClasses = computed(() => [
|
||||
</N8nTooltip>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="hasRunData"
|
||||
v-else-if="hasRunData && executionStatus === 'success'"
|
||||
data-test-id="canvas-node-status-success"
|
||||
:class="[...commonClasses, $style.runData]"
|
||||
>
|
||||
|
||||
@@ -567,6 +567,116 @@ describe('useCanvasMapping', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not count canceled iterations but still count their data', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const nodes = [createTestNode({ name: 'Node 1' })];
|
||||
const connections = {};
|
||||
const workflowObject = createTestWorkflowObject({
|
||||
nodes,
|
||||
connections,
|
||||
});
|
||||
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue([
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }]],
|
||||
},
|
||||
},
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 1,
|
||||
source: [],
|
||||
executionStatus: 'canceled',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }, { json: {} }]],
|
||||
},
|
||||
},
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 2,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
expect(nodeExecutionRunDataOutputMapById.value).toEqual({
|
||||
[nodes[0].id]: {
|
||||
[NodeConnectionTypes.Main]: {
|
||||
0: {
|
||||
iterations: 2, // Only 2 iterations counted (not the canceled one)
|
||||
total: 6, // All data items still counted
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle all canceled iterations correctly', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const nodes = [createTestNode({ name: 'Node 1' })];
|
||||
const connections = {};
|
||||
const workflowObject = createTestWorkflowObject({
|
||||
nodes,
|
||||
connections,
|
||||
});
|
||||
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue([
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
executionStatus: 'canceled',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }]],
|
||||
},
|
||||
},
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 1,
|
||||
source: [],
|
||||
executionStatus: 'canceled',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
expect(nodeExecutionRunDataOutputMapById.value).toEqual({
|
||||
[nodes[0].id]: {
|
||||
[NodeConnectionTypes.Main]: {
|
||||
0: {
|
||||
iterations: 0, // No iterations counted since all are canceled
|
||||
total: 3, // But data items still counted
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('additionalNodePropertiesById', () => {
|
||||
@@ -961,6 +1071,262 @@ describe('useCanvasMapping', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterOutCanceled helper function', () => {
|
||||
it('should return null for null input', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const nodes = [createTestNode({ name: 'Node 1' })];
|
||||
const connections = {};
|
||||
const workflowObject = createTestWorkflowObject({ nodes, connections });
|
||||
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue(null);
|
||||
|
||||
const { nodes: mappedNodes } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
expect(mappedNodes.value[0]?.data?.runData?.iterations).toEqual(0);
|
||||
});
|
||||
|
||||
it('should filter out canceled tasks and return correct iteration count', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const nodes = [createTestNode({ name: 'Node 1' })];
|
||||
const connections = {};
|
||||
const workflowObject = createTestWorkflowObject({ nodes, connections });
|
||||
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue([
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 1,
|
||||
source: [],
|
||||
executionStatus: 'canceled',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 2,
|
||||
source: [],
|
||||
executionStatus: 'error',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { nodes: mappedNodes } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
expect(mappedNodes.value[0]?.data?.runData?.iterations).toEqual(2);
|
||||
expect(mappedNodes.value[0]?.data?.runData?.visible).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return 0 iterations when all tasks are canceled', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const nodes = [createTestNode({ name: 'Node 1' })];
|
||||
const connections = {};
|
||||
const workflowObject = createTestWorkflowObject({ nodes, connections });
|
||||
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue([
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
executionStatus: 'canceled',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 1,
|
||||
source: [],
|
||||
executionStatus: 'canceled',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { nodes: mappedNodes } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
expect(mappedNodes.value[0]?.data?.runData?.iterations).toEqual(0);
|
||||
expect(mappedNodes.value[0]?.data?.runData?.visible).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return correct count when no canceled tasks', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const nodes = [createTestNode({ name: 'Node 1' })];
|
||||
const connections = {};
|
||||
const workflowObject = createTestWorkflowObject({ nodes, connections });
|
||||
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue([
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 1,
|
||||
source: [],
|
||||
executionStatus: 'error',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { nodes: mappedNodes } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
expect(mappedNodes.value[0]?.data?.runData?.iterations).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nodeExecutionStatusById', () => {
|
||||
it('should return last execution status when not canceled', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const node = createTestNode({ name: 'Test Node' });
|
||||
const nodes = [node];
|
||||
const connections = {};
|
||||
const workflowObject = createTestWorkflowObject({ nodes, connections });
|
||||
|
||||
workflowsStore.getWorkflowRunData = {
|
||||
'Test Node': [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { nodes: mappedNodes } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
expect(mappedNodes.value[0]?.data?.execution?.status).toEqual('success');
|
||||
});
|
||||
|
||||
it('should return second-to-last status when last execution is canceled and multiple tasks exist', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const node = createTestNode({ name: 'Test Node' });
|
||||
const nodes = [node];
|
||||
const connections = {};
|
||||
const workflowObject = createTestWorkflowObject({ nodes, connections });
|
||||
|
||||
workflowsStore.getWorkflowRunData = {
|
||||
'Test Node': [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
},
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 1,
|
||||
source: [],
|
||||
executionStatus: 'canceled',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { nodes: mappedNodes } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
expect(mappedNodes.value[0]?.data?.execution?.status).toEqual('success');
|
||||
});
|
||||
|
||||
it('should return canceled status when only one task exists and it is canceled', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const node = createTestNode({ name: 'Test Node' });
|
||||
const nodes = [node];
|
||||
const connections = {};
|
||||
const workflowObject = createTestWorkflowObject({ nodes, connections });
|
||||
|
||||
workflowsStore.getWorkflowRunData = {
|
||||
'Test Node': [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
executionStatus: 'canceled',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { nodes: mappedNodes } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
expect(mappedNodes.value[0]?.data?.execution?.status).toEqual('canceled');
|
||||
});
|
||||
|
||||
it('should return new status when no tasks exist', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const node = createTestNode({ name: 'Test Node' });
|
||||
const nodes = [node];
|
||||
const connections = {};
|
||||
const workflowObject = createTestWorkflowObject({ nodes, connections });
|
||||
|
||||
workflowsStore.getWorkflowRunData = {};
|
||||
|
||||
const { nodes: mappedNodes } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
expect(mappedNodes.value[0]?.data?.execution?.status).toEqual('new');
|
||||
});
|
||||
});
|
||||
|
||||
describe('nodeHasIssuesById', () => {
|
||||
it('should return false when node has no issues or errors', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
@@ -1503,5 +1869,203 @@ describe('useCanvasMapping', () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('connection status with canceled tasks', () => {
|
||||
it('should not set success status when last task is canceled', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
|
||||
const nodes = [manualTriggerNode, setNode];
|
||||
const connections = {
|
||||
[manualTriggerNode.name]: {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: setNode.name, type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
};
|
||||
const workflowObject = createTestWorkflowObject({
|
||||
nodes,
|
||||
connections,
|
||||
});
|
||||
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => {
|
||||
if (nodeName === manualTriggerNode.name) {
|
||||
return [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
executionStatus: 'canceled',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const { connections: mappedConnections } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
expect(mappedConnections.value[0]?.data?.status).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should set success status when last task is canceled but previous is success', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
|
||||
const nodes = [manualTriggerNode, setNode];
|
||||
const connections = {
|
||||
[manualTriggerNode.name]: {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: setNode.name, type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
};
|
||||
const workflowObject = createTestWorkflowObject({
|
||||
nodes,
|
||||
connections,
|
||||
});
|
||||
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => {
|
||||
if (nodeName === manualTriggerNode.name) {
|
||||
return [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 1,
|
||||
source: [],
|
||||
executionStatus: 'canceled',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const { connections: mappedConnections } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
expect(mappedConnections.value[0]?.data?.status).toEqual('success');
|
||||
});
|
||||
|
||||
it('should handle connection with only canceled tasks', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
|
||||
const nodes = [manualTriggerNode, setNode];
|
||||
const connections = {
|
||||
[manualTriggerNode.name]: {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: setNode.name, type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
};
|
||||
const workflowObject = createTestWorkflowObject({
|
||||
nodes,
|
||||
connections,
|
||||
});
|
||||
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => {
|
||||
if (nodeName === manualTriggerNode.name) {
|
||||
return [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
executionStatus: 'canceled',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 1,
|
||||
source: [],
|
||||
executionStatus: 'canceled',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const { connections: mappedConnections } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
expect(mappedConnections.value[0]?.data?.status).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should prioritize running status over canceled task handling', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
|
||||
const nodes = [manualTriggerNode, setNode];
|
||||
const connections = {
|
||||
[manualTriggerNode.name]: {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: setNode.name, type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
};
|
||||
const workflowObject = createTestWorkflowObject({
|
||||
nodes,
|
||||
connections,
|
||||
});
|
||||
|
||||
workflowsStore.isNodeExecuting.mockImplementation((nodeName: string) => {
|
||||
return nodeName === manualTriggerNode.name;
|
||||
});
|
||||
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => {
|
||||
if (nodeName === manualTriggerNode.name) {
|
||||
return [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
executionStatus: 'canceled',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const { connections: mappedConnections } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
expect(mappedConnections.value[0]?.data?.status).toEqual('running');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -345,7 +345,11 @@ export function useCanvasMapping({
|
||||
nodes.value.reduce<Record<string, ExecutionStatus>>((acc, node) => {
|
||||
const tasks = workflowsStore.getWorkflowRunData?.[node.name] ?? [];
|
||||
|
||||
acc[node.id] = tasks.at(-1)?.executionStatus ?? 'new';
|
||||
let lastExecutionStatus = tasks.at(-1)?.executionStatus;
|
||||
if (tasks.length > 1 && lastExecutionStatus === 'canceled') {
|
||||
lastExecutionStatus = tasks.at(-2)?.executionStatus;
|
||||
}
|
||||
acc[node.id] = lastExecutionStatus ?? 'new';
|
||||
return acc;
|
||||
}, {}),
|
||||
);
|
||||
@@ -382,7 +386,9 @@ export function useCanvasMapping({
|
||||
acc[nodeId][connectionType][outputIndex] = acc[nodeId][connectionType][
|
||||
outputIndex
|
||||
] ?? { ...outputData };
|
||||
acc[nodeId][connectionType][outputIndex].iterations += 1;
|
||||
if (runIteration.executionStatus !== 'canceled') {
|
||||
acc[nodeId][connectionType][outputIndex].iterations += 1;
|
||||
}
|
||||
acc[nodeId][connectionType][outputIndex].total +=
|
||||
connectionTypeOutputIndexData.length;
|
||||
}
|
||||
@@ -593,6 +599,14 @@ export function useCanvasMapping({
|
||||
}, {});
|
||||
});
|
||||
|
||||
function filterOutCanceled(tasks: ITaskData[] | null): ITaskData[] | null {
|
||||
if (!tasks) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return tasks.filter((task) => task.executionStatus !== 'canceled');
|
||||
}
|
||||
|
||||
const mappedNodes = computed<CanvasNode[]>(() => {
|
||||
const connectionsBySourceNode = connections.value;
|
||||
const connectionsByDestinationNode =
|
||||
@@ -635,7 +649,7 @@ export function useCanvasMapping({
|
||||
},
|
||||
runData: {
|
||||
outputMap: nodeExecutionRunDataOutputMapById.value[node.id],
|
||||
iterations: nodeExecutionRunDataById.value[node.id]?.length ?? 0,
|
||||
iterations: filterOutCanceled(nodeExecutionRunDataById.value[node.id])?.length ?? 0,
|
||||
visible: !!nodeExecutionRunDataById.value[node.id],
|
||||
},
|
||||
render: renderTypeByNodeId.value[node.id] ?? { type: 'default', options: {} },
|
||||
@@ -677,6 +691,12 @@ export function useCanvasMapping({
|
||||
const runDataTotal =
|
||||
nodeExecutionRunDataOutputMapById.value[connection.source]?.[type]?.[index]?.total ?? 0;
|
||||
|
||||
const sourceTasks = nodeExecutionRunDataById.value[connection.source] ?? [];
|
||||
let lastSourceTask: ITaskData | undefined = sourceTasks[sourceTasks.length - 1];
|
||||
if (lastSourceTask?.executionStatus === 'canceled' && sourceTasks.length > 1) {
|
||||
lastSourceTask = sourceTasks[sourceTasks.length - 2];
|
||||
}
|
||||
|
||||
let status: CanvasConnectionData['status'];
|
||||
if (nodeExecutionRunningById.value[connection.source]) {
|
||||
status = 'running';
|
||||
@@ -687,7 +707,7 @@ export function useCanvasMapping({
|
||||
status = 'pinned';
|
||||
} else if (nodeHasIssuesById.value[connection.source]) {
|
||||
status = 'error';
|
||||
} else if (runDataTotal > 0) {
|
||||
} else if (runDataTotal > 0 && lastSourceTask?.executionStatus !== 'canceled') {
|
||||
status = 'success';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user