mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-13 16:14:07 -08:00
d3 tree and cluster layouts
This commit is contained in:
parent
d7ba206b30
commit
fc591dec91
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
97
packages/editor-ui/src/components/canvas/autoLayout.ts
Normal file
97
packages/editor-ui/src/components/canvas/autoLayout.ts
Normal 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,
|
||||
};
|
||||
};
|
107
pnpm-lock.yaml
107
pnpm-lock.yaml
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue