diff --git a/packages/editor-ui/src/components/Node.vue b/packages/editor-ui/src/components/Node.vue
index f7da98475e..d952659836 100644
--- a/packages/editor-ui/src/components/Node.vue
+++ b/packages/editor-ui/src/components/Node.vue
@@ -61,6 +61,7 @@ import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import {
INodeTypeDescription,
+ ITaskData,
NodeHelpers,
} from 'n8n-workflow';
@@ -76,8 +77,11 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
NodeIcon,
},
computed: {
- workflowDataItems () {
- const workflowResultDataNode = this.$store.getters.getWorkflowResultDataByNodeName(this.data.name);
+ nodeRunData(): ITaskData[] {
+ return this.$store.getters.getWorkflowResultDataByNodeName(this.data.name);
+ },
+ workflowDataItems (): Number {
+ const workflowResultDataNode = this.nodeRunData;
if (workflowResultDataNode === null) {
return 0;
}
@@ -161,9 +165,15 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
this.setSubtitle();
}
},
+ nodeRunData(newValue) {
+ this.$emit('run', {name: this.data.name, data: newValue});
+ },
},
mounted() {
this.setSubtitle();
+ setTimeout(() => {
+ this.$emit('run', {name: this.data.name, data: this.nodeRunData});
+ }, 0);
},
data () {
return {
@@ -402,6 +412,10 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
z-index: 5;
}
+.jtk-connector.jtk-success {
+ z-index: 5;
+}
+
.jtk-endpoint {
z-index:5;
}
diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue
index 25ee44d30e..d45410937d 100644
--- a/packages/editor-ui/src/views/NodeView.vue
+++ b/packages/editor-ui/src/views/NodeView.vue
@@ -22,6 +22,7 @@
@removeNode="removeNode"
@runWorkflow="runWorkflow"
@moved="onNodeMoved"
+ @run="onNodeRun"
:id="'node-' + getNodeIndex(nodeData.name)"
:key="getNodeIndex(nodeData.name)"
:name="nodeData.name"
@@ -132,7 +133,7 @@ import NodeCreator from '@/components/NodeCreator/NodeCreator.vue';
import NodeSettings from '@/components/NodeSettings.vue';
import RunData from '@/components/RunData.vue';
-import { getLeftmostTopNode, getWorkflowCorners, scaleSmaller, scaleBigger, scaleReset, addOrRemoveMidpointArrow, addEndpointArrow, getDefaultOverlays, getIcon, getNewNodePosition } from './helpers';
+import { getLeftmostTopNode, getWorkflowCorners, scaleSmaller, scaleBigger, scaleReset, showOrHideMidpointArrow, addEndpointArrow, getDefaultOverlays, getIcon, getNewNodePosition, hideMidpointArrow } from './helpers';
import mixins from 'vue-typed-mixins';
import { v4 as uuidv4} from 'uuid';
@@ -145,14 +146,14 @@ import {
INodeIssues,
INodeTypeDescription,
INodeTypeNameVersion,
- NodeInputConnections,
NodeHelpers,
Workflow,
IRun,
+ ITaskData,
INodeCredentialsDetails,
+ INodeExecutionData,
} from 'n8n-workflow';
import {
- IConnectionsUi,
ICredentialsResponse,
IExecutionResponse,
IN8nUISettings,
@@ -1329,7 +1330,7 @@ export default mixins(
info.connection.setConnector(['Flowchart', { cornerRadius: 8, stub: JSPLUMB_FLOWCHART_STUB, gap: 5, alwaysRespectStubs: false}]);
addEndpointArrow(info.connection);
- addOrRemoveMidpointArrow(info.connection);
+ showOrHideMidpointArrow(info.connection);
// @ts-ignore
const sourceInfo = info.sourceEndpoint.getParameters();
@@ -1355,9 +1356,11 @@ export default mixins(
const overlay = info.connection.getOverlay('connection-actions');
overlay.setVisible(true);
- const arrow = info.connection.getOverlay('midpoint-arrow');
- if (arrow) {
- arrow.setVisible(false);
+ hideMidpointArrow(info.connection);
+
+ const itemsOverlay = info.connection.getOverlay('output-items-label');
+ if (itemsOverlay) {
+ itemsOverlay.setVisible(false);
}
});
info.connection.bind('mouseout', (connection: IConnection) => {
@@ -1366,9 +1369,11 @@ export default mixins(
overlay.setVisible(false);
timer = undefined;
- const arrow = info.connection.getOverlay('midpoint-arrow');
- if (arrow) {
- arrow.setVisible(true);
+ showOrHideMidpointArrow(info.connection);
+
+ const itemsOverlay = info.connection.getOverlay('output-items-label');
+ if (itemsOverlay) {
+ itemsOverlay.setVisible(true);
}
}, 500);
});
@@ -1722,7 +1727,128 @@ export default mixins(
}) as Connection[];
[...incoming, ...outgoing].forEach((connection: Connection) => {
- addOrRemoveMidpointArrow(connection);
+ showOrHideMidpointArrow(connection);
+ });
+ },
+ onNodeRun ({name, data}: {name: string, data: ITaskData[] | null}) {
+ const sourceIndex = this.$store.getters.getNodeIndex(name);
+ const sourceId = `${NODE_NAME_PREFIX}${sourceIndex}`;
+
+ if (data === null || data.length === 0) {
+ // @ts-ignore
+ const outgoing = this.instance.getConnections({
+ source: sourceId,
+ }) as Connection[];
+
+ outgoing.forEach((connection: Connection) => {
+ const arrow = connection.getOverlay('midpoint-arrow');
+ if (arrow) {
+ // @ts-ignore
+ arrow.setLocation(0.5);
+ }
+
+ connection.removeOverlay('output-items-label');
+ connection.setPaintStyle({stroke: getStyleTokenValue('--color-foreground-dark')});
+ });
+
+ return;
+ }
+
+ const nodeConnections = (this.$store.getters.outgoingConnectionsByNodeName(name) as INodeConnections).main;
+ if (!nodeConnections) {
+ return;
+ }
+
+ const outputMap: {[sourceEndpoint: string]: {[targetId: string]: {[targetEndpoint: string]: {total: number, iterations: number}}}} = {};
+
+ data.forEach((run: ITaskData) => {
+ if (!run.data) {
+ return;
+ }
+
+ run.data.main.forEach((output: INodeExecutionData[] | null, i: number) => {
+ nodeConnections[i]
+ .map((conn: IConnection) => {
+ const targetIndex = this.getNodeIndex(conn.node);
+ const targetId = `${NODE_NAME_PREFIX}${targetIndex}`;
+
+ const sourceEndpoint = `${sourceIndex}-output${i}`;
+ const targetEndpoint = `${targetIndex}-input${conn.index}`;
+
+ if (!outputMap[sourceEndpoint]) {
+ outputMap[sourceEndpoint] = {};
+ }
+
+ if (!outputMap[sourceEndpoint][targetId]) {
+ outputMap[sourceEndpoint][targetId] = {};
+ }
+
+ if (!outputMap[sourceEndpoint][targetId][targetEndpoint]) {
+ outputMap[sourceEndpoint][targetId][targetEndpoint] = {
+ total: 0,
+ iterations: 0,
+ };
+ }
+
+ outputMap[sourceEndpoint][targetId][targetEndpoint].total += output ? output.length : 0;
+ outputMap[sourceEndpoint][targetId][targetEndpoint].iterations += output ? 1 : 0;
+ });
+ });
+ });
+
+ Object.keys(outputMap).forEach((sourceEndpoint: string) => {
+ Object.keys(outputMap[sourceEndpoint]).forEach((targetId: string) => {
+ Object.keys(outputMap[sourceEndpoint][targetId]).forEach((targetEndpoint: string) => {
+ // @ts-ignore
+ const connections = this.instance.getConnections({
+ source: sourceId,
+ target: targetId,
+ }) as Connection[];
+
+ const conn = connections.find((connection: Connection) => {
+ // @ts-ignore
+ const uuids = connection.getUuids();
+ return uuids[0] === sourceEndpoint && uuids[1] === targetEndpoint;
+ });
+
+ if (!conn) {
+ return;
+ }
+
+ const output = outputMap[sourceEndpoint][targetId][targetEndpoint];
+ if (!output || !output.total) {
+ conn.setPaintStyle({stroke: getStyleTokenValue('--color-foreground-dark')});
+ conn.removeOverlay('output-items-label');
+ return;
+ }
+
+ conn.setPaintStyle({stroke: getStyleTokenValue('--color-success')});
+
+ if (conn.getOverlay('output-items-label')) {
+ conn.removeOverlay('output-items-label');
+ }
+
+ let label = `${output.total}`;
+ label = output.total > 1 ? `${label} items` : `${label} item`;
+ label = output.iterations > 1 ? `${label} total` : label;
+
+ conn.addOverlay([
+ 'Label',
+ {
+ id: 'output-items-label',
+ label,
+ cssClass: 'connection-output-name-label',
+ location: .5,
+ },
+ ]);
+
+ const arrow = connections[0].getOverlay('midpoint-arrow');
+ if (arrow) {
+ // @ts-ignore
+ arrow.setLocation(0.6);
+ }
+ });
+ });
});
},
removeNode (nodeName: string) {
diff --git a/packages/editor-ui/src/views/helpers.ts b/packages/editor-ui/src/views/helpers.ts
index fe034a3c2d..5d20570aa6 100644
--- a/packages/editor-ui/src/views/helpers.ts
+++ b/packages/editor-ui/src/views/helpers.ts
@@ -1,4 +1,3 @@
-import { JSPLUMB_FLOWCHART_STUB } from "@/constants";
import { INodeUi, IZoomConfig, XYPositon } from "@/Interface";
import { Connection, OverlaySpec } from "jsplumb";
@@ -95,6 +94,7 @@ export const getDefaultOverlays = (): OverlaySpec[] => ([
width: 12,
foldback: 1,
length: 10,
+ visible: true,
},
],
[
@@ -104,46 +104,10 @@ export const getDefaultOverlays = (): OverlaySpec[] => ([
label: 'Drop connection
to create node',
cssClass: 'drop-add-node-label',
location: 0.5,
+ visible: false,
},
],
-]);
-
-export const addEndpointArrow = (connection: Connection) => {
- const hasArrow = !!connection.getOverlay('midpoint-arrow');
- if (!hasArrow) {
- connection.addOverlay([
- 'Arrow',
- {
- id: 'endpoint-arrow',
- location: 1,
- width: 12,
- foldback: 1,
- length: 10,
- },
- ]);
- }
-};
-
-export const addOrRemoveMidpointArrow = (connection: Connection) => {
- const sourceEndpoint = connection.endpoints[0];
- const targetEndpoint = connection.endpoints[1];
- const requiresArrow = sourceEndpoint.anchor.lastReturnValue[0] >= targetEndpoint.anchor.lastReturnValue[0];
-
- const hasArrow = !!connection.getOverlay('midpoint-arrow');
-
- if (!requiresArrow) {
- if (hasArrow) {
- connection.removeOverlay('midpoint-arrow');
- }
-
- return;
- }
-
- if (hasArrow) {
- return;
- }
-
- connection.addOverlay([
+ [
'Arrow',
{
id: 'midpoint-arrow',
@@ -151,10 +115,42 @@ export const addOrRemoveMidpointArrow = (connection: Connection) => {
width: 12,
foldback: 1,
length: 10,
+ visible: false,
+ },
+ ],
+]);
+
+export const addEndpointArrow = (connection: Connection) => {
+ connection.addOverlay([
+ 'Arrow',
+ {
+ id: 'endpoint-arrow',
+ location: 1,
+ width: 12,
+ foldback: 1,
+ length: 10,
},
]);
};
+export const hideMidpointArrow = (connection: Connection) => {
+ const arrow = connection.getOverlay('midpoint-arrow');
+ if (arrow) {
+ arrow.setVisible(false);
+ }
+};
+
+export const showOrHideMidpointArrow = (connection: Connection) => {
+ const sourceEndpoint = connection.endpoints[0];
+ const targetEndpoint = connection.endpoints[1];
+ const requiresArrow = sourceEndpoint.anchor.lastReturnValue[0] >= targetEndpoint.anchor.lastReturnValue[0];
+
+ const arrow = connection.getOverlay('midpoint-arrow');
+ if (arrow) {
+ arrow.setVisible(requiresArrow);
+ }
+};
+
export const getIcon = (name: string): string => {
if (name === 'trash') {
return ``;