fix(editor): Provide correct node output runData information in new canvas (no-changelog) (#10691)

This commit is contained in:
Alex Grozav
2024-09-06 13:37:44 +03:00
committed by GitHub
parent 156eb72ebe
commit 468f01aaa8
16 changed files with 421 additions and 50 deletions

View File

@@ -2,18 +2,18 @@ import type { Ref } from 'vue';
import { ref } from 'vue';
import { NodeConnectionType } from 'n8n-workflow';
import type { Workflow } from 'n8n-workflow';
import { createPinia, setActivePinia } from 'pinia';
import { setActivePinia } from 'pinia';
import { useCanvasMapping } from '@/composables/useCanvasMapping';
import type { INodeUi } from '@/Interface';
import {
createTestNode,
createTestWorkflowObject,
mockNode,
mockNodes,
mockNodeTypeDescription,
} from '@/__tests__/mocks';
import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE, STICKY_NODE_TYPE, STORES } from '@/constants';
import { useWorkflowsStore } from '@/stores/workflows.store';
import {
createCanvasConnectionHandleString,
@@ -21,19 +21,32 @@ import {
} from '@/utils/canvasUtilsV2';
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
import { MarkerType } from '@vue-flow/core';
import { createTestingPinia } from '@pinia/testing';
import { mockedStore } from '@/__tests__/utils';
beforeEach(() => {
const pinia = createPinia();
const pinia = createTestingPinia({
initialState: {
[STORES.WORKFLOWS]: {
workflowExecutionData: {},
},
[STORES.NODE_TYPES]: {
nodeTypes: {
[MANUAL_TRIGGER_NODE_TYPE]: {
1: mockNodeTypeDescription({
name: MANUAL_TRIGGER_NODE_TYPE,
}),
},
[SET_NODE_TYPE]: {
1: mockNodeTypeDescription({
name: SET_NODE_TYPE,
}),
},
},
},
},
});
setActivePinia(pinia);
useNodeTypesStore().setNodeTypes([
mockNodeTypeDescription({
name: MANUAL_TRIGGER_NODE_TYPE,
}),
mockNodeTypeDescription({
name: SET_NODE_TYPE,
}),
]);
});
afterEach(() => {
@@ -61,6 +74,7 @@ describe('useCanvasMapping', () => {
describe('nodes', () => {
it('should map nodes to canvas nodes', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const manualTriggerNode = mockNode({
name: 'Manual Trigger',
type: MANUAL_TRIGGER_NODE_TYPE,
@@ -79,6 +93,8 @@ describe('useCanvasMapping', () => {
workflowObject: ref(workflowObject) as Ref<Workflow>,
});
workflowsStore.isNodeExecuting.mockReturnValue(false);
expect(mappedNodes.value).toEqual([
{
id: manualTriggerNode.id,
@@ -106,7 +122,8 @@ describe('useCanvasMapping', () => {
visible: false,
},
runData: {
count: 0,
iterations: 0,
outputMap: {},
visible: false,
},
inputs: [
@@ -169,6 +186,7 @@ describe('useCanvasMapping', () => {
});
it('should handle execution state', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const manualTriggerNode = mockNode({
name: 'Manual Trigger',
type: MANUAL_TRIGGER_NODE_TYPE,
@@ -181,7 +199,7 @@ describe('useCanvasMapping', () => {
connections,
});
useWorkflowsStore().addExecutingNode(manualTriggerNode.name);
workflowsStore.isNodeExecuting.mockReturnValue(true);
const { nodes: mappedNodes } = useCanvasMapping({
nodes: ref(nodes),
@@ -336,6 +354,195 @@ describe('useCanvasMapping', () => {
});
});
});
describe('runData', () => {
describe('nodeExecutionRunDataOutputMapById', () => {
it('should return an empty object when there is no run data', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const nodes: INodeUi[] = [];
const connections = {};
const workflowObject = createTestWorkflowObject({
nodes,
connections,
});
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue(null);
const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({
nodes: ref(nodes),
connections: ref(connections),
workflowObject: ref(workflowObject) as Ref<Workflow>,
});
expect(nodeExecutionRunDataOutputMapById.value).toEqual({});
});
it('should calculate iterations and total correctly for single node', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const nodes = [createTestNode({ name: 'Node 1' })];
const connections = {};
const workflowObject = createTestWorkflowObject({
nodes,
connections,
});
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue([
{
startTime: 0,
executionTime: 0,
source: [],
data: {
[NodeConnectionType.Main]: [[{ json: {} }, { json: {} }]],
},
},
]);
const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({
nodes: ref(nodes),
connections: ref(connections),
workflowObject: ref(workflowObject) as Ref<Workflow>,
});
expect(nodeExecutionRunDataOutputMapById.value).toEqual({
[nodes[0].id]: {
[NodeConnectionType.Main]: {
0: {
iterations: 1,
total: 2,
},
},
},
});
});
it('should handle multiple nodes with different connection types', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const nodes = [
createTestNode({ id: 'node1', name: 'Node 1' }),
createTestNode({ id: 'node2', name: 'Node 2' }),
];
const connections = {};
const workflowObject = createTestWorkflowObject({
nodes,
connections,
});
workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => {
if (nodeName === 'Node 1') {
return [
{
startTime: 0,
executionTime: 0,
source: [],
data: {
[NodeConnectionType.Main]: [[{ json: {} }]],
[NodeConnectionType.AiAgent]: [[{ json: {} }, { json: {} }]],
},
},
];
} else if (nodeName === 'Node 2') {
return [
{
startTime: 0,
executionTime: 0,
source: [],
data: {
[NodeConnectionType.Main]: [[{ json: {} }, { json: {} }, { json: {} }]],
},
},
];
}
return null;
});
const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({
nodes: ref(nodes),
connections: ref(connections),
workflowObject: ref(workflowObject) as Ref<Workflow>,
});
expect(nodeExecutionRunDataOutputMapById.value).toEqual({
node1: {
[NodeConnectionType.Main]: {
0: {
iterations: 1,
total: 1,
},
},
[NodeConnectionType.AiAgent]: {
0: {
iterations: 1,
total: 2,
},
},
},
node2: {
[NodeConnectionType.Main]: {
0: {
iterations: 1,
total: 3,
},
},
},
});
});
it('handles multiple 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,
source: [],
data: {
[NodeConnectionType.Main]: [[{ json: {} }]],
},
},
{
startTime: 0,
executionTime: 0,
source: [],
data: {
[NodeConnectionType.Main]: [[{ json: {} }, { json: {} }, { json: {} }]],
},
},
{
startTime: 0,
executionTime: 0,
source: [],
data: {
[NodeConnectionType.Main]: [[{ json: {} }, { json: {} }]],
},
},
]);
const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({
nodes: ref(nodes),
connections: ref(connections),
workflowObject: ref(workflowObject) as Ref<Workflow>,
});
expect(nodeExecutionRunDataOutputMapById.value).toEqual({
[nodes[0].id]: {
[NodeConnectionType.Main]: {
0: {
iterations: 3,
total: 6,
},
},
},
});
});
});
});
});
describe('connections', () => {

View File

@@ -18,11 +18,13 @@ import type {
CanvasNodeDefaultRender,
CanvasNodeDefaultRenderLabelSize,
CanvasNodeStickyNoteRender,
ExecutionOutputMap,
} from '@/types';
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
import {
mapLegacyConnectionsToCanvasConnections,
mapLegacyEndpointsToCanvasConnectionPort,
parseCanvasConnectionHandleString,
} from '@/utils/canvasUtilsV2';
import type {
ExecutionStatus,
@@ -241,6 +243,39 @@ export function useCanvasMapping({
}, {}),
);
const nodeExecutionRunDataOutputMapById = computed(() =>
Object.keys(nodeExecutionRunDataById.value).reduce<Record<string, ExecutionOutputMap>>(
(acc, nodeId) => {
acc[nodeId] = {};
const outputData = { iterations: 0, total: 0 };
for (const runIteration of nodeExecutionRunDataById.value[nodeId] ?? []) {
const data = runIteration.data ?? {};
for (const connectionType of Object.keys(data)) {
const connectionTypeData = data[connectionType] ?? {};
acc[nodeId][connectionType] = acc[nodeId][connectionType] ?? {};
for (const outputIndex of Object.keys(connectionTypeData)) {
const parsedOutputIndex = parseInt(outputIndex, 10);
const connectionTypeOutputIndexData = connectionTypeData[parsedOutputIndex] ?? [];
acc[nodeId][connectionType][outputIndex] = acc[nodeId][connectionType][
outputIndex
] ?? { ...outputData };
acc[nodeId][connectionType][outputIndex].iterations += 1;
acc[nodeId][connectionType][outputIndex].total +=
connectionTypeOutputIndexData.length;
}
}
}
return acc;
},
{},
),
);
const nodeIssuesById = computed(() =>
nodes.value.reduce<Record<string, string[]>>((acc, node) => {
const issues: string[] = [];
@@ -359,7 +394,8 @@ export function useCanvasMapping({
running: nodeExecutionRunningById.value[node.id],
},
runData: {
count: nodeExecutionRunDataById.value[node.id]?.length ?? 0,
outputMap: nodeExecutionRunDataOutputMapById.value[node.id],
iterations: nodeExecutionRunDataById.value[node.id]?.length ?? 0,
visible: !!nodeExecutionRunDataById.value[node.id],
},
render: renderTypeByNodeId.value[node.id] ?? { type: 'default', options: {} },
@@ -426,7 +462,6 @@ export function useCanvasMapping({
function getConnectionLabel(connection: CanvasConnection): string {
const fromNode = nodes.value.find((node) => node.name === connection.data?.fromNodeName);
if (!fromNode) {
return '';
}
@@ -438,10 +473,13 @@ export function useCanvasMapping({
interpolate: { count: String(pinnedDataCount) },
});
} else if (nodeExecutionRunDataById.value[fromNode.id]) {
const runDataCount = nodeExecutionRunDataById.value[fromNode.id]?.length ?? 0;
const { type, index } = parseCanvasConnectionHandleString(connection.sourceHandle);
const runDataTotal =
nodeExecutionRunDataOutputMapById.value[fromNode.id]?.[type]?.[index]?.total ?? 0;
return i18n.baseText('ndv.output.items', {
adjustToNumber: runDataCount,
interpolate: { count: String(runDataCount) },
adjustToNumber: runDataTotal,
interpolate: { count: String(runDataTotal) },
});
}
@@ -449,6 +487,7 @@ export function useCanvasMapping({
}
return {
nodeExecutionRunDataOutputMapById,
connections: mappedConnections,
nodes: mappedNodes,
};

View File

@@ -27,7 +27,8 @@ describe('useCanvasNode', () => {
expect(result.isSelected.value).toBeUndefined();
expect(result.pinnedDataCount.value).toBe(0);
expect(result.hasPinnedData.value).toBe(false);
expect(result.runDataCount.value).toBe(0);
expect(result.runDataOutputMap.value).toEqual({});
expect(result.runDataIterations.value).toBe(0);
expect(result.hasRunData.value).toBe(false);
expect(result.issues.value).toEqual([]);
expect(result.hasIssues.value).toBe(false);
@@ -54,7 +55,7 @@ describe('useCanvasNode', () => {
},
issues: { items: ['issue1'], visible: true },
execution: { status: 'running', waiting: 'waiting', running: true },
runData: { count: 1, visible: true },
runData: { outputMap: {}, iterations: 1, visible: true },
pinnedData: { count: 1, visible: true },
render: {
type: CanvasNodeRenderType.Default,
@@ -86,7 +87,8 @@ describe('useCanvasNode', () => {
expect(result.isSelected.value).toBe(true);
expect(result.pinnedDataCount.value).toBe(1);
expect(result.hasPinnedData.value).toBe(true);
expect(result.runDataCount.value).toBe(1);
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.hasIssues.value).toBe(true);

View File

@@ -10,9 +10,10 @@ import { CanvasNodeRenderType, CanvasConnectionMode } from '@/types';
export function useCanvasNode() {
const node = inject(CanvasNodeKey);
const data = computed<CanvasNodeData>(
const data = computed(
() =>
node?.data.value ?? {
node?.data.value ??
({
id: '',
name: '',
subtitle: '',
@@ -27,12 +28,12 @@ export function useCanvasNode() {
execution: {
running: false,
},
runData: { count: 0, visible: false },
runData: { iterations: 0, outputMap: {}, visible: false },
render: {
type: CanvasNodeRenderType.Default,
options: {},
},
},
} satisfies CanvasNodeData),
);
const id = computed(() => node?.id.value ?? '');
@@ -58,7 +59,8 @@ export function useCanvasNode() {
const executionWaiting = computed(() => data.value.execution.waiting);
const executionRunning = computed(() => data.value.execution.running);
const runDataCount = computed(() => data.value.runData.count);
const runDataOutputMap = computed(() => data.value.runData.outputMap);
const runDataIterations = computed(() => data.value.runData.iterations);
const hasRunData = computed(() => data.value.runData.visible);
const render = computed(() => data.value.render);
@@ -79,7 +81,8 @@ export function useCanvasNode() {
isSelected,
pinnedDataCount,
hasPinnedData,
runDataCount,
runDataIterations,
runDataOutputMap,
hasRunData,
issues,
hasIssues,

View File

@@ -14,9 +14,11 @@ export function useCanvasNodeHandle() {
const label = computed(() => handle?.label.value ?? '');
const isConnected = computed(() => handle?.isConnected.value ?? false);
const isConnecting = computed(() => handle?.isConnecting.value ?? false);
const isReadOnly = computed(() => handle?.isReadOnly.value);
const type = computed(() => handle?.type.value ?? NodeConnectionType.Main);
const mode = computed(() => handle?.mode.value ?? CanvasConnectionMode.Input);
const isReadOnly = computed(() => handle?.isReadOnly.value);
const index = computed(() => handle?.index.value ?? 0);
const runData = computed(() => handle?.runData.value);
return {
label,
@@ -25,5 +27,7 @@ export function useCanvasNodeHandle() {
isReadOnly,
type,
mode,
index,
runData,
};
}