d3 tree and cluster layouts

This commit is contained in:
r00gm 2024-10-31 09:32:17 +01:00
parent d7ba206b30
commit fc591dec91
No known key found for this signature in database
4 changed files with 140 additions and 81 deletions

View file

@ -51,6 +51,7 @@
"change-case": "^5.4.4",
"chart.js": "^4.4.0",
"codemirror-lang-html-n8n": "^1.0.0",
"d3-hierarchy": "^3.1.2",
"dateformat": "^3.0.3",
"email-providers": "^2.0.1",
"esprima-next": "5.8.4",
@ -86,6 +87,7 @@
"@iconify/json": "^2.2.228",
"@pinia/testing": "^0.1.6",
"@sentry/vite-plugin": "^2.22.5",
"@types/d3-hierarchy": "^3.1.7",
"@types/dateformat": "^3.0.0",
"@types/file-saver": "^2.0.1",
"@types/humanize-duration": "^3.27.1",

View file

@ -13,7 +13,7 @@ import type {
NodeChange,
NodePositionChange,
} from '@vue-flow/core';
import { useVueFlow, VueFlow, PanelPosition, MarkerType } from '@vue-flow/core';
import { useVueFlow, VueFlow, Panel, PanelPosition, MarkerType } from '@vue-flow/core';
import { Background } from '@vue-flow/background';
import { MiniMap } from '@vue-flow/minimap';
import Node from './elements/nodes/CanvasNode.vue';
@ -44,6 +44,7 @@ import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue';
import { CanvasNodeRenderType } from '@/types';
import CanvasBackgroundStripedPattern from './elements/CanvasBackgroundStripedPattern.vue';
import { isMiddleMouseButton } from '@/utils/eventUtils';
import { useAutoLayout } from './autoLayout';
const $style = useCssModule();
@ -547,6 +548,8 @@ watch(() => props.readOnly, setReadonly, {
immediate: true,
});
const { autoLayout } = useAutoLayout(props.id);
const setAutoLayout = (type: 'cluster' | 'tree') => onNodesChange(autoLayout(type));
/**
* Provide
*/
@ -587,6 +590,16 @@ provide(CanvasKey, {
@move-end="onPaneMoveEnd"
@mousedown="onPaneMouseDown"
>
<Panel position="top-left">
<N8nButton
type="tertiary"
size="large"
@click="setAutoLayout('tree')"
style="margin-right: var(--spacing-xs)"
>tree</N8nButton
>
<N8nButton type="tertiary" size="large" @click="setAutoLayout('cluster')">cluster</N8nButton>
</Panel>
<template #node-canvas-node="canvasNodeProps">
<Node
v-bind="canvasNodeProps"

View file

@ -0,0 +1,97 @@
import { useVueFlow, type NodePositionChange } from '@vue-flow/core';
import * as d3 from 'd3-hierarchy';
import { CanvasNodeRenderType } from '@/types';
import type { CanvasNode } from '@/types';
export const useAutoLayout = (id: string) => {
const { getNodes, getIncomers, getOutgoers, findNode } = useVueFlow(id);
const getTriggersAndOrphans = (nodes: CanvasNode[]): CanvasNode[] => {
return nodes.filter((node) => {
if (node.data?.render.type !== CanvasNodeRenderType.Default) {
return false;
}
if (node.data.render.options.trigger) return true;
// the node is not connected to a parent node (orphan)
const incomers = getIncomers(node.id);
const outgoers = getOutgoers(node.id);
return incomers.length === 0 && outgoers.length === 0;
});
};
type Member = {
id: string;
children: Member[];
};
const useHierarchyBuilder = (nodes: CanvasNode[]) => {
// Recursion helper to prevent processing the same node more than once
const processed = new Set();
const buildHierarchy = (node: CanvasNode): Member => {
if (processed.has(node.id)) {
return {
id: node.id,
children: [],
};
}
processed.add(node.id);
const outGoers = getOutgoers(node).filter((out) => !processed.has(out.id));
const children = outGoers.map(buildHierarchy);
// ending nodes
const leafs = getIncomers(node.id)
.filter((income) => {
if (processed.has(income.id)) return false;
if (income.data.render.options.trigger) return false;
return !getIncomers(income.id).length;
})
.map((item) => ({ id: item.id, children: [] }));
return {
id: node.id,
children: children.concat(leafs),
// children,
};
};
return nodes.map(buildHierarchy);
};
const autoLayout = (type: 'cluster' | 'tree' = 'tree') => {
const startingNodes = getTriggersAndOrphans(getNodes.value);
const root = d3.hierarchy({
id: undefined,
children: useHierarchyBuilder(startingNodes),
});
const d3Tree = d3[type]<{ id: undefined; children: Member[] }>().nodeSize([200, 400])(root);
const changes = d3Tree.descendants().reduce<NodePositionChange[]>((acc, node) => {
const nodeId = node.data.id;
if (!nodeId) return acc;
const uiNode = findNode(nodeId);
if (!uiNode) return acc;
acc.push({
type: 'position',
from: uiNode.position,
id: nodeId,
position: { x: node.y, y: node.x },
});
return acc;
}, []);
return changes;
};
return {
autoLayout,
};
};

View file

@ -1090,7 +1090,7 @@ importers:
dependencies:
'@langchain/core':
specifier: 'catalog:'
version: 0.3.3(patch_hash=ekay3bw7hexufl733lypqvmx2e)(openai@4.63.0(zod@3.23.8))
version: 0.3.3(patch_hash=ekay3bw7hexufl733lypqvmx2e)(openai@4.63.0(encoding@0.1.13)(zod@3.23.8))
'@n8n/client-oauth2':
specifier: workspace:*
version: link:../@n8n/client-oauth2
@ -1405,6 +1405,9 @@ importers:
codemirror-lang-html-n8n:
specifier: ^1.0.0
version: 1.0.0
d3-hierarchy:
specifier: ^3.1.2
version: 3.1.2
dateformat:
specifier: ^3.0.3
version: 3.0.3
@ -1505,6 +1508,9 @@ importers:
'@sentry/vite-plugin':
specifier: ^2.22.5
version: 2.22.5(encoding@0.1.13)
'@types/d3-hierarchy':
specifier: ^3.1.7
version: 3.1.7
'@types/dateformat':
specifier: ^3.0.0
version: 3.0.1
@ -1921,7 +1927,7 @@ importers:
devDependencies:
'@langchain/core':
specifier: 'catalog:'
version: 0.3.3(patch_hash=ekay3bw7hexufl733lypqvmx2e)(openai@4.63.0)
version: 0.3.3(patch_hash=ekay3bw7hexufl733lypqvmx2e)(openai@4.63.0(encoding@0.1.13)(zod@3.23.8))
'@types/deep-equal':
specifier: ^1.0.1
version: 1.0.1
@ -2227,7 +2233,7 @@ packages:
'@azure/core-http@3.0.4':
resolution: {integrity: sha512-Fok9VVhMdxAFOtqiiAtg74fL0UJkt0z3D+ouUUxcRLzZNBioPRAMJFVxiWoJljYpXsRi4GDQHzQHDc9AiYaIUQ==}
engines: {node: '>=14.0.0'}
deprecated: deprecating as we migrated to core v2
deprecated: This package is no longer supported. Please migrate to use @azure/core-rest-pipeline
'@azure/core-lro@2.4.0':
resolution: {integrity: sha512-F65+rYkll1dpw3RGm8/SSiSj+/QkMeYDanzS/QKlM1dmuneVyXbO46C88V1MRHluLGdMP6qfD3vDRYALn0z0tQ==}
@ -4809,6 +4815,9 @@ packages:
'@types/cookiejar@2.1.5':
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
'@types/d3-hierarchy@3.1.7':
resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==}
'@types/dateformat@3.0.1':
resolution: {integrity: sha512-KlPPdikagvL6ELjWsljbyDIPzNCeliYkqRpI+zea99vBBbCIA5JNshZAwQKTON139c87y9qvTFVgkFd14rtS4g==}
@ -6531,6 +6540,10 @@ packages:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
d3-hierarchy@3.1.2:
resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==}
engines: {node: '>=12'}
d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
@ -14689,38 +14702,6 @@ snapshots:
transitivePeerDependencies:
- openai
'@langchain/core@0.3.3(patch_hash=ekay3bw7hexufl733lypqvmx2e)(openai@4.63.0(zod@3.23.8))':
dependencies:
ansi-styles: 5.2.0
camelcase: 6.3.0
decamelize: 1.2.0
js-tiktoken: 1.0.12
langsmith: 0.1.59(openai@4.63.0(zod@3.23.8))
mustache: 4.2.0
p-queue: 6.6.2
p-retry: 4.6.2
uuid: 10.0.0
zod: 3.23.8
zod-to-json-schema: 3.23.3(zod@3.23.8)
transitivePeerDependencies:
- openai
'@langchain/core@0.3.3(patch_hash=ekay3bw7hexufl733lypqvmx2e)(openai@4.63.0)':
dependencies:
ansi-styles: 5.2.0
camelcase: 6.3.0
decamelize: 1.2.0
js-tiktoken: 1.0.12
langsmith: 0.1.59(openai@4.63.0)
mustache: 4.2.0
p-queue: 6.6.2
p-retry: 4.6.2
uuid: 10.0.0
zod: 3.23.8
zod-to-json-schema: 3.23.3(zod@3.23.8)
transitivePeerDependencies:
- openai
'@langchain/google-common@0.1.1(@langchain/core@0.3.3(patch_hash=ekay3bw7hexufl733lypqvmx2e)(openai@4.63.0(encoding@0.1.13)(zod@3.23.8)))(zod@3.23.8)':
dependencies:
'@langchain/core': 0.3.3(patch_hash=ekay3bw7hexufl733lypqvmx2e)(openai@4.63.0(encoding@0.1.13)(zod@3.23.8))
@ -16484,6 +16465,8 @@ snapshots:
'@types/cookiejar@2.1.5': {}
'@types/d3-hierarchy@3.1.7': {}
'@types/dateformat@3.0.1': {}
'@types/deep-equal@1.0.1': {}
@ -17232,7 +17215,7 @@ snapshots:
'@vue/test-utils@2.4.6':
dependencies:
js-beautify: 1.14.9
vue-component-type-helpers: 2.1.6
vue-component-type-helpers: 2.1.8
'@vueuse/components@10.11.0(vue@3.5.11(typescript@5.6.2))':
dependencies:
@ -18574,6 +18557,8 @@ snapshots:
d3-ease@3.0.1: {}
d3-hierarchy@3.1.2: {}
d3-interpolate@3.0.1:
dependencies:
d3-color: 3.1.0
@ -19316,7 +19301,7 @@ snapshots:
eslint-import-resolver-node@0.3.9:
dependencies:
debug: 3.2.7(supports-color@5.5.0)
debug: 3.2.7(supports-color@8.1.1)
is-core-module: 2.13.1
resolve: 1.22.8
transitivePeerDependencies:
@ -19341,7 +19326,7 @@ snapshots:
eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.6.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0):
dependencies:
debug: 3.2.7(supports-color@5.5.0)
debug: 3.2.7(supports-color@8.1.1)
optionalDependencies:
'@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.6.2)
eslint: 8.57.0
@ -19361,7 +19346,7 @@ snapshots:
array.prototype.findlastindex: 1.2.3
array.prototype.flat: 1.3.2
array.prototype.flatmap: 1.3.2
debug: 3.2.7(supports-color@5.5.0)
debug: 3.2.7(supports-color@8.1.1)
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
@ -20159,7 +20144,7 @@ snapshots:
array-parallel: 0.1.3
array-series: 0.1.5
cross-spawn: 4.0.2
debug: 3.2.7(supports-color@5.5.0)
debug: 3.2.7(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@ -21498,28 +21483,6 @@ snapshots:
optionalDependencies:
openai: 4.63.0(encoding@0.1.13)(zod@3.23.8)
langsmith@0.1.59(openai@4.63.0(zod@3.23.8)):
dependencies:
'@types/uuid': 10.0.0
commander: 10.0.1
p-queue: 6.6.2
p-retry: 4.6.2
semver: 7.6.0
uuid: 10.0.0
optionalDependencies:
openai: 4.63.0(zod@3.23.8)
langsmith@0.1.59(openai@4.63.0):
dependencies:
'@types/uuid': 10.0.0
commander: 10.0.1
p-queue: 6.6.2
p-retry: 4.6.2
semver: 7.6.0
uuid: 10.0.0
optionalDependencies:
openai: 4.63.0(zod@3.23.8)
lazy-ass@1.6.0: {}
ldapts@4.2.6:
@ -22864,22 +22827,6 @@ snapshots:
- encoding
- supports-color
openai@4.63.0(zod@3.23.8):
dependencies:
'@types/node': 18.16.16
'@types/node-fetch': 2.6.4
abort-controller: 3.0.0
agentkeepalive: 4.2.1
form-data-encoder: 1.7.2
formdata-node: 4.4.1
node-fetch: 2.7.0(encoding@0.1.13)
optionalDependencies:
zod: 3.23.8
transitivePeerDependencies:
- encoding
- supports-color
optional: true
openapi-sampler@1.5.1:
dependencies:
'@types/json-schema': 7.0.15
@ -23060,7 +23007,7 @@ snapshots:
pdf-parse@1.1.1:
dependencies:
debug: 3.2.7(supports-color@5.5.0)
debug: 3.2.7(supports-color@8.1.1)
node-ensure: 0.0.0
transitivePeerDependencies:
- supports-color
@ -23889,7 +23836,7 @@ snapshots:
rhea@1.0.24:
dependencies:
debug: 3.2.7(supports-color@5.5.0)
debug: 3.2.7(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color