@eventcatalog/core 3.48.4 → 4.0.0-beta.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/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/catalog-to-astro-content-directory.cjs +1 -1
- package/dist/chunk-BWN544LB.js +73 -0
- package/dist/{chunk-XNVSSL4W.js → chunk-JTZT2SDF.js} +1 -1
- package/dist/{chunk-JY4H7A4S.js → chunk-M2ZRT7KQ.js} +1 -1
- package/dist/{chunk-SY4XL6OQ.js → chunk-MBVTS76Q.js} +1 -1
- package/dist/{chunk-KDLSBQRP.js → chunk-TZTPGQGL.js} +1 -1
- package/dist/{chunk-FNDSHOV2.js → chunk-U5OTNDVQ.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/core-node-modules.cjs +108 -0
- package/dist/core-node-modules.d.cts +7 -0
- package/dist/core-node-modules.d.ts +7 -0
- package/dist/core-node-modules.js +8 -0
- package/dist/docs/development/components/components/15-prompt.md +5 -1
- package/dist/eventcatalog.cjs +124 -48
- package/dist/eventcatalog.js +26 -15
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/astro.config.mjs +27 -23
- package/eventcatalog/public/icons/languages/java.svg +1 -0
- package/eventcatalog/public/icons/payments/stripe.svg +1 -0
- package/eventcatalog/src/components/Badge.astro +10 -10
- package/eventcatalog/src/components/CopyAsMarkdown.tsx +154 -0
- package/eventcatalog/src/components/Grids/DomainGrid.tsx +207 -44
- package/eventcatalog/src/components/Grids/MessageGrid.tsx +0 -20
- package/eventcatalog/src/components/Grids/SystemGrid.tsx +185 -0
- package/eventcatalog/src/components/MDX/ADRTable/ADRTable.astro +99 -0
- package/eventcatalog/src/components/MDX/ADRTable/ADRTable.client.tsx +400 -0
- package/eventcatalog/src/components/MDX/ContextDiagram/ContextDiagramPortal.tsx +21 -0
- package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +1 -0
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +42 -6
- package/eventcatalog/src/components/MDX/Prompt/Prompt.astro +62 -9
- package/eventcatalog/src/components/MDX/ResourceRef/ResourceRef.astro +8 -1
- package/eventcatalog/src/components/MDX/SystemContextMap/SystemContextMapPortal.tsx +22 -0
- package/eventcatalog/src/components/MDX/components.tsx +7 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +1 -1
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +67 -37
- package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +47 -0
- package/eventcatalog/src/components/Tables/Discover/FilterComponents.tsx +10 -2
- package/eventcatalog/src/components/Tables/Discover/columns.tsx +115 -27
- package/eventcatalog/src/components/Tables/Discover/index.ts +7 -1
- package/eventcatalog/src/components/Tables/SystemResources/SystemResourcesTable.tsx +267 -0
- package/eventcatalog/src/content.config.ts +96 -8
- package/eventcatalog/src/enterprise/collections/resource-docs-utils.ts +4 -1
- package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +12 -13
- package/eventcatalog/src/enterprise/custom-documentation/utils/custom-docs.ts +6 -0
- package/eventcatalog/src/enterprise/feature.ts +3 -1
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +96 -140
- package/eventcatalog/src/layouts/VisualiserLayout.astro +66 -0
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +4 -2
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +2 -0
- package/eventcatalog/src/pages/diagrams/[id]/[version]/index.astro +15 -15
- package/eventcatalog/src/pages/discover/[type]/_index.data.ts +1 -0
- package/eventcatalog/src/pages/discover/[type]/index.astro +47 -2
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +15 -14
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +13 -14
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts +11 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +81 -26
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +89 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +85 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version].md.ts +2 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version].mdx.ts +2 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/_index.data.ts +1 -0
- package/eventcatalog/src/pages/docs/teams/[id]/index.astro +24 -1
- package/eventcatalog/src/pages/docs/users/[id]/index.astro +24 -1
- package/eventcatalog/src/pages/index.astro +30 -7
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +1 -1
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version].mermaid.ts +3 -0
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/_index.data.ts +94 -0
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/index.astro +51 -0
- package/eventcatalog/src/pages/visualiser/system-context-map/index.astro +41 -0
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/_index.data.ts +91 -0
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/index.astro +51 -0
- package/eventcatalog/src/stores/sidebar-store/builders/adr.ts +3 -1
- package/eventcatalog/src/stores/sidebar-store/builders/agent.ts +3 -1
- package/eventcatalog/src/stores/sidebar-store/builders/container.ts +1 -1
- package/eventcatalog/src/stores/sidebar-store/builders/data-product.ts +1 -1
- package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +127 -49
- package/eventcatalog/src/stores/sidebar-store/builders/message.ts +1 -1
- package/eventcatalog/src/stores/sidebar-store/builders/service.ts +4 -2
- package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +2 -1
- package/eventcatalog/src/stores/sidebar-store/builders/system.ts +166 -0
- package/eventcatalog/src/stores/sidebar-store/state.ts +99 -6
- package/eventcatalog/src/types/index.ts +2 -0
- package/eventcatalog/src/utils/collection-colors.ts +6 -1
- package/eventcatalog/src/utils/collections/domains.ts +35 -0
- package/eventcatalog/src/utils/collections/icons.ts +4 -5
- package/eventcatalog/src/utils/collections/systems.ts +110 -0
- package/eventcatalog/src/utils/collections/teams.ts +15 -10
- package/eventcatalog/src/utils/collections/users.ts +7 -4
- package/eventcatalog/src/utils/collections/util.ts +2 -0
- package/eventcatalog/src/utils/icon-map.ts +20 -0
- package/eventcatalog/src/utils/icon.ts +11 -0
- package/eventcatalog/src/utils/node-graphs/container-node-graph.ts +12 -2
- package/eventcatalog/src/utils/node-graphs/domains-node-graph.ts +0 -170
- package/eventcatalog/src/utils/node-graphs/system-context-node-graph.ts +400 -0
- package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +208 -0
- package/eventcatalog/src/utils/node-graphs/utils/utils.ts +12 -0
- package/eventcatalog/src/utils/page-loaders/page-data-loader.ts +2 -0
- package/package.json +17 -14
- package/eventcatalog/src/pages/visualiser/context-map/index.astro +0 -30
|
@@ -17,3 +17,14 @@ export function iconFieldsForResource(
|
|
|
17
17
|
const styleIcon = data?.styles?.icon;
|
|
18
18
|
return isIconPath(styleIcon) ? { leftIcon: styleIcon } : { icon: defaultIcon };
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Like {@link iconFieldsForResource} but with no default icon: returns the custom
|
|
23
|
+
* `styles.icon` when one is defined, otherwise no icon at all. Used where the
|
|
24
|
+
* surrounding context (a typed section header) already conveys the resource type,
|
|
25
|
+
* so the default per-item icon would be redundant.
|
|
26
|
+
*/
|
|
27
|
+
export function customIconFieldsForResource(data: { styles?: { icon?: string } } | undefined): { leftIcon: string } | {} {
|
|
28
|
+
const styleIcon = data?.styles?.icon;
|
|
29
|
+
return isIconPath(styleIcon) ? { leftIcon: styleIcon } : {};
|
|
30
|
+
}
|
|
@@ -24,9 +24,17 @@ interface Props {
|
|
|
24
24
|
mode?: 'simple' | 'full';
|
|
25
25
|
channelRenderMode?: 'flat' | 'single';
|
|
26
26
|
collection?: CollectionEntry<'containers'>[];
|
|
27
|
+
layout?: boolean;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
export const getNodesAndEdges = async ({
|
|
30
|
+
export const getNodesAndEdges = async ({
|
|
31
|
+
id,
|
|
32
|
+
version,
|
|
33
|
+
defaultFlow,
|
|
34
|
+
mode = 'simple',
|
|
35
|
+
channelRenderMode = 'flat',
|
|
36
|
+
layout = true,
|
|
37
|
+
}: Props) => {
|
|
30
38
|
// 1. Fetch data
|
|
31
39
|
const containers = await getContainers();
|
|
32
40
|
|
|
@@ -289,7 +297,9 @@ export const getNodesAndEdges = async ({ id, version, defaultFlow, mode = 'simpl
|
|
|
289
297
|
});
|
|
290
298
|
|
|
291
299
|
// Render the diagram in memory getting hte X and Y
|
|
292
|
-
|
|
300
|
+
if (layout) {
|
|
301
|
+
dagre.layout(flow);
|
|
302
|
+
}
|
|
293
303
|
|
|
294
304
|
return {
|
|
295
305
|
nodes: calculatedNodes(flow, nodes),
|
|
@@ -13,181 +13,11 @@ import { getNodesAndEdges as getAgentsNodeAndEdges } from './agents-node-graph';
|
|
|
13
13
|
import { getNodesAndEdges as getDataProductsNodeAndEdges } from './data-products-node-graph';
|
|
14
14
|
import merge from 'lodash.merge';
|
|
15
15
|
import { createVersionedMap, findInMap } from '@utils/collections/util';
|
|
16
|
-
import type { Node } from '@xyflow/react';
|
|
17
16
|
import { getProducersOfMessage } from '@utils/collections/services';
|
|
18
17
|
import { getProducersOfMessage as getAgentProducersOfMessage } from '@utils/collections/agents';
|
|
19
18
|
|
|
20
19
|
type DagreGraph = any;
|
|
21
20
|
|
|
22
|
-
interface Props {
|
|
23
|
-
defaultFlow?: DagreGraph;
|
|
24
|
-
channelRenderMode?: 'single' | 'flat';
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const getNodesAndEdgesForDomainContextMap = async ({ defaultFlow = null }: Props) => {
|
|
28
|
-
const flow = defaultFlow ?? createDagreGraph({ ranksep: 360, nodesep: 50, edgesep: 50 });
|
|
29
|
-
let nodes = [] as any,
|
|
30
|
-
edges = [] as any;
|
|
31
|
-
|
|
32
|
-
// 1. Parallel Fetching
|
|
33
|
-
const [allDomains, services, agents, events, commands, queries] = await Promise.all([
|
|
34
|
-
getCollection('domains'),
|
|
35
|
-
getCollection('services'),
|
|
36
|
-
getCollection('agents'),
|
|
37
|
-
getCollection('events'),
|
|
38
|
-
getCollection('commands'),
|
|
39
|
-
getCollection('queries'),
|
|
40
|
-
]);
|
|
41
|
-
|
|
42
|
-
const domains = allDomains.filter((domain) => !domain.id.includes('/versioned'));
|
|
43
|
-
const messages = [...events, ...commands, ...queries];
|
|
44
|
-
|
|
45
|
-
// 2. Build optimized maps
|
|
46
|
-
const serviceMap = createVersionedMap(services);
|
|
47
|
-
const agentMap = createVersionedMap(agents);
|
|
48
|
-
const messageMap = createVersionedMap(messages);
|
|
49
|
-
|
|
50
|
-
domains.forEach((domain, index) => {
|
|
51
|
-
const nodeId = generateIdForNode(domain);
|
|
52
|
-
const rawServices = domain.data.services ?? [];
|
|
53
|
-
const rawAgents = domain.data.agents ?? [];
|
|
54
|
-
|
|
55
|
-
// Optimized service resolution
|
|
56
|
-
const domainServices = rawServices
|
|
57
|
-
.map((service) => findInMap(serviceMap, service.id, service.version))
|
|
58
|
-
.filter((e) => e !== undefined);
|
|
59
|
-
const domainAgents = rawAgents.map((agent) => findInMap(agentMap, agent.id, agent.version)).filter((e) => e !== undefined);
|
|
60
|
-
const domainResources = [...domainServices, ...domainAgents];
|
|
61
|
-
|
|
62
|
-
// Calculate domain node size based on services
|
|
63
|
-
const servicesCount = domainResources.length;
|
|
64
|
-
const SERVICES_PER_ROW = 1;
|
|
65
|
-
const SERVICE_WIDTH = 330;
|
|
66
|
-
const SERVICE_HEIGHT = 120;
|
|
67
|
-
const PADDING = 40;
|
|
68
|
-
const TITLE_HEIGHT = 20;
|
|
69
|
-
|
|
70
|
-
const rows = Math.ceil(servicesCount / SERVICES_PER_ROW);
|
|
71
|
-
const domainWidth = SERVICE_WIDTH * SERVICES_PER_ROW;
|
|
72
|
-
const domainHeight = SERVICE_HEIGHT * rows + PADDING * 4;
|
|
73
|
-
|
|
74
|
-
// Position domains in a grid layout
|
|
75
|
-
const DOMAINS_PER_ROW = 2;
|
|
76
|
-
const rowIndex = Math.floor(index / DOMAINS_PER_ROW);
|
|
77
|
-
const colIndex = index % DOMAINS_PER_ROW;
|
|
78
|
-
|
|
79
|
-
nodes.push({
|
|
80
|
-
id: nodeId,
|
|
81
|
-
type: 'group',
|
|
82
|
-
position: {
|
|
83
|
-
x: colIndex * (domainWidth + 400),
|
|
84
|
-
y: rowIndex * (domainHeight + 300),
|
|
85
|
-
},
|
|
86
|
-
style: {
|
|
87
|
-
width: domainWidth,
|
|
88
|
-
height: domainHeight,
|
|
89
|
-
backgroundColor: 'transparent',
|
|
90
|
-
borderRadius: '8px',
|
|
91
|
-
border: '1px solid #ddd',
|
|
92
|
-
'box-shadow': '0 0 10px 0 rgba(0, 0, 0, 0.1)',
|
|
93
|
-
},
|
|
94
|
-
data: {
|
|
95
|
-
label: domain.data.name,
|
|
96
|
-
domain,
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
nodes.push({
|
|
101
|
-
id: `domain-context-map-title-${domain.data.name}`,
|
|
102
|
-
data: { label: `Bounded Context: ${domain.data.name}` },
|
|
103
|
-
position: { x: 0, y: 0 },
|
|
104
|
-
style: {
|
|
105
|
-
height: 20,
|
|
106
|
-
backgroundColor: 'transparent',
|
|
107
|
-
border: 'none',
|
|
108
|
-
color: 'black',
|
|
109
|
-
width: domainWidth,
|
|
110
|
-
},
|
|
111
|
-
extent: 'parent',
|
|
112
|
-
parentId: nodeId,
|
|
113
|
-
connectable: false,
|
|
114
|
-
sourcePosition: 'left',
|
|
115
|
-
targetPosition: 'right',
|
|
116
|
-
draggable: false,
|
|
117
|
-
} as Node);
|
|
118
|
-
|
|
119
|
-
// Position services in a grid within the domain
|
|
120
|
-
if (domainResources) {
|
|
121
|
-
domainResources.forEach((service, serviceIndex) => {
|
|
122
|
-
const row = Math.floor(serviceIndex / SERVICES_PER_ROW);
|
|
123
|
-
const col = serviceIndex % SERVICES_PER_ROW;
|
|
124
|
-
|
|
125
|
-
// Add spacing between services
|
|
126
|
-
const SERVICE_MARGIN = 25;
|
|
127
|
-
const xPosition = PADDING + col * (SERVICE_WIDTH + SERVICE_MARGIN) + 20;
|
|
128
|
-
const yPosition = PADDING + row * (SERVICE_HEIGHT + SERVICE_MARGIN) + TITLE_HEIGHT;
|
|
129
|
-
|
|
130
|
-
nodes.push({
|
|
131
|
-
id: generateIdForNode(service),
|
|
132
|
-
sourcePosition: 'right',
|
|
133
|
-
targetPosition: 'left',
|
|
134
|
-
type: service.collection,
|
|
135
|
-
position: {
|
|
136
|
-
x: xPosition,
|
|
137
|
-
y: yPosition,
|
|
138
|
-
},
|
|
139
|
-
parentId: nodeId,
|
|
140
|
-
extent: 'parent',
|
|
141
|
-
draggable: false,
|
|
142
|
-
data: {
|
|
143
|
-
mode: 'full',
|
|
144
|
-
...(service.collection === 'agents' ? { agent: service.data } : { service: service.data }),
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// Edges
|
|
149
|
-
const rawReceives = service.data.receives ?? [];
|
|
150
|
-
const rawSends = service.data.sends ?? [];
|
|
151
|
-
|
|
152
|
-
// Optimized message resolution
|
|
153
|
-
const receives = rawReceives
|
|
154
|
-
.map((receive) => findInMap(messageMap, receive.id, receive.version))
|
|
155
|
-
.filter((msg): msg is any => !!msg); // Filter undefined
|
|
156
|
-
|
|
157
|
-
// Note: 'sends' was defined but not used in the original loop logic for edges?
|
|
158
|
-
// Based on original code, it iterates `receives`.
|
|
159
|
-
|
|
160
|
-
for (const receive of receives) {
|
|
161
|
-
const producers = [...getAgentProducersOfMessage(agents, receive), ...getProducersOfMessage(services, receive)];
|
|
162
|
-
|
|
163
|
-
for (const producer of producers) {
|
|
164
|
-
const isSameDomain = domainResources.some(
|
|
165
|
-
(domainService) => domainService.collection === producer.collection && domainService.data.id === producer.data.id
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
if (!isSameDomain) {
|
|
169
|
-
edges.push(
|
|
170
|
-
createEdge({
|
|
171
|
-
id: generatedIdForEdge(producer, service),
|
|
172
|
-
source: generateIdForNode(producer),
|
|
173
|
-
target: generateIdForNode(service),
|
|
174
|
-
label: getEdgeLabelForServiceAsTarget(receive),
|
|
175
|
-
zIndex: 1000,
|
|
176
|
-
})
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
nodes,
|
|
187
|
-
edges,
|
|
188
|
-
};
|
|
189
|
-
};
|
|
190
|
-
|
|
191
21
|
interface NodesAndEdgesProps {
|
|
192
22
|
id: string;
|
|
193
23
|
version: string;
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { getCollection } from 'astro:content';
|
|
2
|
+
import type { CollectionEntry } from 'astro:content';
|
|
3
|
+
import dagre from 'dagre';
|
|
4
|
+
import {
|
|
5
|
+
createDagreGraph,
|
|
6
|
+
calculatedNodes,
|
|
7
|
+
createEdge,
|
|
8
|
+
generateIdForNode,
|
|
9
|
+
buildContextMenuForSystem,
|
|
10
|
+
DEFAULT_NODE_HEIGHT,
|
|
11
|
+
} from '@utils/node-graphs/utils/utils';
|
|
12
|
+
import { MarkerType } from '@xyflow/react';
|
|
13
|
+
import { createVersionedMap, findInMap } from '@utils/collections/util';
|
|
14
|
+
|
|
15
|
+
type DagreGraph = any;
|
|
16
|
+
|
|
17
|
+
interface NodesAndEdgesProps {
|
|
18
|
+
id: string;
|
|
19
|
+
version: string;
|
|
20
|
+
defaultFlow?: DagreGraph;
|
|
21
|
+
mode?: 'simple' | 'full';
|
|
22
|
+
layout?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// System nodes are wider than the default to fit the system name + summary comfortably.
|
|
26
|
+
const SYSTEM_NODE_WIDTH = 270;
|
|
27
|
+
// Actor nodes are narrower than systems — a compact pill with an icon + name.
|
|
28
|
+
const ACTOR_NODE_WIDTH = 180;
|
|
29
|
+
|
|
30
|
+
type SystemActor = {
|
|
31
|
+
id: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
label?: string;
|
|
34
|
+
direction?: 'inbound' | 'outbound';
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type SystemRelationship = {
|
|
38
|
+
id: string;
|
|
39
|
+
version?: string;
|
|
40
|
+
label?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type IncomingSystemRelationship = {
|
|
44
|
+
source: CollectionEntry<'systems'>;
|
|
45
|
+
relationship: SystemRelationship;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
interface ContextGraphFromSeedsProps {
|
|
49
|
+
// The systems the breadth-first traversal starts from. The single-system context
|
|
50
|
+
// diagram seeds with one system; the domain-level diagram seeds with every system
|
|
51
|
+
// in the domain.
|
|
52
|
+
seedSystems: CollectionEntry<'systems'>[];
|
|
53
|
+
systemMap: Map<string, CollectionEntry<'systems'>[]>;
|
|
54
|
+
serviceMap: Map<string, CollectionEntry<'services'>[]>;
|
|
55
|
+
defaultFlow?: DagreGraph;
|
|
56
|
+
mode?: 'simple' | 'full';
|
|
57
|
+
layout?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Shared builder for the System Diagram.
|
|
62
|
+
*
|
|
63
|
+
* Starting from the given seed systems, we walk system relationships in both directions
|
|
64
|
+
* (breadth-first) to the systems it relates to and the systems that relate to it, then
|
|
65
|
+
* their relationships, and so on — building the reachable neighbourhood. Each system
|
|
66
|
+
* becomes a node; each relationship that has a `label` becomes a labelled edge
|
|
67
|
+
* (relationships without a label are intentionally not drawn). Actors are rendered
|
|
68
|
+
* inline and deduped across all systems.
|
|
69
|
+
*/
|
|
70
|
+
const buildContextGraphFromSeeds = ({
|
|
71
|
+
seedSystems,
|
|
72
|
+
systemMap,
|
|
73
|
+
serviceMap,
|
|
74
|
+
defaultFlow,
|
|
75
|
+
mode = 'simple',
|
|
76
|
+
layout = true,
|
|
77
|
+
}: ContextGraphFromSeedsProps) => {
|
|
78
|
+
const flow = defaultFlow || createDagreGraph({ ranksep: 280, nodesep: 80, edgesep: 50 });
|
|
79
|
+
const nodes = new Map<string, any>();
|
|
80
|
+
const edges = new Map<string, any>();
|
|
81
|
+
|
|
82
|
+
// Total messages a system handles = the sum of sends + receives across all of
|
|
83
|
+
// its services. Used for the "Messages" stat on the system node.
|
|
84
|
+
const countMessagesForSystem = (system: CollectionEntry<'systems'>) => {
|
|
85
|
+
return (system.data.services || []).reduce((total, ref) => {
|
|
86
|
+
const service = findInMap(serviceMap, ref.id, ref.version);
|
|
87
|
+
if (!service) return total;
|
|
88
|
+
return total + (service.data.sends || []).length + (service.data.receives || []).length;
|
|
89
|
+
}, 0);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Actor node ids, tracked so they can be sized differently during layout.
|
|
93
|
+
const actorNodeIds = new Set<string>();
|
|
94
|
+
// System node ids, tracked so we can detect reciprocal system-to-system
|
|
95
|
+
// relationships and collapse them into a single double-headed edge.
|
|
96
|
+
const systemNodeIds = new Set<string>();
|
|
97
|
+
const incomingRelationshipsByTargetNodeId = new Map<string, IncomingSystemRelationship[]>();
|
|
98
|
+
|
|
99
|
+
for (const versionedSystems of systemMap.values()) {
|
|
100
|
+
for (const source of versionedSystems) {
|
|
101
|
+
const relationships = (source.data.relationships || []) as SystemRelationship[];
|
|
102
|
+
|
|
103
|
+
for (const relationship of relationships) {
|
|
104
|
+
const target = findInMap(systemMap, relationship.id, relationship.version);
|
|
105
|
+
if (!target) continue;
|
|
106
|
+
|
|
107
|
+
const targetNodeId = generateIdForNode(target);
|
|
108
|
+
const incoming = incomingRelationshipsByTargetNodeId.get(targetNodeId) || [];
|
|
109
|
+
incoming.push({ source, relationship });
|
|
110
|
+
incomingRelationshipsByTargetNodeId.set(targetNodeId, incoming);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Add (or reuse) an actor node. Actors are inline and deduped by `actor:{id}`,
|
|
116
|
+
// so the same actor referenced from multiple systems appears once.
|
|
117
|
+
const addActorNode = (actor: SystemActor) => {
|
|
118
|
+
const nodeId = `actor-${actor.id}`;
|
|
119
|
+
if (nodes.has(nodeId)) return nodeId;
|
|
120
|
+
|
|
121
|
+
actorNodeIds.add(nodeId);
|
|
122
|
+
nodes.set(nodeId, {
|
|
123
|
+
id: nodeId,
|
|
124
|
+
type: 'context-actor',
|
|
125
|
+
sourcePosition: 'right',
|
|
126
|
+
targetPosition: 'left',
|
|
127
|
+
data: {
|
|
128
|
+
mode,
|
|
129
|
+
name: actor.name || actor.id,
|
|
130
|
+
},
|
|
131
|
+
position: { x: 0, y: 0 },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return nodeId;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const addSystemNode = (system: CollectionEntry<'systems'>) => {
|
|
138
|
+
const nodeId = generateIdForNode(system);
|
|
139
|
+
systemNodeIds.add(nodeId);
|
|
140
|
+
if (nodes.has(nodeId)) return nodeId;
|
|
141
|
+
|
|
142
|
+
nodes.set(nodeId, {
|
|
143
|
+
id: nodeId,
|
|
144
|
+
type: 'systems',
|
|
145
|
+
sourcePosition: 'right',
|
|
146
|
+
targetPosition: 'left',
|
|
147
|
+
data: {
|
|
148
|
+
mode,
|
|
149
|
+
system: { ...system.data },
|
|
150
|
+
// Counts of what the system groups, shown as a stats row on the node.
|
|
151
|
+
// The raw frontmatter arrays are pointer lists, so their length is the count.
|
|
152
|
+
servicesCount: (system.data.services || []).length,
|
|
153
|
+
entitiesCount: (system.data.entities || []).length,
|
|
154
|
+
containersCount: (system.data.containers || []).length,
|
|
155
|
+
messagesCount: countMessagesForSystem(system),
|
|
156
|
+
contextMenu: buildContextMenuForSystem({ id: system.data.id, version: system.data.version }),
|
|
157
|
+
},
|
|
158
|
+
position: { x: 0, y: 0 },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return nodeId;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const addSystemRelationshipEdge = ({
|
|
165
|
+
sourceNodeId,
|
|
166
|
+
targetNodeId,
|
|
167
|
+
label,
|
|
168
|
+
}: {
|
|
169
|
+
sourceNodeId: string;
|
|
170
|
+
targetNodeId: string;
|
|
171
|
+
label?: string;
|
|
172
|
+
}) => {
|
|
173
|
+
if (!label) return;
|
|
174
|
+
|
|
175
|
+
const edgeId = `${sourceNodeId}-${targetNodeId}`;
|
|
176
|
+
edges.set(
|
|
177
|
+
edgeId,
|
|
178
|
+
createEdge({
|
|
179
|
+
id: edgeId,
|
|
180
|
+
source: sourceNodeId,
|
|
181
|
+
target: targetNodeId,
|
|
182
|
+
label,
|
|
183
|
+
type: 'straight',
|
|
184
|
+
markerEnd: { type: MarkerType.ArrowClosed, width: 20, height: 20 },
|
|
185
|
+
})
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Breadth-first traversal over the relationship graph, following each system's
|
|
190
|
+
// `relationships` to other systems and reverse relationships from other systems.
|
|
191
|
+
// We visit each system once.
|
|
192
|
+
const visited = new Set<string>();
|
|
193
|
+
const queue: CollectionEntry<'systems'>[] = [...seedSystems];
|
|
194
|
+
|
|
195
|
+
while (queue.length > 0) {
|
|
196
|
+
const system = queue.shift() as CollectionEntry<'systems'>;
|
|
197
|
+
const sourceNodeId = generateIdForNode(system);
|
|
198
|
+
|
|
199
|
+
if (visited.has(sourceNodeId)) continue;
|
|
200
|
+
visited.add(sourceNodeId);
|
|
201
|
+
|
|
202
|
+
addSystemNode(system);
|
|
203
|
+
|
|
204
|
+
const relationships = (system.data.relationships || []) as SystemRelationship[];
|
|
205
|
+
|
|
206
|
+
for (const relationship of relationships) {
|
|
207
|
+
const target = findInMap(systemMap, relationship.id, relationship.version);
|
|
208
|
+
// Skip dangling targets — degrade gracefully like the other pointer types.
|
|
209
|
+
if (!target) continue;
|
|
210
|
+
|
|
211
|
+
const targetNodeId = addSystemNode(target);
|
|
212
|
+
|
|
213
|
+
// Only draw an edge when the relationship declares a label. A relationship without a
|
|
214
|
+
// label still pulls the target system into the diagram, but has no edge drawn.
|
|
215
|
+
addSystemRelationshipEdge({ sourceNodeId, targetNodeId, label: relationship.label });
|
|
216
|
+
|
|
217
|
+
if (!visited.has(targetNodeId)) {
|
|
218
|
+
queue.push(target);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const incomingRelationships = incomingRelationshipsByTargetNodeId.get(sourceNodeId) || [];
|
|
223
|
+
for (const { source, relationship } of incomingRelationships) {
|
|
224
|
+
const incomingSourceNodeId = addSystemNode(source);
|
|
225
|
+
|
|
226
|
+
// Preserve relationship direction: the referring system remains the edge source,
|
|
227
|
+
// even though this traversal discovered it from the target side.
|
|
228
|
+
addSystemRelationshipEdge({
|
|
229
|
+
sourceNodeId: incomingSourceNodeId,
|
|
230
|
+
targetNodeId: sourceNodeId,
|
|
231
|
+
label: relationship.label,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (!visited.has(incomingSourceNodeId)) {
|
|
235
|
+
queue.push(source);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Actors that interact with this system. Each becomes an actor node with an
|
|
240
|
+
// edge whose direction depends on `direction` (inbound = actor -> system,
|
|
241
|
+
// outbound = system -> actor). Actors don't pull other systems into the graph.
|
|
242
|
+
const actors = (system.data.actors || []) as SystemActor[];
|
|
243
|
+
|
|
244
|
+
for (const actor of actors) {
|
|
245
|
+
const actorNodeId = addActorNode(actor);
|
|
246
|
+
const direction = actor.direction || 'inbound';
|
|
247
|
+
const source = direction === 'inbound' ? actorNodeId : sourceNodeId;
|
|
248
|
+
const target = direction === 'inbound' ? sourceNodeId : actorNodeId;
|
|
249
|
+
|
|
250
|
+
const edgeId = `${source}-${target}`;
|
|
251
|
+
if (edges.has(edgeId)) continue;
|
|
252
|
+
|
|
253
|
+
edges.set(
|
|
254
|
+
edgeId,
|
|
255
|
+
createEdge({
|
|
256
|
+
id: edgeId,
|
|
257
|
+
source,
|
|
258
|
+
target,
|
|
259
|
+
label: actor.label,
|
|
260
|
+
type: 'straight',
|
|
261
|
+
markerEnd: { type: MarkerType.ArrowClosed, width: 20, height: 20 },
|
|
262
|
+
})
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Collapse reciprocal system-to-system relationships. When system A relates to B
|
|
268
|
+
// and B also relates back to A, we'd otherwise draw two parallel edges. Instead we
|
|
269
|
+
// keep a single edge with arrowheads on both ends. Each direction's label is
|
|
270
|
+
// preserved (joined when they differ). Only system↔system edges are merged — actor
|
|
271
|
+
// edges are left untouched.
|
|
272
|
+
const mergedEdgeKeys = new Set<string>();
|
|
273
|
+
for (const edge of Array.from(edges.values())) {
|
|
274
|
+
if (mergedEdgeKeys.has(edge.id)) continue;
|
|
275
|
+
if (!systemNodeIds.has(edge.source) || !systemNodeIds.has(edge.target)) continue;
|
|
276
|
+
|
|
277
|
+
const reverseId = `${edge.target}-${edge.source}`;
|
|
278
|
+
const reverse = edges.get(reverseId);
|
|
279
|
+
if (!reverse || reverseId === edge.id) continue;
|
|
280
|
+
|
|
281
|
+
// Merge the two labels, keeping a single value when they match.
|
|
282
|
+
const labels = [edge.label, reverse.label].filter(Boolean);
|
|
283
|
+
const mergedLabel = labels.length > 1 && labels[0] !== labels[1] ? labels.join(' / ') : labels[0];
|
|
284
|
+
|
|
285
|
+
edges.set(edge.id, {
|
|
286
|
+
...edge,
|
|
287
|
+
label: mergedLabel,
|
|
288
|
+
markerStart: { type: MarkerType.ArrowClosed, width: 20, height: 20 },
|
|
289
|
+
});
|
|
290
|
+
edges.delete(reverseId);
|
|
291
|
+
mergedEdgeKeys.add(edge.id);
|
|
292
|
+
mergedEdgeKeys.add(reverseId);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Lay the graph out
|
|
296
|
+
nodes.forEach((node) =>
|
|
297
|
+
flow.setNode(node.id, {
|
|
298
|
+
width: actorNodeIds.has(node.id) ? ACTOR_NODE_WIDTH : SYSTEM_NODE_WIDTH,
|
|
299
|
+
height: DEFAULT_NODE_HEIGHT,
|
|
300
|
+
})
|
|
301
|
+
);
|
|
302
|
+
edges.forEach((edge) => flow.setEdge(edge.source, edge.target));
|
|
303
|
+
|
|
304
|
+
if (layout) {
|
|
305
|
+
dagre.layout(flow);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
nodes: calculatedNodes(flow, Array.from(nodes.values())),
|
|
310
|
+
edges: [...edges.values()],
|
|
311
|
+
};
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Builds the System Diagram for a single system, seeding the traversal from
|
|
316
|
+
* that system and walking its relationships and actors outward.
|
|
317
|
+
*/
|
|
318
|
+
export const getNodesAndEdges = async ({ id, version, defaultFlow, mode = 'simple', layout = true }: NodesAndEdgesProps) => {
|
|
319
|
+
const [allSystems, allServices] = await Promise.all([getCollection('systems'), getCollection('services')]);
|
|
320
|
+
const systemMap = createVersionedMap(allSystems);
|
|
321
|
+
const serviceMap = createVersionedMap(allServices);
|
|
322
|
+
|
|
323
|
+
const rootSystem = findInMap(systemMap, id, version);
|
|
324
|
+
|
|
325
|
+
// Nothing found...
|
|
326
|
+
if (!rootSystem) {
|
|
327
|
+
return { nodes: [], edges: [] };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return buildContextGraphFromSeeds({ seedSystems: [rootSystem], systemMap, serviceMap, defaultFlow, mode, layout });
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Builds the System Diagram for a whole domain: seeds the traversal with every
|
|
335
|
+
* system the domain declares, then walks their relationships and actors. The result is
|
|
336
|
+
* the domain's systems plus any related systems they point at (or that point at them),
|
|
337
|
+
* with actors rendered inline — giving a single context view across the domain.
|
|
338
|
+
*/
|
|
339
|
+
export const getNodesAndEdgesForDomainSystems = async ({
|
|
340
|
+
id,
|
|
341
|
+
version,
|
|
342
|
+
defaultFlow,
|
|
343
|
+
mode = 'simple',
|
|
344
|
+
layout = true,
|
|
345
|
+
}: NodesAndEdgesProps) => {
|
|
346
|
+
const [allDomains, allSystems, allServices] = await Promise.all([
|
|
347
|
+
getCollection('domains'),
|
|
348
|
+
getCollection('systems'),
|
|
349
|
+
getCollection('services'),
|
|
350
|
+
]);
|
|
351
|
+
const domainMap = createVersionedMap(allDomains);
|
|
352
|
+
const systemMap = createVersionedMap(allSystems);
|
|
353
|
+
const serviceMap = createVersionedMap(allServices);
|
|
354
|
+
|
|
355
|
+
const domain = findInMap(domainMap, id, version);
|
|
356
|
+
|
|
357
|
+
// Nothing found, or the domain has no systems to seed from.
|
|
358
|
+
if (!domain) {
|
|
359
|
+
return { nodes: [], edges: [] };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const seedSystems = (domain.data.systems || [])
|
|
363
|
+
.map((ref: { id: string; version?: string }) => findInMap(systemMap, ref.id, ref.version))
|
|
364
|
+
.filter((system): system is CollectionEntry<'systems'> => !!system);
|
|
365
|
+
|
|
366
|
+
if (seedSystems.length === 0) {
|
|
367
|
+
return { nodes: [], edges: [] };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return buildContextGraphFromSeeds({ seedSystems, systemMap, serviceMap, defaultFlow, mode, layout });
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
interface AllSystemsProps {
|
|
374
|
+
defaultFlow?: DagreGraph;
|
|
375
|
+
mode?: 'simple' | 'full';
|
|
376
|
+
layout?: boolean;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Builds a global System Context Map for the whole catalog: every system becomes a
|
|
381
|
+
* seed, so the traversal draws all systems, the relationships between them, and the
|
|
382
|
+
* actors around them — a single map of how every system in the catalog relates.
|
|
383
|
+
*/
|
|
384
|
+
export const getNodesAndEdgesForAllSystems = async ({ defaultFlow, mode = 'simple', layout = true }: AllSystemsProps = {}) => {
|
|
385
|
+
const [allSystems, allServices] = await Promise.all([getCollection('systems'), getCollection('services')]);
|
|
386
|
+
const systemMap = createVersionedMap(allSystems);
|
|
387
|
+
const serviceMap = createVersionedMap(allServices);
|
|
388
|
+
|
|
389
|
+
// Seed with the latest version of every system. `systemMap` sorts each entry so
|
|
390
|
+
// index [0] is the latest, which avoids drawing every historical version.
|
|
391
|
+
const seedSystems = Array.from(systemMap.values())
|
|
392
|
+
.map((versions) => versions[0])
|
|
393
|
+
.filter((system): system is CollectionEntry<'systems'> => !!system);
|
|
394
|
+
|
|
395
|
+
if (seedSystems.length === 0) {
|
|
396
|
+
return { nodes: [], edges: [] };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return buildContextGraphFromSeeds({ seedSystems, systemMap, serviceMap, defaultFlow, mode, layout });
|
|
400
|
+
};
|