@k-4u/resource-mapper-core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.aws-icons-last-updated +1 -0
- package/.svelte-kit/ambient.d.ts +263 -0
- package/.svelte-kit/generated/client/app.js +31 -0
- package/.svelte-kit/generated/client/matchers.js +1 -0
- package/.svelte-kit/generated/client/nodes/0.js +1 -0
- package/.svelte-kit/generated/client/nodes/1.js +1 -0
- package/.svelte-kit/generated/client/nodes/2.js +3 -0
- package/.svelte-kit/generated/client/nodes/3.js +3 -0
- package/.svelte-kit/generated/client-optimized/app.js +31 -0
- package/.svelte-kit/generated/client-optimized/matchers.js +1 -0
- package/.svelte-kit/generated/client-optimized/nodes/0.js +1 -0
- package/.svelte-kit/generated/client-optimized/nodes/1.js +1 -0
- package/.svelte-kit/generated/client-optimized/nodes/2.js +3 -0
- package/.svelte-kit/generated/client-optimized/nodes/3.js +3 -0
- package/.svelte-kit/generated/root.js +3 -0
- package/.svelte-kit/generated/root.svelte +68 -0
- package/.svelte-kit/generated/server/internal.js +53 -0
- package/.svelte-kit/non-ambient.d.ts +43 -0
- package/.svelte-kit/output/client/.vite/manifest.json +175 -0
- package/.svelte-kit/output/client/_app/immutable/assets/0.Czt_67iE.css +1 -0
- package/.svelte-kit/output/client/_app/immutable/assets/TeamContactCard.Dxj5nUCr.css +1 -0
- package/.svelte-kit/output/client/_app/immutable/assets/helpers.ysDrpaDf.css +1 -0
- package/.svelte-kit/output/client/_app/immutable/assets/libavoid.DQJapW5w.wasm +0 -0
- package/.svelte-kit/output/client/_app/immutable/chunks/BlLuv0eP.js +46 -0
- package/.svelte-kit/output/client/_app/immutable/chunks/CSBHmwYv.js +1 -0
- package/.svelte-kit/output/client/_app/immutable/chunks/CTCi5ueQ.js +1 -0
- package/.svelte-kit/output/client/_app/immutable/chunks/CfOzjaik.js +2 -0
- package/.svelte-kit/output/client/_app/immutable/chunks/D4PdvFNs.js +1 -0
- package/.svelte-kit/output/client/_app/immutable/chunks/DXgP-QUS.js +2 -0
- package/.svelte-kit/output/client/_app/immutable/chunks/DlbDC5An.js +1 -0
- package/.svelte-kit/output/client/_app/immutable/chunks/wRWe7aK9.js +1 -0
- package/.svelte-kit/output/client/_app/immutable/entry/app.ConrMuHl.js +2 -0
- package/.svelte-kit/output/client/_app/immutable/entry/start.Bm6FyGme.js +1 -0
- package/.svelte-kit/output/client/_app/immutable/nodes/0.d3cL-ETU.js +1 -0
- package/.svelte-kit/output/client/_app/immutable/nodes/1.D6z9rPGv.js +1 -0
- package/.svelte-kit/output/client/_app/immutable/nodes/2.CLD-8chl.js +1 -0
- package/.svelte-kit/output/client/_app/immutable/nodes/3.DXYeBoel.js +1 -0
- package/.svelte-kit/output/client/_app/version.json +1 -0
- package/.svelte-kit/output/client/libavoid.wasm +0 -0
- package/.svelte-kit/output/client/static/robots.txt +3 -0
- package/.svelte-kit/output/prerendered/dependencies/_app/env.js +1 -0
- package/.svelte-kit/output/server/.vite/manifest.json +224 -0
- package/.svelte-kit/output/server/_app/immutable/assets/LoadingOverlay.DBbe6V8W.css +1 -0
- package/.svelte-kit/output/server/_app/immutable/assets/_layout.Czt_67iE.css +1 -0
- package/.svelte-kit/output/server/_app/immutable/assets/_page.D9P41uDZ.css +1 -0
- package/.svelte-kit/output/server/chunks/ErrorDisplay.js +59 -0
- package/.svelte-kit/output/server/chunks/LoadingOverlay.js +12 -0
- package/.svelte-kit/output/server/chunks/LoadingOverlay.svelte_svelte_type_style_lang.js +1671 -0
- package/.svelte-kit/output/server/chunks/connections.js +33 -0
- package/.svelte-kit/output/server/chunks/diagram.js +7 -0
- package/.svelte-kit/output/server/chunks/environment.js +34 -0
- package/.svelte-kit/output/server/chunks/equality.js +57 -0
- package/.svelte-kit/output/server/chunks/exports.js +174 -0
- package/.svelte-kit/output/server/chunks/false.js +4 -0
- package/.svelte-kit/output/server/chunks/index.js +59 -0
- package/.svelte-kit/output/server/chunks/index2.js +2939 -0
- package/.svelte-kit/output/server/chunks/index3.js +20 -0
- package/.svelte-kit/output/server/chunks/internal.js +1017 -0
- package/.svelte-kit/output/server/chunks/shared.js +770 -0
- package/.svelte-kit/output/server/chunks/utils.js +43 -0
- package/.svelte-kit/output/server/entries/pages/_error.svelte.js +64 -0
- package/.svelte-kit/output/server/entries/pages/_layout.svelte.js +65 -0
- package/.svelte-kit/output/server/entries/pages/_page.svelte.js +3991 -0
- package/.svelte-kit/output/server/entries/pages/_page.ts.js +30 -0
- package/.svelte-kit/output/server/entries/pages/group/_groupId_/_page.svelte.js +67 -0
- package/.svelte-kit/output/server/entries/pages/group/_groupId_/_page.ts.js +47 -0
- package/.svelte-kit/output/server/index.js +3747 -0
- package/.svelte-kit/output/server/internal.js +13 -0
- package/.svelte-kit/output/server/manifest-full.js +47 -0
- package/.svelte-kit/output/server/manifest.js +47 -0
- package/.svelte-kit/output/server/nodes/0.js +8 -0
- package/.svelte-kit/output/server/nodes/1.js +8 -0
- package/.svelte-kit/output/server/nodes/2.js +10 -0
- package/.svelte-kit/output/server/nodes/3.js +10 -0
- package/.svelte-kit/output/server/remote-entry.js +557 -0
- package/.svelte-kit/tsconfig.json +61 -0
- package/.svelte-kit/types/route_meta_data.json +8 -0
- package/.svelte-kit/types/src/routes/$types.d.ts +26 -0
- package/.svelte-kit/types/src/routes/group/[groupId]/$types.d.ts +21 -0
- package/.svelte-kit/types/src/routes/group/[groupId]/proxy+page.ts +49 -0
- package/.svelte-kit/types/src/routes/proxy+page.ts +33 -0
- package/build/_app/env.js +1 -0
- package/build/_app/env.js.br +1 -0
- package/build/_app/env.js.gz +0 -0
- package/build/_app/immutable/assets/0.Czt_67iE.css +1 -0
- package/build/_app/immutable/assets/0.Czt_67iE.css.br +0 -0
- package/build/_app/immutable/assets/0.Czt_67iE.css.gz +0 -0
- package/build/_app/immutable/assets/TeamContactCard.Dxj5nUCr.css +1 -0
- package/build/_app/immutable/assets/TeamContactCard.Dxj5nUCr.css.br +0 -0
- package/build/_app/immutable/assets/TeamContactCard.Dxj5nUCr.css.gz +0 -0
- package/build/_app/immutable/assets/helpers.ysDrpaDf.css +1 -0
- package/build/_app/immutable/assets/helpers.ysDrpaDf.css.br +0 -0
- package/build/_app/immutable/assets/helpers.ysDrpaDf.css.gz +0 -0
- package/build/_app/immutable/assets/libavoid.DQJapW5w.wasm +0 -0
- package/build/_app/immutable/assets/libavoid.DQJapW5w.wasm.br +0 -0
- package/build/_app/immutable/assets/libavoid.DQJapW5w.wasm.gz +0 -0
- package/build/_app/immutable/chunks/BlLuv0eP.js +46 -0
- package/build/_app/immutable/chunks/BlLuv0eP.js.br +0 -0
- package/build/_app/immutable/chunks/BlLuv0eP.js.gz +0 -0
- package/build/_app/immutable/chunks/CSBHmwYv.js +1 -0
- package/build/_app/immutable/chunks/CSBHmwYv.js.br +0 -0
- package/build/_app/immutable/chunks/CSBHmwYv.js.gz +0 -0
- package/build/_app/immutable/chunks/CTCi5ueQ.js +1 -0
- package/build/_app/immutable/chunks/CTCi5ueQ.js.br +0 -0
- package/build/_app/immutable/chunks/CTCi5ueQ.js.gz +0 -0
- package/build/_app/immutable/chunks/CfOzjaik.js +2 -0
- package/build/_app/immutable/chunks/CfOzjaik.js.br +0 -0
- package/build/_app/immutable/chunks/CfOzjaik.js.gz +0 -0
- package/build/_app/immutable/chunks/D4PdvFNs.js +1 -0
- package/build/_app/immutable/chunks/D4PdvFNs.js.br +0 -0
- package/build/_app/immutable/chunks/D4PdvFNs.js.gz +0 -0
- package/build/_app/immutable/chunks/DXgP-QUS.js +2 -0
- package/build/_app/immutable/chunks/DXgP-QUS.js.br +0 -0
- package/build/_app/immutable/chunks/DXgP-QUS.js.gz +0 -0
- package/build/_app/immutable/chunks/DlbDC5An.js +1 -0
- package/build/_app/immutable/chunks/DlbDC5An.js.br +0 -0
- package/build/_app/immutable/chunks/DlbDC5An.js.gz +0 -0
- package/build/_app/immutable/chunks/wRWe7aK9.js +1 -0
- package/build/_app/immutable/chunks/wRWe7aK9.js.br +0 -0
- package/build/_app/immutable/chunks/wRWe7aK9.js.gz +0 -0
- package/build/_app/immutable/entry/app.ConrMuHl.js +2 -0
- package/build/_app/immutable/entry/app.ConrMuHl.js.br +0 -0
- package/build/_app/immutable/entry/app.ConrMuHl.js.gz +0 -0
- package/build/_app/immutable/entry/start.Bm6FyGme.js +1 -0
- package/build/_app/immutable/entry/start.Bm6FyGme.js.br +2 -0
- package/build/_app/immutable/entry/start.Bm6FyGme.js.gz +0 -0
- package/build/_app/immutable/nodes/0.d3cL-ETU.js +1 -0
- package/build/_app/immutable/nodes/0.d3cL-ETU.js.br +0 -0
- package/build/_app/immutable/nodes/0.d3cL-ETU.js.gz +0 -0
- package/build/_app/immutable/nodes/1.D6z9rPGv.js +1 -0
- package/build/_app/immutable/nodes/1.D6z9rPGv.js.br +0 -0
- package/build/_app/immutable/nodes/1.D6z9rPGv.js.gz +0 -0
- package/build/_app/immutable/nodes/2.CLD-8chl.js +1 -0
- package/build/_app/immutable/nodes/2.CLD-8chl.js.br +0 -0
- package/build/_app/immutable/nodes/2.CLD-8chl.js.gz +0 -0
- package/build/_app/immutable/nodes/3.DXYeBoel.js +1 -0
- package/build/_app/immutable/nodes/3.DXYeBoel.js.br +0 -0
- package/build/_app/immutable/nodes/3.DXYeBoel.js.gz +0 -0
- package/build/_app/version.json +1 -0
- package/build/_app/version.json.br +0 -0
- package/build/_app/version.json.gz +0 -0
- package/build/index.html +34 -0
- package/build/index.html.br +0 -0
- package/build/index.html.gz +0 -0
- package/build/libavoid.wasm +0 -0
- package/build/libavoid.wasm.br +0 -0
- package/build/libavoid.wasm.gz +0 -0
- package/build/static/robots.txt +3 -0
- package/coverage/coverage-final.json +6 -0
- package/coverage/coverage-summary.json +7 -0
- package/coverage/junit.xml +57 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +131 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/stores/index.html +116 -0
- package/coverage/lcov-report/stores/routingStore.ts.html +781 -0
- package/coverage/lcov-report/utils/flow/helpers.ts.html +127 -0
- package/coverage/lcov-report/utils/flow/index.html +161 -0
- package/coverage/lcov-report/utils/flow/layout.ts.html +805 -0
- package/coverage/lcov-report/utils/flow/serviceIds.ts.html +97 -0
- package/coverage/lcov-report/utils/flow/servicesGraph.ts.html +859 -0
- package/coverage/lcov.info +646 -0
- package/coverage/test-results.json +1 -0
- package/data/services/api/api-gateway.yaml +18 -0
- package/data/services/api/group-info.yaml +7 -0
- package/data/services/api/lambda-orders.yaml +21 -0
- package/data/services/api/lambda-products.yaml +15 -0
- package/data/services/api/lambda-users.yaml +15 -0
- package/data/services/compute/alb.yaml +15 -0
- package/data/services/compute/ecs-inventory.yaml +16 -0
- package/data/services/compute/ecs-notification.yaml +15 -0
- package/data/services/compute/group-info.yaml +7 -0
- package/data/services/data/dynamodb-notifications.yaml +12 -0
- package/data/services/data/dynamodb-orders.yaml +9 -0
- package/data/services/data/dynamodb-products.yaml +9 -0
- package/data/services/data/dynamodb-users.yaml +9 -0
- package/data/services/data/group-info.yaml +7 -0
- package/data/services/data/rds-postgres.yaml +9 -0
- package/data/services/data/redis.yaml +10 -0
- package/data/services/frontend/cloudfront.yaml +12 -0
- package/data/services/frontend/group-info.yaml +7 -0
- package/data/services/frontend/route53.yaml +15 -0
- package/data/services/frontend/s3-website.yaml +9 -0
- package/data/teams/cloud-shepherds.yaml +15 -0
- package/data/teams/data-wizards.yaml +15 -0
- package/data/teams/interface-architects.yaml +19 -0
- package/e2e/demo.test.ts +54 -0
- package/e2e/header-toolbar.simple.spec.ts +0 -0
- package/e2e/header-toolbar.spec.ts +53 -0
- package/e2e/layout.spec.ts +30 -0
- package/package.json +69 -0
- package/playwright.config.ts +10 -0
- package/plugins/mapper-data-plugin.ts +32 -0
- package/project.json +23 -0
- package/src/app.css +125 -0
- package/src/app.d.ts +31 -0
- package/src/app.html +11 -0
- package/src/lib/assets/favicon.svg +19 -0
- package/src/lib/components/EmptyState.svelte +37 -0
- package/src/lib/components/ErrorDisplay.svelte +82 -0
- package/src/lib/components/FlowCanvas.svelte +223 -0
- package/src/lib/components/GenericSidebarCard.svelte +44 -0
- package/src/lib/components/GroupDetailSidebar.svelte +31 -0
- package/src/lib/components/Header.svelte +57 -0
- package/src/lib/components/Legend.svelte +25 -0
- package/src/lib/components/LoadingOverlay.svelte +42 -0
- package/src/lib/components/LoadingSpinner.svelte +10 -0
- package/src/lib/components/ServiceDetailSidebar.svelte +90 -0
- package/src/lib/components/TeamContactCard.svelte +166 -0
- package/src/lib/components/flow/ExternalNode.svelte +45 -0
- package/src/lib/components/flow/MainGroupNode.svelte +24 -0
- package/src/lib/components/flow/ServiceGroupNode.svelte +17 -0
- package/src/lib/components/flow/ServiceNode.svelte +40 -0
- package/src/lib/components/flow/SnakeEdge.svelte +206 -0
- package/src/lib/components/flow/index.ts +6 -0
- package/src/lib/components/index.ts +12 -0
- package/src/lib/data/connections.ts +26 -0
- package/src/lib/data/groups.ts +11 -0
- package/src/lib/data/services.ts +12 -0
- package/src/lib/data/teams.ts +11 -0
- package/src/lib/index.ts +1 -0
- package/src/lib/state/theme.svelte.ts +21 -0
- package/src/lib/stores/diagram.ts +6 -0
- package/src/lib/stores/routingStore.test.ts +133 -0
- package/src/lib/stores/routingStore.ts +232 -0
- package/src/lib/utils/awsIcons.ts +117 -0
- package/src/lib/utils/flow/groupOverviewGraph.ts +73 -0
- package/src/lib/utils/flow/helpers.ts +14 -0
- package/src/lib/utils/flow/layout.test.ts +271 -0
- package/src/lib/utils/flow/layout.ts +240 -0
- package/src/lib/utils/flow/serviceIds.ts +5 -0
- package/src/lib/utils/flow/servicesGraph.test.ts +119 -0
- package/src/lib/utils/flow/servicesGraph.ts +258 -0
- package/src/routes/+error.svelte +36 -0
- package/src/routes/+layout.svelte +27 -0
- package/src/routes/+page.svelte +81 -0
- package/src/routes/+page.ts +31 -0
- package/src/routes/group/[groupId]/+page.svelte +102 -0
- package/src/routes/group/[groupId]/+page.ts +48 -0
- package/src/routes/layout.css +0 -0
- package/static/static/robots.txt +3 -0
- package/svelte.config.js +28 -0
- package/tailwind.config.js +12 -0
- package/tsconfig.json +22 -0
- package/vite.config.ts +81 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import {describe, it, expect} from 'vitest'
|
|
2
|
+
import type {Edge, Node} from '@xyflow/svelte'
|
|
3
|
+
import {layoutFlowGraph, calculateEdgeOffset, OFFSET_STEP} from './layout'
|
|
4
|
+
import type {FlowEdgeData, FlowGraphInput, FlowNodeData} from '$shared/flow-types'
|
|
5
|
+
|
|
6
|
+
// Basic helpers to build nodes/edges with minimal required data
|
|
7
|
+
function serviceNode(id: string, label = id, serviceType?: string, position = {x: 0, y: 0}): Node<FlowNodeData> {
|
|
8
|
+
return {
|
|
9
|
+
id,
|
|
10
|
+
data: {label, kind: 'service', serviceType},
|
|
11
|
+
position,
|
|
12
|
+
type: 'service'
|
|
13
|
+
} as unknown as Node<FlowNodeData>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function groupNode(id: string, label = id, position = {x: 0, y: 0}): Node<FlowNodeData> {
|
|
17
|
+
return {
|
|
18
|
+
id,
|
|
19
|
+
data: {label, kind: 'group'},
|
|
20
|
+
position,
|
|
21
|
+
type: 'serviceGroup'
|
|
22
|
+
} as unknown as Node<FlowNodeData>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function edge(id: string, source: string, target: string, data: FlowEdgeData = {}): Edge<FlowEdgeData> {
|
|
26
|
+
return {
|
|
27
|
+
id,
|
|
28
|
+
source,
|
|
29
|
+
target,
|
|
30
|
+
data
|
|
31
|
+
} as Edge<FlowEdgeData>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Silence verbose debug logs during tests
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
36
|
+
const noop = () => {
|
|
37
|
+
}
|
|
38
|
+
// @ts-expect-error allow override in test env
|
|
39
|
+
console.debug = noop
|
|
40
|
+
// @ts-expect-error allow override in test env
|
|
41
|
+
console.log = noop
|
|
42
|
+
|
|
43
|
+
describe('layoutFlowGraph', () => {
|
|
44
|
+
it('returns positions for nodes and preserves signature', async () => {
|
|
45
|
+
const input: FlowGraphInput = {
|
|
46
|
+
groupNodes: [],
|
|
47
|
+
serviceNodes: [serviceNode('s1', 'Service 1')],
|
|
48
|
+
edges: [],
|
|
49
|
+
signature: 'sig-1'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const out = await layoutFlowGraph(input)
|
|
53
|
+
|
|
54
|
+
expect(out.signature).toBe(input.signature)
|
|
55
|
+
expect(out.nodes.length).toBeGreaterThanOrEqual(1)
|
|
56
|
+
|
|
57
|
+
const n = out.nodes.find((x) => x.id === 's1')
|
|
58
|
+
expect(n).toBeTruthy()
|
|
59
|
+
expect(n?.position?.x).toBeDefined()
|
|
60
|
+
expect(n?.position?.y).toBeDefined()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('keeps nodes and edges ids and data', async () => {
|
|
64
|
+
const input: FlowGraphInput = {
|
|
65
|
+
groupNodes: [],
|
|
66
|
+
serviceNodes: [serviceNode('a', 'Service A', 'lambda'), serviceNode('b', 'Service B', 'sqs')],
|
|
67
|
+
edges: [edge('e1', 'a', 'b', {label: 'A->B', direction: 'outgoing'})],
|
|
68
|
+
signature: 'sig-2'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const out = await layoutFlowGraph(input)
|
|
72
|
+
|
|
73
|
+
// Nodes should be present
|
|
74
|
+
const nodeA = out.nodes.find((n) => n.id === 'a')
|
|
75
|
+
const nodeB = out.nodes.find((n) => n.id === 'b')
|
|
76
|
+
expect(nodeA).toBeTruthy()
|
|
77
|
+
expect(nodeB).toBeTruthy()
|
|
78
|
+
|
|
79
|
+
// Verify component-specific data is preserved
|
|
80
|
+
expect(nodeA?.data.label).toBe('Service A')
|
|
81
|
+
expect(nodeA?.data.serviceType).toBe('lambda')
|
|
82
|
+
expect(nodeB?.data.serviceType).toBe('sqs')
|
|
83
|
+
|
|
84
|
+
// Edge should be preserved with same ids and data
|
|
85
|
+
expect(out.edges.length).toBe(1)
|
|
86
|
+
const e = out.edges[0]
|
|
87
|
+
expect(e.id).toBe('e1')
|
|
88
|
+
expect(e.source).toBe('a')
|
|
89
|
+
expect(e.target).toBe('b')
|
|
90
|
+
expect(e.data?.label).toBe('A->B')
|
|
91
|
+
expect(e.data?.direction).toBe('outgoing')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('handles a simple group with a child service node', async () => {
|
|
95
|
+
const g = groupNode('g1', 'Group 1')
|
|
96
|
+
const s = {...serviceNode('s1', 'Service in group'), parentId: 'g1'} as unknown as Node<FlowNodeData>
|
|
97
|
+
|
|
98
|
+
const input: FlowGraphInput = {
|
|
99
|
+
groupNodes: [g],
|
|
100
|
+
serviceNodes: [s],
|
|
101
|
+
edges: [],
|
|
102
|
+
signature: 'sig-3'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const out = await layoutFlowGraph(input)
|
|
106
|
+
|
|
107
|
+
// Expect both the group and the service node to be present in flattened output
|
|
108
|
+
const groupOut = out.nodes.find((n) => n.id === 'g1')
|
|
109
|
+
const childOut = out.nodes.find((n) => n.id === 's1')
|
|
110
|
+
|
|
111
|
+
expect(groupOut).toBeTruthy()
|
|
112
|
+
expect(childOut).toBeTruthy()
|
|
113
|
+
|
|
114
|
+
// Both should have positions
|
|
115
|
+
expect(groupOut?.position?.x).toBeDefined()
|
|
116
|
+
expect(groupOut?.position?.y).toBeDefined()
|
|
117
|
+
expect(childOut?.position?.x).toBeDefined()
|
|
118
|
+
expect(childOut?.position?.y).toBeDefined()
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('calculateEdgeOffset', () => {
|
|
123
|
+
it('returns 0 for a single edge', () => {
|
|
124
|
+
const nodes: Node<FlowNodeData>[] = [serviceNode('s1'), serviceNode('t1')]
|
|
125
|
+
const edges: Edge<FlowEdgeData>[] = [
|
|
126
|
+
{id: 'e1', source: 's1', target: 't1', sourceHandle: 'output', targetHandle: 'input'} as Edge<FlowEdgeData>
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
expect(calculateEdgeOffset('e1', nodes, edges, true)).toBe(0)
|
|
130
|
+
expect(calculateEdgeOffset('e1', nodes, edges, false)).toBe(0)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('spreads multiple edges and sorts them by target position', () => {
|
|
134
|
+
const nodes: Node<FlowNodeData>[] = [
|
|
135
|
+
serviceNode('s1', 'Source', undefined, {x: 0, y: 100}),
|
|
136
|
+
serviceNode('t1', 'Top Target', undefined, {x: 200, y: 0}),
|
|
137
|
+
serviceNode('t2', 'Bottom Target', undefined, {x: 200, y: 200})
|
|
138
|
+
]
|
|
139
|
+
const edges: Edge<FlowEdgeData>[] = [
|
|
140
|
+
{id: 'e_to_t1', source: 's1', target: 't1', sourceHandle: 'output'} as Edge<FlowEdgeData>,
|
|
141
|
+
{id: 'e_to_t2', source: 's1', target: 't2', sourceHandle: 'output'} as Edge<FlowEdgeData>
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
// e_to_t1 connects to t1 (y=0)
|
|
145
|
+
// e_to_t2 connects to t2 (y=200)
|
|
146
|
+
// Sorted order: e_to_t1, e_to_t2
|
|
147
|
+
// index for e_to_t1 = 0. index for e_to_t2 = 1.
|
|
148
|
+
// offsets = (index - 0.5) * OFFSET_STEP
|
|
149
|
+
|
|
150
|
+
expect(calculateEdgeOffset('e_to_t1', nodes, edges, true)).toBe(-OFFSET_STEP / 2)
|
|
151
|
+
expect(calculateEdgeOffset('e_to_t2', nodes, edges, true)).toBe(OFFSET_STEP / 2)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('spreads three edges correctly', () => {
|
|
155
|
+
const nodes: Node<FlowNodeData>[] = [
|
|
156
|
+
serviceNode('s1', 'Source', undefined, {x: 0, y: 100}),
|
|
157
|
+
serviceNode('t1', 'T1', undefined, {x: 200, y: 0}),
|
|
158
|
+
serviceNode('t2', 'T2', undefined, {x: 200, y: 100}),
|
|
159
|
+
serviceNode('t3', 'T3', undefined, {x: 200, y: 200})
|
|
160
|
+
]
|
|
161
|
+
const edges: Edge<FlowEdgeData>[] = [
|
|
162
|
+
{id: 'e1', source: 's1', target: 't1', sourceHandle: 'output'} as Edge<FlowEdgeData>,
|
|
163
|
+
{id: 'e2', source: 's1', target: 't2', sourceHandle: 'output'} as Edge<FlowEdgeData>,
|
|
164
|
+
{id: 'e3', source: 's1', target: 't3', sourceHandle: 'output'} as Edge<FlowEdgeData>
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
// Sorted order: e1 (y=0), e2 (y=100), e3 (y=200)
|
|
168
|
+
// index: 0, 1, 2. (n-1)/2 = 1.
|
|
169
|
+
// offsets: (0-1)*OFFSET_STEP, (1-1)*OFFSET_STEP, (2-1)*OFFSET_STEP
|
|
170
|
+
|
|
171
|
+
expect(calculateEdgeOffset('e1', nodes, edges, true)).toBe(-OFFSET_STEP)
|
|
172
|
+
expect(calculateEdgeOffset('e2', nodes, edges, true)).toBe(0)
|
|
173
|
+
expect(calculateEdgeOffset('e3', nodes, edges, true)).toBe(OFFSET_STEP)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('correctly calculates offsets for nodes in groups vs orphans using absolute coordinates', () => {
|
|
177
|
+
// Node 1 (orphan) at x=10, y=60
|
|
178
|
+
// Group 1 at x=422, y=67
|
|
179
|
+
// Node 2 (child of Group 1) at x=10, y=160 (relative) -> Abs: y = 67 + 160 = 227
|
|
180
|
+
// Node 3 (child of Group 1) at x=350, y=260 (relative) -> Abs: y = 67 + 260 = 327
|
|
181
|
+
// Edges: Node 1 -> Node 3 (e1), Node 2 -> Node 3 (e2)
|
|
182
|
+
// Both connect to Node 3's input handle (target).
|
|
183
|
+
const nodes: Node<FlowNodeData>[] = [
|
|
184
|
+
serviceNode('n1', 'Orphan', undefined, {x: 10, y: 60}),
|
|
185
|
+
groupNode('g1', 'Group 1', {x: 422, y: 67}),
|
|
186
|
+
{...serviceNode('n2', 'Child 2', undefined, {x: 10, y: 160}), parentId: 'g1'} as Node<FlowNodeData>,
|
|
187
|
+
{...serviceNode('n3', 'Target 3', undefined, {x: 350, y: 260}), parentId: 'g1'} as Node<FlowNodeData>
|
|
188
|
+
]
|
|
189
|
+
const edges: Edge<FlowEdgeData>[] = [
|
|
190
|
+
{id: 'e1', source: 'n1', target: 'n3', targetHandle: 'input'} as Edge<FlowEdgeData>,
|
|
191
|
+
{id: 'e2', source: 'n2', target: 'n3', targetHandle: 'input'} as Edge<FlowEdgeData>
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
// Absolute positions:
|
|
195
|
+
// n1 (abs y=60)
|
|
196
|
+
// n2 (abs y=67+160=227)
|
|
197
|
+
// Both are sources of Node 3.
|
|
198
|
+
// Order should be e1 (60), e2 (227).
|
|
199
|
+
// Offset for e1: -4, Offset for e2: 4.
|
|
200
|
+
// This confirms n1's edge is "above" n2's edge (smaller Y offset).
|
|
201
|
+
// The user said: "verify if the connection coming from the second node is above of the connection coming from the first node."
|
|
202
|
+
// Maybe the user meant if it WAS above? No, I'll just check if e1 is above e2.
|
|
203
|
+
|
|
204
|
+
const offset1 = calculateEdgeOffset('e1', nodes, edges, false) // target offset for e1
|
|
205
|
+
const offset2 = calculateEdgeOffset('e2', nodes, edges, false) // target offset for e2
|
|
206
|
+
|
|
207
|
+
// Expected: offset1 < offset2 because n1 (abs 60) < n2 (abs 227)
|
|
208
|
+
expect(offset1).toBe(-OFFSET_STEP / 2)
|
|
209
|
+
expect(offset2).toBe(OFFSET_STEP / 2)
|
|
210
|
+
|
|
211
|
+
// Now if the parent was at y=-200
|
|
212
|
+
// n2 (abs y=-200+160=-40)
|
|
213
|
+
// n1 (abs y=60)
|
|
214
|
+
// Now n2 is HIGHER than n1.
|
|
215
|
+
// Order should be e2 (-40), e1 (60).
|
|
216
|
+
// Offset for e2: -4, Offset for e1: 4.
|
|
217
|
+
const nodes2: Node<FlowNodeData>[] = [
|
|
218
|
+
serviceNode('n1', 'Orphan', undefined, {x: 10, y: 60}),
|
|
219
|
+
groupNode('g1', 'Group 1', {x: 422, y: -200}),
|
|
220
|
+
{...serviceNode('n2', 'Child 2', undefined, {x: 10, y: 160}), parentId: 'g1'} as Node<FlowNodeData>,
|
|
221
|
+
{...serviceNode('n3', 'Target 3', undefined, {x: 350, y: 260}), parentId: 'g1'} as Node<FlowNodeData>
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
const offset1_neg = calculateEdgeOffset('e1', nodes2, edges, false)
|
|
225
|
+
const offset2_neg = calculateEdgeOffset('e2', nodes2, edges, false)
|
|
226
|
+
|
|
227
|
+
// Expected: offset2_neg < offset1_neg because n2 (abs -40) < n1 (abs 60)
|
|
228
|
+
expect(offset2_neg).toBe(-OFFSET_STEP / 2)
|
|
229
|
+
expect(offset1_neg).toBe(OFFSET_STEP / 2)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('correctly calculates absolute coordinates for deeply nested nodes', () => {
|
|
233
|
+
const nodes: Node<FlowNodeData>[] = [
|
|
234
|
+
groupNode('g1', 'G1', {x: 100, y: 100}),
|
|
235
|
+
{...groupNode('g2', 'G2', {x: 50, y: 50}), parentId: 'g1'} as Node<FlowNodeData>,
|
|
236
|
+
{...serviceNode('n1', 'N1', undefined, {x: 10, y: 10}), parentId: 'g2'} as Node<FlowNodeData>
|
|
237
|
+
]
|
|
238
|
+
// n1 abs position should be: g1(100,100) + g2(50,50) + n1(10,10) = (160, 160)
|
|
239
|
+
|
|
240
|
+
// We'll create another node n2 and an edge n1 -> n2.
|
|
241
|
+
const nodesFull = [
|
|
242
|
+
...nodes,
|
|
243
|
+
serviceNode('n2', 'N2', undefined, {x: 500, y: 500})
|
|
244
|
+
]
|
|
245
|
+
const edges = [
|
|
246
|
+
{id: 'e1', source: 'n1', target: 'n2', sourceHandle: 'output', targetHandle: 'input'} as Edge<FlowEdgeData>,
|
|
247
|
+
{id: 'e2', source: 'n3', target: 'n4', sourceHandle: 'output', targetHandle: 'input'} as Edge<FlowEdgeData>
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
// If n1 is at (160, 160) and n2 is at (500, 500)
|
|
251
|
+
const n3 = serviceNode('n3', 'N3', undefined, {x: 160, y: 170})
|
|
252
|
+
const n4 = serviceNode('n4', 'N4', undefined, {x: 500, y: 510})
|
|
253
|
+
const nodesExtra = [...nodesFull, n3, n4]
|
|
254
|
+
|
|
255
|
+
// e1 should be compared with other edges if they share handle or nodes
|
|
256
|
+
// Let's use same handle for easier testing of ordering
|
|
257
|
+
const edgesShared = [
|
|
258
|
+
{id: 'e1', source: 'n1', target: 'n2', sourceHandle: 'output', targetHandle: 'input'} as Edge<FlowEdgeData>,
|
|
259
|
+
{id: 'e2', source: 'n3', target: 'n2', sourceHandle: 'output', targetHandle: 'input'} as Edge<FlowEdgeData>
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
// n1 abs y=160. n3 abs y=170.
|
|
263
|
+
// e1 should get -4, e2 should get 4.
|
|
264
|
+
|
|
265
|
+
const o1 = calculateEdgeOffset('e1', nodesExtra, edgesShared, false)
|
|
266
|
+
const o2 = calculateEdgeOffset('e2', nodesExtra, edgesShared, false)
|
|
267
|
+
|
|
268
|
+
expect(o1).toBe(-OFFSET_STEP / 2)
|
|
269
|
+
expect(o2).toBe(OFFSET_STEP / 2)
|
|
270
|
+
})
|
|
271
|
+
})
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import ELK from 'elkjs/lib/elk.bundled.js';
|
|
2
|
+
import {type Edge, type Node} from '@xyflow/svelte';
|
|
3
|
+
import type {FlowEdgeData, FlowGraphInput, FlowGraphOutput, FlowNodeData} from '$shared/flow-types';
|
|
4
|
+
import type {ElkNode} from "elkjs/lib/elk-api";
|
|
5
|
+
|
|
6
|
+
const elk = new ELK();
|
|
7
|
+
const PADDING = 10;
|
|
8
|
+
export const OFFSET_STEP = 20;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Helper to calculate absolute position of a node by traversing its parent chain.
|
|
12
|
+
*/
|
|
13
|
+
export function getAbsolutePosition(nodeId: string, nodes: Node<FlowNodeData>[]): { x: number, y: number } {
|
|
14
|
+
const node = nodes.find(n => n.id === nodeId);
|
|
15
|
+
if (!node) return {x: 0, y: 0};
|
|
16
|
+
|
|
17
|
+
let x = node.position?.x ?? 0;
|
|
18
|
+
let y = node.position?.y ?? 0;
|
|
19
|
+
|
|
20
|
+
if (node.parentId) {
|
|
21
|
+
const parentPos = getAbsolutePosition(node.parentId, nodes);
|
|
22
|
+
x += parentPos.x;
|
|
23
|
+
y += parentPos.y;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {x, y};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Calculates a deterministic offset for an edge to prevent overlapping.
|
|
31
|
+
* TODO: Replace this method with elk's built-in edge routing and spacing options once we can guarantee it works well in all cases.
|
|
32
|
+
* This probably means that we'll have to replace it once libavoid is integrated into elk: https://github.com/kieler/elkjs/issues/210
|
|
33
|
+
* This function handles two types of offsets:
|
|
34
|
+
* 1. Handle Offset: For multiple edges sharing the same handle on a node.
|
|
35
|
+
* 2. Trunk Offset: For edges that don't share a handle but might have overlapping
|
|
36
|
+
* paths (trunks) in the same "lane".
|
|
37
|
+
*/
|
|
38
|
+
export function calculateEdgeOffset(
|
|
39
|
+
edgeId: string,
|
|
40
|
+
nodes: Node<FlowNodeData>[],
|
|
41
|
+
edges: Edge<FlowEdgeData>[],
|
|
42
|
+
isSource: boolean
|
|
43
|
+
): number {
|
|
44
|
+
const edge = edges.find(e => e.id === edgeId);
|
|
45
|
+
if (!edge) return 0;
|
|
46
|
+
|
|
47
|
+
const nodeId = isSource ? edge.source : edge.target;
|
|
48
|
+
const handleId = isSource ? edge.sourceHandle : edge.targetHandle;
|
|
49
|
+
|
|
50
|
+
const handle = isSource ? edge.sourceHandle : edge.targetHandle;
|
|
51
|
+
const isVertical = handle?.toLowerCase().includes('top') || handle?.toLowerCase().includes('bottom');
|
|
52
|
+
|
|
53
|
+
let finalSiblings: Edge<FlowEdgeData>[] = [];
|
|
54
|
+
let usedAreaFallback = false;
|
|
55
|
+
|
|
56
|
+
// HANDLE OFFSET: Siblings on the same node and same handle
|
|
57
|
+
const siblings = edges.filter(e =>
|
|
58
|
+
(isSource ? e.source : e.target) === nodeId &&
|
|
59
|
+
(isSource ? e.sourceHandle : e.targetHandle) === handleId
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (siblings.length <= 1) {
|
|
63
|
+
// Fallback for bidirectional or single edges between same nodes
|
|
64
|
+
usedAreaFallback = true;
|
|
65
|
+
const otherId = isSource ? edge.target : edge.source;
|
|
66
|
+
finalSiblings = edges.filter(e => {
|
|
67
|
+
const isSamePair = (e.source === nodeId && e.target === otherId) ||
|
|
68
|
+
(e.source === otherId && e.target === nodeId);
|
|
69
|
+
return isSamePair && (e.source === nodeId || e.target === nodeId);
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
finalSiblings = siblings;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (finalSiblings.length <= 1) return 0;
|
|
76
|
+
|
|
77
|
+
// Sort:
|
|
78
|
+
finalSiblings.sort((a, b) => {
|
|
79
|
+
if (usedAreaFallback) {
|
|
80
|
+
// For horizontal paths (Left/Right), we split them vertically by their overall Y midpoint
|
|
81
|
+
const aSPos = getAbsolutePosition(a.source, nodes);
|
|
82
|
+
const aTPos = getAbsolutePosition(a.target, nodes);
|
|
83
|
+
const bSPos = getAbsolutePosition(b.source, nodes);
|
|
84
|
+
const bTPos = getAbsolutePosition(b.target, nodes);
|
|
85
|
+
|
|
86
|
+
if (isVertical) {
|
|
87
|
+
const midA = (aSPos.x + aTPos.x) / 2;
|
|
88
|
+
const midB = (bSPos.x + bTPos.x) / 2;
|
|
89
|
+
if (midA !== midB) return midA - midB;
|
|
90
|
+
} else {
|
|
91
|
+
const midA = (aSPos.y + aTPos.y) / 2;
|
|
92
|
+
const midB = (bSPos.y + bTPos.y) / 2;
|
|
93
|
+
if (midA !== midB) return midA - midB;
|
|
94
|
+
}
|
|
95
|
+
return a.id.localeCompare(b.id);
|
|
96
|
+
}
|
|
97
|
+
const otherIdA = isSource ? a.target : a.source;
|
|
98
|
+
const otherIdB = isSource ? b.target : b.source;
|
|
99
|
+
const posA = getAbsolutePosition(otherIdA, nodes);
|
|
100
|
+
const posB = getAbsolutePosition(otherIdB, nodes);
|
|
101
|
+
const primary = isVertical ? (posA.x - posB.x) : (posA.y - posB.y);
|
|
102
|
+
if (primary !== 0) return primary;
|
|
103
|
+
return a.id.localeCompare(b.id);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const index = finalSiblings.findIndex(e => e.id === edgeId);
|
|
107
|
+
return (index - (finalSiblings.length - 1) / 2) * OFFSET_STEP;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const ELK_OPTIONS: Record<string, string> = {
|
|
111
|
+
'elk.algorithm': 'layered',
|
|
112
|
+
'elk.direction': 'RIGHT',
|
|
113
|
+
//Horizontal
|
|
114
|
+
'elk.spacing.nodeNodeBetweenLayers': '80',
|
|
115
|
+
//Vertical
|
|
116
|
+
'elk.spacing.nodeNode': '80',
|
|
117
|
+
|
|
118
|
+
'elk.spacing.edgeNode': '60', // Gap between lines and boxes
|
|
119
|
+
'elk.spacing.edgeEdge': '40', // Gap between parallel lines
|
|
120
|
+
|
|
121
|
+
'elk.layered.spacing.edgeNodeBetweenLayers': '60',
|
|
122
|
+
'elk.edgeRouting': 'ORTHOGONAL',
|
|
123
|
+
'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
|
|
124
|
+
'elk.portConstraints': 'FREE',
|
|
125
|
+
'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP',
|
|
126
|
+
'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX',
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
function convertEdgesToElkEdges(input: FlowGraphInput) {
|
|
130
|
+
return input.edges.map((edge) => ({
|
|
131
|
+
id: edge.id,
|
|
132
|
+
sources: [edge.source],
|
|
133
|
+
targets: [edge.target],
|
|
134
|
+
originalEdge: edge,
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Helper to get the dimensions of a node, prioritizing measured values from Svelte Flow.
|
|
140
|
+
*/
|
|
141
|
+
export function getNodeDimensions(node: Node<FlowNodeData>): { w: number, h: number } {
|
|
142
|
+
const w = Math.round(node.measured?.width ?? node.width ?? 150);
|
|
143
|
+
const h = Math.round(node.measured?.height ?? node.height ?? 40);
|
|
144
|
+
return {w, h};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
export async function layoutFlowGraph(input: FlowGraphInput): Promise<FlowGraphOutput> {
|
|
149
|
+
const elkEdges = convertEdgesToElkEdges(input);
|
|
150
|
+
// console.debug('[layoutFlowGraph] Starting layout with input:', {input});
|
|
151
|
+
// Helper: Build node properties and handle dimensions
|
|
152
|
+
const prepareElkNode = (node: Node<FlowNodeData>) => {
|
|
153
|
+
const {w, h} = getNodeDimensions(node);
|
|
154
|
+
return {
|
|
155
|
+
...node,
|
|
156
|
+
width: w,
|
|
157
|
+
height: h
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// 1. Build the nested ELK structure
|
|
162
|
+
const elkChildren: ElkNode[] = input.groupNodes.map(parent => ({
|
|
163
|
+
...prepareElkNode(parent),
|
|
164
|
+
layoutOptions: {
|
|
165
|
+
...ELK_OPTIONS,
|
|
166
|
+
'elk.padding': `[top=60,left=${PADDING},bottom=${PADDING},right=${PADDING}]`,
|
|
167
|
+
},
|
|
168
|
+
children: input.serviceNodes.filter(child => child.parentId === parent.id).map(prepareElkNode)
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
// Add nodes that aren't in any group
|
|
172
|
+
input.serviceNodes
|
|
173
|
+
.filter(child => child.parentId === undefined)
|
|
174
|
+
.forEach(node => elkChildren.push(prepareElkNode(node)));
|
|
175
|
+
|
|
176
|
+
const elkGraph = {
|
|
177
|
+
id: 'root',
|
|
178
|
+
layoutOptions: ELK_OPTIONS,
|
|
179
|
+
children: elkChildren,
|
|
180
|
+
edges: elkEdges
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return elk.layout(elkGraph).then(async (layoutedGraph) => {
|
|
184
|
+
console.debug('[layoutFlowGraph] Layout completed:', {layoutedGraph});
|
|
185
|
+
const flattenedNodes: Node<FlowNodeData>[] = [];
|
|
186
|
+
const flattenedEdges: Edge<FlowEdgeData>[] = [];
|
|
187
|
+
|
|
188
|
+
// Lookup maps to handle the coordinate system bridge
|
|
189
|
+
const parentPosLookup = new Map<string, { x: number, y: number }>();
|
|
190
|
+
const nodeToParent = new Map<string, string>();
|
|
191
|
+
const nodeLookup = new Map<String, Node<FlowNodeData>>();
|
|
192
|
+
|
|
193
|
+
// 2. Process the results
|
|
194
|
+
layoutedGraph.children?.forEach(parentOrOrphan => {
|
|
195
|
+
const px = parentOrOrphan.x || 0;
|
|
196
|
+
const py = parentOrOrphan.y || 0;
|
|
197
|
+
|
|
198
|
+
// If it has children, it's a group node in our context
|
|
199
|
+
if (parentOrOrphan.children) {
|
|
200
|
+
parentPosLookup.set(parentOrOrphan.id, {x: px, y: py});
|
|
201
|
+
|
|
202
|
+
flattenedNodes.push({
|
|
203
|
+
...parentOrOrphan,
|
|
204
|
+
position: {x: px, y: py},
|
|
205
|
+
draggable: true,
|
|
206
|
+
} as Node<FlowNodeData>);
|
|
207
|
+
|
|
208
|
+
// Map children and record their parent for edge offsetting
|
|
209
|
+
parentOrOrphan.children.forEach(child => {
|
|
210
|
+
nodeToParent.set(child.id, parentOrOrphan.id);
|
|
211
|
+
const node = {
|
|
212
|
+
...child,
|
|
213
|
+
position: {x: child.x || 0, y: child.y || 0},
|
|
214
|
+
draggable: true,
|
|
215
|
+
extent: [[PADDING, 50], [(parentOrOrphan.width || 0) - PADDING, (parentOrOrphan.height || 0) - PADDING]]
|
|
216
|
+
} as Node<FlowNodeData>;
|
|
217
|
+
flattenedNodes.push(node);
|
|
218
|
+
nodeLookup.set(child.id, node);
|
|
219
|
+
});
|
|
220
|
+
} else {
|
|
221
|
+
// It's a top-level orphan node
|
|
222
|
+
const node = {
|
|
223
|
+
...parentOrOrphan,
|
|
224
|
+
position: {x: px, y: py},
|
|
225
|
+
draggable: true,
|
|
226
|
+
} as Node<FlowNodeData>;
|
|
227
|
+
flattenedNodes.push(node);
|
|
228
|
+
nodeLookup.set(node.id, node);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const edges = input.edges.filter(e => e.data.connectionType !== 'service-group');
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
nodes: flattenedNodes,
|
|
236
|
+
edges,
|
|
237
|
+
signature: input.signature
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { buildGroupServicesGraph } from './servicesGraph'
|
|
3
|
+
import { ConnectionType, ServiceType, type GroupInfo, type ServiceDefinition } from '$shared/types'
|
|
4
|
+
|
|
5
|
+
describe('servicesGraph', () => {
|
|
6
|
+
const currentGroup: GroupInfo = {
|
|
7
|
+
id: 'g1',
|
|
8
|
+
name: 'Group 1',
|
|
9
|
+
groupName: 'g1'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const allGroups = new Map<string, GroupInfo>([
|
|
13
|
+
['g1', currentGroup]
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
it('correctly loads bidirectional connections between two services in the same group', () => {
|
|
17
|
+
const serviceA: ServiceDefinition = {
|
|
18
|
+
friendlyName: 'Service A',
|
|
19
|
+
identifier: 'svc-a',
|
|
20
|
+
serviceType: ServiceType.LAMBDA,
|
|
21
|
+
groupId: 'g1',
|
|
22
|
+
outgoingConnections: [
|
|
23
|
+
{
|
|
24
|
+
connectionType: ConnectionType.TCP,
|
|
25
|
+
targetIdentifier: { groupId: 'g1', serviceId: 'svc-b' },
|
|
26
|
+
description: 'A to B'
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const serviceB: ServiceDefinition = {
|
|
32
|
+
friendlyName: 'Service B',
|
|
33
|
+
identifier: 'svc-b',
|
|
34
|
+
serviceType: ServiceType.DYNAMODB,
|
|
35
|
+
groupId: 'g1',
|
|
36
|
+
outgoingConnections: [
|
|
37
|
+
{
|
|
38
|
+
connectionType: ConnectionType.TCP,
|
|
39
|
+
targetIdentifier: { groupId: 'g1', serviceId: 'svc-a' },
|
|
40
|
+
description: 'B to A'
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const currentServices = [serviceA, serviceB]
|
|
46
|
+
const result = buildGroupServicesGraph(currentGroup, currentServices, allGroups, [])
|
|
47
|
+
|
|
48
|
+
// We expect two nodes and two edges
|
|
49
|
+
expect(result.graph.serviceNodes.length).toBe(2)
|
|
50
|
+
|
|
51
|
+
// Check edges
|
|
52
|
+
expect(result.graph.edges.length).toBe(2)
|
|
53
|
+
|
|
54
|
+
const edgeAB = result.graph.edges.find(e => e.source === 'svc::g1::svc-a' && e.target === 'svc::g1::svc-b')
|
|
55
|
+
const edgeBA = result.graph.edges.find(e => e.source === 'svc::g1::svc-b' && e.target === 'svc::g1::svc-a')
|
|
56
|
+
|
|
57
|
+
expect(edgeAB).toBeDefined()
|
|
58
|
+
expect(edgeBA).toBeDefined()
|
|
59
|
+
|
|
60
|
+
// Verify IDs are unique
|
|
61
|
+
expect(edgeAB?.id).not.toBe(edgeBA?.id)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('correctly loads bidirectional connections involving an external service', () => {
|
|
65
|
+
const serviceA: ServiceDefinition = {
|
|
66
|
+
friendlyName: 'Service A',
|
|
67
|
+
identifier: 'svc-a',
|
|
68
|
+
serviceType: ServiceType.LAMBDA,
|
|
69
|
+
groupId: 'g1',
|
|
70
|
+
outgoingConnections: [
|
|
71
|
+
{
|
|
72
|
+
connectionType: ConnectionType.TCP,
|
|
73
|
+
targetIdentifier: { groupId: 'g2', serviceId: 'svc-ext' },
|
|
74
|
+
description: 'A to External'
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
incomingConnections: [
|
|
78
|
+
{
|
|
79
|
+
connectionType: ConnectionType.TCP,
|
|
80
|
+
sourceIdentifier: { groupId: 'g2', serviceId: 'svc-ext' },
|
|
81
|
+
description: 'External to A'
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const group2: GroupInfo = { id: 'g2', name: 'Group 2', groupName: 'g2' }
|
|
87
|
+
const serviceExt: ServiceDefinition = {
|
|
88
|
+
friendlyName: 'External Service',
|
|
89
|
+
identifier: 'svc-ext',
|
|
90
|
+
serviceType: ServiceType.RDS,
|
|
91
|
+
groupId: 'g2'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const externalGroups = [
|
|
95
|
+
{
|
|
96
|
+
group: group2,
|
|
97
|
+
services: [serviceExt],
|
|
98
|
+
direction: 'outgoing' as const
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
group: group2,
|
|
102
|
+
services: [serviceExt],
|
|
103
|
+
direction: 'incoming' as const
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
const result = buildGroupServicesGraph(currentGroup, [serviceA], allGroups, [], externalGroups)
|
|
108
|
+
|
|
109
|
+
// Should have serviceA and ONE external node (shared between directions)
|
|
110
|
+
expect(result.graph.serviceNodes.length).toBe(2)
|
|
111
|
+
expect(result.graph.edges.length).toBe(2)
|
|
112
|
+
|
|
113
|
+
const edgeToExt = result.graph.edges.find(e => e.source === 'svc::g1::svc-a' && e.target === 'svc::g2::svc-ext')
|
|
114
|
+
const edgeFromExt = result.graph.edges.find(e => e.target === 'svc::g1::svc-a' && e.source === 'svc::g2::svc-ext')
|
|
115
|
+
|
|
116
|
+
expect(edgeToExt).toBeDefined()
|
|
117
|
+
expect(edgeFromExt).toBeDefined()
|
|
118
|
+
})
|
|
119
|
+
})
|