@eventcatalog/core 5.0.0-beta.12 → 5.0.0-beta.13
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 +4 -4
- package/dist/catalog-runtime.cjs +1 -1
- package/dist/{chunk-FY6MSP6J.js → chunk-BO2HHVZ3.js} +1 -1
- package/dist/{chunk-CX2VB7AI.js → chunk-JPT3QC5V.js} +1 -1
- package/dist/{chunk-BWC4YKW3.js → chunk-PLTLW45Q.js} +5 -5
- package/dist/{chunk-TSY5CC27.js → chunk-T75DOVFP.js} +1 -1
- package/dist/{chunk-QB7X34OD.js → chunk-XA5W6DXX.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +2 -2
- package/dist/eventcatalog.js +14 -14
- package/dist/federation/federate.js +3 -3
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio-fonts.css.mjs +3 -0
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.css.mjs +3 -0
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.mjs +3 -0
- package/dist/runtime-dependencies/manifest.json +3 -0
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +11 -3
- package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +3 -3
- package/eventcatalog/src/features/mcp/apps/canvas/styles.css +3 -1
- package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +7 -5
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +133 -71
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +66 -66
- package/eventcatalog/src/features/studio/agent-choreography.ts +5 -4
- package/eventcatalog/src/features/studio/agents.ts +28 -0
- package/eventcatalog/src/features/studio/api/canvases-api.ts +1 -1
- package/eventcatalog/src/features/studio/canvas-actions.ts +22 -14
- package/eventcatalog/src/features/studio/canvas-preview.ts +1 -2
- package/eventcatalog/src/features/studio/catalog.ts +1 -4
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +246 -115
- package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +7 -3
- package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +272 -119
- package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +1 -2
- package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +1 -1
- package/eventcatalog/src/features/studio/components/JoinForm.tsx +101 -26
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +102 -241
- package/eventcatalog/src/features/studio/components/NodeDetails.tsx +183 -0
- package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +1 -1
- package/eventcatalog/src/features/studio/components/Presence.tsx +6 -8
- package/eventcatalog/src/features/studio/components/ShareDialog.tsx +277 -141
- package/eventcatalog/src/features/studio/components/ShortcutsDialog.tsx +41 -0
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +1005 -354
- package/eventcatalog/src/features/studio/document-name.ts +7 -0
- package/eventcatalog/src/features/studio/from-visualiser.ts +1 -1
- package/eventcatalog/src/features/studio/hooks/presence-sender.ts +19 -2
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +9 -3
- package/eventcatalog/src/features/studio/hooks/use-comments.ts +2 -2
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +162 -23
- package/eventcatalog/src/features/studio/identity.ts +23 -1
- package/eventcatalog/src/features/studio/pages/[id].astro +2 -1
- package/eventcatalog/src/features/studio/pages/index.astro +1 -2
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +7 -22
- package/eventcatalog/src/features/studio/server/runtime.ts +4 -5
- package/package.json +3 -3
- package/eventcatalog/src/features/studio/canvas-doc.ts +0 -569
- package/eventcatalog/src/features/studio/clipboard.ts +0 -97
- package/eventcatalog/src/features/studio/components/CanvasControls.tsx +0 -119
- package/eventcatalog/src/features/studio/components/Comments.tsx +0 -777
- package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +0 -145
- package/eventcatalog/src/features/studio/components/DragGhost.tsx +0 -115
- package/eventcatalog/src/features/studio/components/Picture.tsx +0 -21
- package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +0 -212
- package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +0 -271
- package/eventcatalog/src/features/studio/components/icons.ts +0 -53
- package/eventcatalog/src/features/studio/components/status.ts +0 -53
- package/eventcatalog/src/features/studio/edges.ts +0 -56
- package/eventcatalog/src/features/studio/flow-state.ts +0 -121
- package/eventcatalog/src/features/studio/grouping.ts +0 -230
- package/eventcatalog/src/features/studio/hooks/use-counter-scale.ts +0 -20
- package/eventcatalog/src/features/studio/layout.ts +0 -56
- package/eventcatalog/src/features/studio/level-layout.ts +0 -145
- package/eventcatalog/src/features/studio/levels.ts +0 -337
- package/eventcatalog/src/features/studio/node-types.ts +0 -245
- package/eventcatalog/src/features/studio/placement.ts +0 -197
- package/eventcatalog/src/features/studio/studio.css +0 -273
- package/dist/{chunk-KHACCTW2.js → chunk-ZEY2XUJN.js} +4 -4
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
import { MarkerType, type Edge, type Node } from '@xyflow/react';
|
|
2
|
-
import { getMessagesLabel } from '@eventcatalog/visualiser/layout';
|
|
3
|
-
import { MESSAGE_COLLECTIONS } from './edges';
|
|
4
|
-
import { GROUP_TYPES, isGroupType, isWrittenOnCanvas } from './node-types';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Levels of detail, the same as EventCatalog's diagrams:
|
|
8
|
-
* - L1: domains, systems and how they relate (services folded into the system or domain that contains them)
|
|
9
|
-
* - L2: services and data stores, with messages and channels hidden (the nodes either side connected directly)
|
|
10
|
-
* - L3: everything, including messages and channels (the canvas people edit)
|
|
11
|
-
*
|
|
12
|
-
* What a domain or system contains is what sits inside it (as a container), or what it's connected to with
|
|
13
|
-
* "contains" (as a card, which catalog domains and systems get when their services are on the canvas).
|
|
14
|
-
*/
|
|
15
|
-
export type Level = 1 | 2 | 3;
|
|
16
|
-
|
|
17
|
-
export const LEVELS: { level: Level; description: string }[] = [
|
|
18
|
-
{ level: 1, description: 'Domains, systems and their relationships' },
|
|
19
|
-
{ level: 2, description: 'Services and data stores' },
|
|
20
|
-
{ level: 3, description: 'Everything, including messages and channels' },
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
// Domains and systems: as cards (what they contain is connected with "contains") or containers (it's inside)
|
|
24
|
-
const CONTAINER_TYPES = new Set(['system', 'context-domain', GROUP_TYPES.system, GROUP_TYPES.domain]);
|
|
25
|
-
const isSystem = (node: Node) => node.type === 'system' || node.type === GROUP_TYPES.system;
|
|
26
|
-
const isDomain = (node: Node) => node.type === 'context-domain' || node.type === GROUP_TYPES.domain;
|
|
27
|
-
|
|
28
|
-
const isMessage = (node: Node) => !!MESSAGE_COLLECTIONS[node.type ?? ''];
|
|
29
|
-
const isCarrier = (node: Node) => isMessage(node) || node.type === 'channel';
|
|
30
|
-
const isContainer = (node: Node) => CONTAINER_TYPES.has(node.type ?? '');
|
|
31
|
-
const messageName = (node: Node) => String((node.data as { message?: { name?: string } }).message?.name ?? node.id);
|
|
32
|
-
|
|
33
|
-
// Edges as the visualiser's levels draw them: what's folded together is joined by a plain line, and what was joined
|
|
34
|
-
// through hidden messages or channels by a dashed, muted one ("bridged"), each with the visualiser's arrow. On L1
|
|
35
|
-
// the visualiser folds messages into the domain or system they're in, so edges to and from them are plain.
|
|
36
|
-
const EDGE_STROKE = 'var(--ec-edge-stroke, #6b7280)';
|
|
37
|
-
const MUTED = 'rgb(var(--ec-page-text-muted))';
|
|
38
|
-
const FOLDED_EDGE = {
|
|
39
|
-
type: 'smoothstep',
|
|
40
|
-
style: { strokeWidth: 1, stroke: EDGE_STROKE },
|
|
41
|
-
markerEnd: { type: MarkerType.ArrowClosed, width: 20, height: 20 },
|
|
42
|
-
} satisfies Partial<Edge>;
|
|
43
|
-
const BRIDGED_EDGE = {
|
|
44
|
-
type: 'smoothstep',
|
|
45
|
-
style: { strokeWidth: 1.5, stroke: MUTED, strokeDasharray: '5 5' },
|
|
46
|
-
markerEnd: { type: MarkerType.ArrowClosed, width: 20, height: 20, color: MUTED },
|
|
47
|
-
} satisfies Partial<Edge>;
|
|
48
|
-
|
|
49
|
-
/** What a node becomes at a level: kept (as itself or another node), carried through (messages, channels) or dropped */
|
|
50
|
-
type Representative = { kind: 'node'; id: string } | { kind: 'carrier' } | { kind: 'drop' };
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Collapse a graph: each node is kept, replaced by another node (e.g. a service by its system), carried through
|
|
54
|
-
* (messages and channels: the nodes either side get connected directly, "bridged") or dropped. Edges between two
|
|
55
|
-
* nodes that are kept as they are stay as they were (`keepDirect`, for L2); the rest are merged per pair and kind
|
|
56
|
-
* (folded or bridged), labelled with the messages they carry.
|
|
57
|
-
*
|
|
58
|
-
* With `relationships` (L1), edges drawn straight between two nodes that are kept as they are (e.g. one system to
|
|
59
|
-
* another) are relationships, like the visualiser's context relationships: labelled as they were, and shown
|
|
60
|
-
* instead of the messages between the same two nodes (either way).
|
|
61
|
-
*/
|
|
62
|
-
const collapseGraph = (
|
|
63
|
-
nodes: Node[],
|
|
64
|
-
edges: Edge[],
|
|
65
|
-
representativeOf: (node: Node) => Representative,
|
|
66
|
-
{
|
|
67
|
-
keepDirect = false,
|
|
68
|
-
relationships = false,
|
|
69
|
-
foldsMessagesInto,
|
|
70
|
-
}: {
|
|
71
|
-
keepDirect?: boolean;
|
|
72
|
-
relationships?: boolean;
|
|
73
|
-
/** Nodes the messages they send or receive are folded into (L1: domains and systems), so edges through them are plain */
|
|
74
|
-
foldsMessagesInto?: (node: Node) => boolean;
|
|
75
|
-
} = {}
|
|
76
|
-
) => {
|
|
77
|
-
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
78
|
-
// Whether one node is inside another (e.g. a system in its domain): they're not connected, one holds the other
|
|
79
|
-
const isInside = (id: string, containerId: string) => {
|
|
80
|
-
for (let node = byId.get(id); node?.parentId; node = byId.get(node.parentId)) if (node.parentId === containerId) return true;
|
|
81
|
-
return false;
|
|
82
|
-
};
|
|
83
|
-
const representatives = new Map(nodes.map((node) => [node.id, representativeOf(node)]));
|
|
84
|
-
const outgoing = new Map<string, Edge[]>();
|
|
85
|
-
edges.forEach((edge) => outgoing.set(edge.source, [...(outgoing.get(edge.source) ?? []), edge]));
|
|
86
|
-
|
|
87
|
-
type Merged = {
|
|
88
|
-
source: string;
|
|
89
|
-
target: string;
|
|
90
|
-
bridged: boolean;
|
|
91
|
-
relationship: boolean;
|
|
92
|
-
messages: Map<string, string>;
|
|
93
|
-
labels: string[];
|
|
94
|
-
};
|
|
95
|
-
const merged = new Map<string, Merged>();
|
|
96
|
-
const kept: Edge[] = [];
|
|
97
|
-
const addMessage = (messages: Map<string, string>, node: Node) => {
|
|
98
|
-
if (isMessage(node)) messages.set(messageName(node), MESSAGE_COLLECTIONS[node.type!]);
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
edges.forEach((edge) => {
|
|
102
|
-
const from = representatives.get(edge.source);
|
|
103
|
-
if (from?.kind !== 'node') return;
|
|
104
|
-
const to = representatives.get(edge.target);
|
|
105
|
-
const direct = from.id === edge.source && to?.kind === 'node' && to.id === edge.target;
|
|
106
|
-
if (keepDirect && direct) return kept.push(edge);
|
|
107
|
-
// Follow the edge through carried nodes to the node(s) it reaches, collecting the messages on the way
|
|
108
|
-
const stack = [{ id: edge.target, messages: new Map<string, string>(), labels: [String(edge.label ?? '')], bridged: false }];
|
|
109
|
-
const visited = new Set<string>();
|
|
110
|
-
addMessage(stack[0].messages, byId.get(edge.source)!);
|
|
111
|
-
while (stack.length) {
|
|
112
|
-
const current = stack.pop()!;
|
|
113
|
-
if (visited.has(current.id)) continue;
|
|
114
|
-
visited.add(current.id);
|
|
115
|
-
const node = byId.get(current.id);
|
|
116
|
-
const reached = representatives.get(current.id);
|
|
117
|
-
if (!node || !reached || reached.kind === 'drop') continue;
|
|
118
|
-
const messages = new Map(current.messages);
|
|
119
|
-
addMessage(messages, node);
|
|
120
|
-
if (reached.kind === 'carrier') {
|
|
121
|
-
(outgoing.get(current.id) ?? []).forEach((next) =>
|
|
122
|
-
stack.push({ id: next.target, messages, labels: [] as string[], bridged: true })
|
|
123
|
-
);
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
// Edges inside what was collapsed disappear, and so do edges to what a node is in (e.g. a service loose in a
|
|
127
|
-
// domain, folded into it, and a system in the same domain)
|
|
128
|
-
if (from.id === reached.id || isInside(from.id, reached.id) || isInside(reached.id, from.id)) continue;
|
|
129
|
-
const relationship = relationships && direct && current.id === edge.target;
|
|
130
|
-
const key = `${relationship ? 'relationship' : current.bridged ? 'bridged' : 'level'}-${from.id}-${reached.id}`;
|
|
131
|
-
const entry: Merged = merged.get(key) ?? {
|
|
132
|
-
source: from.id,
|
|
133
|
-
target: reached.id,
|
|
134
|
-
bridged: current.bridged,
|
|
135
|
-
relationship,
|
|
136
|
-
messages: new Map(),
|
|
137
|
-
labels: [],
|
|
138
|
-
};
|
|
139
|
-
messages.forEach((collection, name) => entry.messages.set(name, collection));
|
|
140
|
-
entry.labels.push(...current.labels.filter(Boolean));
|
|
141
|
-
merged.set(key, entry);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const keptIds = new Set([...representatives.values()].flatMap((rep) => (rep.kind === 'node' ? [rep.id] : [])));
|
|
146
|
-
// A relationship between two nodes is shown instead of the messages between them (either way)
|
|
147
|
-
const related = new Set(
|
|
148
|
-
[...merged.values()]
|
|
149
|
-
.filter((entry) => entry.relationship)
|
|
150
|
-
.flatMap(({ source, target }) => [`${source}>${target}`, `${target}>${source}`])
|
|
151
|
-
);
|
|
152
|
-
// Actors' connections are relationships too, labelled as they were (even when what they use is folded)
|
|
153
|
-
const isActor = (id: string) => byId.get(id)?.type === 'actor';
|
|
154
|
-
const labelOf = ({ source, target, bridged, relationship, messages, labels }: Merged) => {
|
|
155
|
-
const written = labels.length ? [...new Set(labels)].join(', ') : undefined;
|
|
156
|
-
if (relationship) return written;
|
|
157
|
-
return getMessagesLabel(messages) ?? (!bridged && (isActor(source) || isActor(target)) ? written : undefined);
|
|
158
|
-
};
|
|
159
|
-
return {
|
|
160
|
-
nodes: nodes.filter((node) => keptIds.has(node.id)),
|
|
161
|
-
edges: [
|
|
162
|
-
...kept,
|
|
163
|
-
...[...merged.entries()]
|
|
164
|
-
.filter(([, entry]) => entry.relationship || !related.has(`${entry.source}>${entry.target}`))
|
|
165
|
-
.map(([id, entry]): Edge => {
|
|
166
|
-
const label = labelOf(entry);
|
|
167
|
-
const folded = [entry.source, entry.target].some((nodeId) => {
|
|
168
|
-
const node = byId.get(nodeId);
|
|
169
|
-
return !!node && !!foldsMessagesInto?.(node);
|
|
170
|
-
});
|
|
171
|
-
return {
|
|
172
|
-
id,
|
|
173
|
-
source: entry.source,
|
|
174
|
-
target: entry.target,
|
|
175
|
-
...(entry.bridged && !folded ? BRIDGED_EDGE : FOLDED_EDGE),
|
|
176
|
-
...(label && { label }),
|
|
177
|
-
};
|
|
178
|
-
}),
|
|
179
|
-
],
|
|
180
|
-
};
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* The system and domain each node is in: the containers it sits inside, or the domain and system cards
|
|
185
|
-
* connected to it with "contains"
|
|
186
|
-
*/
|
|
187
|
-
const getContainers = (nodes: Node[], edges: Edge[]) => {
|
|
188
|
-
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
189
|
-
const containers = new Map<string, { system?: string; domain?: string }>();
|
|
190
|
-
const add = (nodeId: string, container: Node) => {
|
|
191
|
-
const entry = containers.get(nodeId) ?? {};
|
|
192
|
-
if (isSystem(container)) entry.system ??= container.id;
|
|
193
|
-
else entry.domain ??= container.id;
|
|
194
|
-
containers.set(nodeId, entry);
|
|
195
|
-
};
|
|
196
|
-
for (const node of nodes) {
|
|
197
|
-
for (
|
|
198
|
-
let parent = node.parentId ? byId.get(node.parentId) : undefined;
|
|
199
|
-
parent;
|
|
200
|
-
parent = parent.parentId ? byId.get(parent.parentId) : undefined
|
|
201
|
-
) {
|
|
202
|
-
if (isContainer(parent)) add(node.id, parent);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
for (const edge of edges) {
|
|
206
|
-
const container = byId.get(edge.source);
|
|
207
|
-
if (container && isContainer(container) && edge.label === 'contains') add(edge.target, container);
|
|
208
|
-
}
|
|
209
|
-
return containers;
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Containers are sized by the layout: to fit what's in them, or (empty) the size the visualiser lays empty groups
|
|
214
|
-
* out at. Same ids, so switching levels grows and shrinks them.
|
|
215
|
-
*/
|
|
216
|
-
const sizedByLayout = ({ nodes, edges }: { nodes: Node[]; edges: Edge[] }) => ({
|
|
217
|
-
edges,
|
|
218
|
-
nodes: nodes.map((node) =>
|
|
219
|
-
isGroupType(node.type) ? { ...node, width: undefined, height: undefined, measured: undefined } : node
|
|
220
|
-
),
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
/** L2: messages and channels hidden, the nodes either side connected directly */
|
|
224
|
-
const level2 = (nodes: Node[], edges: Edge[]) =>
|
|
225
|
-
collapseGraph(nodes, edges, (node) => (isCarrier(node) ? { kind: 'carrier' } : { kind: 'node', id: node.id }), {
|
|
226
|
-
keepDirect: true,
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
/** Whether a node is inside a container, at any depth */
|
|
230
|
-
const isWithin = (node: Node, containerId: string, byId: Map<string, Node>) => {
|
|
231
|
-
for (let parent = node.parentId; parent; parent = byId.get(parent)?.parentId) if (parent === containerId) return true;
|
|
232
|
-
return false;
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* L1 as EventCatalog's diagrams draw it: systems as cards with what's in them counted, and domains with nothing
|
|
237
|
-
* shown in them (no system, in them or their subdomains, or subdomain) as domain cards
|
|
238
|
-
*/
|
|
239
|
-
const asSystemsLevel = (
|
|
240
|
-
{ nodes, edges }: { nodes: Node[]; edges: Edge[] },
|
|
241
|
-
{ all, allEdges, containers }: { all: Node[]; allEdges: Edge[]; containers: ReturnType<typeof getContainers> }
|
|
242
|
-
) => {
|
|
243
|
-
const byId = new Map(all.map((node) => [node.id, node]));
|
|
244
|
-
const count = (id: string, matches: (node: Node) => boolean) =>
|
|
245
|
-
all.filter((node) => matches(node) && containers.get(node.id)?.system === id).length;
|
|
246
|
-
// The messages a system's services send and receive
|
|
247
|
-
const messagesOf = (id: string) => {
|
|
248
|
-
const services = new Set(all.filter((node) => containers.get(node.id)?.system === id).map((node) => node.id));
|
|
249
|
-
const messages = new Set<string>();
|
|
250
|
-
for (const edge of allEdges) {
|
|
251
|
-
const [inside, other] = services.has(edge.source) ? [edge.source, edge.target] : [edge.target, edge.source];
|
|
252
|
-
if (services.has(inside) && byId.get(other) && isMessage(byId.get(other)!)) messages.add(other);
|
|
253
|
-
}
|
|
254
|
-
return messages.size;
|
|
255
|
-
};
|
|
256
|
-
return {
|
|
257
|
-
edges,
|
|
258
|
-
nodes: nodes.map((node): Node => {
|
|
259
|
-
const { id, parentId, position, data } = node;
|
|
260
|
-
if (node.type === GROUP_TYPES.system)
|
|
261
|
-
return {
|
|
262
|
-
id,
|
|
263
|
-
type: 'system',
|
|
264
|
-
position,
|
|
265
|
-
...(parentId && { parentId }),
|
|
266
|
-
data: {
|
|
267
|
-
mode: 'full',
|
|
268
|
-
...data,
|
|
269
|
-
servicesCount: count(id, (child) => child.type === 'service' || child.type === 'agent'),
|
|
270
|
-
containersCount: count(id, (child) => child.type === 'data'),
|
|
271
|
-
messagesCount: messagesOf(id),
|
|
272
|
-
},
|
|
273
|
-
};
|
|
274
|
-
// A domain container stays one while something is shown in it (a system, or a subdomain shown as a card)
|
|
275
|
-
if (
|
|
276
|
-
node.type === GROUP_TYPES.domain &&
|
|
277
|
-
!all.some((other) => isSystem(other) && isWithin(other, id, byId)) &&
|
|
278
|
-
!nodes.some((other) => isWithin(other, id, byId))
|
|
279
|
-
)
|
|
280
|
-
return {
|
|
281
|
-
id,
|
|
282
|
-
type: 'context-domain',
|
|
283
|
-
position,
|
|
284
|
-
...(parentId && { parentId }),
|
|
285
|
-
data: {
|
|
286
|
-
mode: 'full',
|
|
287
|
-
...data,
|
|
288
|
-
subdomain: !!parentId,
|
|
289
|
-
systemsCount: 0,
|
|
290
|
-
servicesCount: all.filter((other) => other.type === 'service' && isWithin(other, id, byId)).length,
|
|
291
|
-
},
|
|
292
|
-
};
|
|
293
|
-
return node;
|
|
294
|
-
}),
|
|
295
|
-
};
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
/** L1: services folded into their system (or domain); actors, external systems and loose services stay */
|
|
299
|
-
const level1 = (nodes: Node[], edges: Edge[]) => {
|
|
300
|
-
const containers = getContainers(nodes, edges);
|
|
301
|
-
const collapsed = collapseGraph(
|
|
302
|
-
nodes,
|
|
303
|
-
edges,
|
|
304
|
-
(node) => {
|
|
305
|
-
const container = containers.get(node.id);
|
|
306
|
-
// Systems stay (inside their domain's container, if they're in one); everything else folds into its system or domain
|
|
307
|
-
if (isContainer(node) && (isSystem(node) || !container?.system)) return { kind: 'node', id: node.id };
|
|
308
|
-
if (container?.system) return { kind: 'node', id: container.system };
|
|
309
|
-
// Messages and channels connect the systems either side, even when they sit in a domain rather than a system
|
|
310
|
-
// (the visualiser has them outside its domains; catalog domains on a canvas hold their messages)
|
|
311
|
-
if (isCarrier(node)) return { kind: 'carrier' };
|
|
312
|
-
if (container?.domain) return { kind: 'node', id: container.domain };
|
|
313
|
-
if (node.type === 'data' || isWrittenOnCanvas(node.type)) return { kind: 'drop' };
|
|
314
|
-
return { kind: 'node', id: node.id };
|
|
315
|
-
},
|
|
316
|
-
{ relationships: true, foldsMessagesInto: isContainer }
|
|
317
|
-
);
|
|
318
|
-
return asSystemsLevel(collapsed, { all: nodes, allEdges: edges, containers });
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* The graph for a level (not laid out: positions come from the layout). Notes aren't in it: the ones added on the
|
|
323
|
-
* level are shown where they were put, over its layout.
|
|
324
|
-
*/
|
|
325
|
-
export const getLevelGraph = (nodes: Node[], edges: Edge[], level: Level) => {
|
|
326
|
-
if (level === 3) return { nodes, edges };
|
|
327
|
-
const withoutNotes = nodes.filter((node) => node.type !== 'note');
|
|
328
|
-
return sizedByLayout(level === 1 ? level1(withoutNotes, edges) : level2(withoutNotes, edges));
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
/** Why a level can't be shown for this canvas, if it can't */
|
|
332
|
-
export const getLevelUnavailableReason = (nodes: Node[], level: Level): string | undefined => {
|
|
333
|
-
// Like the visualiser: L1 needs systems, or domains to relate (one domain on its own has nothing to show)
|
|
334
|
-
if (level === 1 && !nodes.some(isSystem) && nodes.filter(isDomain).length < 2)
|
|
335
|
-
return 'Not available: add a system, or more than one domain, to see how they relate';
|
|
336
|
-
return undefined;
|
|
337
|
-
};
|
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The kinds of node people (and agents) can put on a canvas, and what's in their data. Shared by the browser and
|
|
3
|
-
* the server, so it has no UI in it: the icons for each type are in components/icons.ts.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export type NodeCategory = 'boundaries' | 'messages' | 'architecture' | 'other';
|
|
7
|
-
|
|
8
|
-
export const NODE_CATEGORIES: { id: NodeCategory; label: string }[] = [
|
|
9
|
-
{ id: 'boundaries', label: 'Boundaries' },
|
|
10
|
-
{ id: 'messages', label: 'Messages' },
|
|
11
|
-
{ id: 'architecture', label: 'Architecture' },
|
|
12
|
-
{ id: 'other', label: 'Other' },
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
/** The catalog collections whose resources can go on a canvas */
|
|
16
|
-
export const CATALOG_COLLECTIONS = [
|
|
17
|
-
'domains',
|
|
18
|
-
'systems',
|
|
19
|
-
'services',
|
|
20
|
-
'events',
|
|
21
|
-
'commands',
|
|
22
|
-
'queries',
|
|
23
|
-
'channels',
|
|
24
|
-
'containers',
|
|
25
|
-
] as const;
|
|
26
|
-
|
|
27
|
-
export type CatalogCollection = (typeof CATALOG_COLLECTIONS)[number];
|
|
28
|
-
export const isCatalogCollection = (collection: string): collection is CatalogCollection =>
|
|
29
|
-
(CATALOG_COLLECTIONS as readonly string[]).includes(collection);
|
|
30
|
-
|
|
31
|
-
/** Domains and systems as containers: boxes other nodes sit in */
|
|
32
|
-
export const GROUP_TYPES = { domain: 'domain-group', system: 'system-group' } as const;
|
|
33
|
-
export const isGroupType = (type?: string) => type === GROUP_TYPES.domain || type === GROUP_TYPES.system;
|
|
34
|
-
|
|
35
|
-
export type NodeDefinition = {
|
|
36
|
-
/** React Flow node type, matches the visualiser node component keys */
|
|
37
|
-
type: string;
|
|
38
|
-
label: string;
|
|
39
|
-
category: NodeCategory;
|
|
40
|
-
/** Not in the components list (still drawn, for canvases that have them) */
|
|
41
|
-
unlisted?: boolean;
|
|
42
|
-
/** Where the resource lives inside node.data (e.g. `service` for data.service). Undefined = data root */
|
|
43
|
-
resourceKey?: string;
|
|
44
|
-
/** Nodes that are sized on the canvas (containers, notes, text) start at this size */
|
|
45
|
-
defaultSize?: { width: number; height: number };
|
|
46
|
-
/** Only its width is set: its height follows what's in it (text) */
|
|
47
|
-
autoHeight?: boolean;
|
|
48
|
-
/**
|
|
49
|
-
* The first level of detail that shows it (it's on the more detailed ones too, see levels.ts): L1 domains, systems
|
|
50
|
-
* and actors, L2 services, data stores, text and the like, L3 messages and channels. Notes are on every level.
|
|
51
|
-
*/
|
|
52
|
-
firstLevel: 1 | 2 | 3;
|
|
53
|
-
createData: () => Record<string, unknown>;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const resource = (name: string, summary: string) => ({ name, version: '0.0.1', summary, owners: [] });
|
|
57
|
-
|
|
58
|
-
export const nodeDefinitions: NodeDefinition[] = [
|
|
59
|
-
{
|
|
60
|
-
type: GROUP_TYPES.domain,
|
|
61
|
-
firstLevel: 1,
|
|
62
|
-
label: 'Domain',
|
|
63
|
-
category: 'boundaries',
|
|
64
|
-
resourceKey: 'domain',
|
|
65
|
-
defaultSize: { width: 720, height: 460 },
|
|
66
|
-
createData: () => ({ domain: { name: 'New Domain', version: '0.0.1' } }),
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
type: GROUP_TYPES.system,
|
|
70
|
-
firstLevel: 1,
|
|
71
|
-
label: 'System',
|
|
72
|
-
category: 'boundaries',
|
|
73
|
-
resourceKey: 'system',
|
|
74
|
-
defaultSize: { width: 560, height: 360 },
|
|
75
|
-
createData: () => ({ system: { name: 'New System', version: '0.0.1' } }),
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
type: 'service',
|
|
79
|
-
firstLevel: 2,
|
|
80
|
-
category: 'architecture',
|
|
81
|
-
label: 'Service',
|
|
82
|
-
resourceKey: 'service',
|
|
83
|
-
createData: () => ({
|
|
84
|
-
mode: 'full',
|
|
85
|
-
service: { ...resource('New Service', 'Describe what this service does.'), sends: [], receives: [] },
|
|
86
|
-
}),
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
type: 'event',
|
|
90
|
-
firstLevel: 3,
|
|
91
|
-
category: 'messages',
|
|
92
|
-
label: 'Event',
|
|
93
|
-
resourceKey: 'message',
|
|
94
|
-
createData: () => ({ mode: 'full', message: resource('NewEvent', 'Something happened.') }),
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
type: 'command',
|
|
98
|
-
firstLevel: 3,
|
|
99
|
-
category: 'messages',
|
|
100
|
-
label: 'Command',
|
|
101
|
-
resourceKey: 'message',
|
|
102
|
-
createData: () => ({ mode: 'full', message: resource('NewCommand', 'Asks a service to do something.') }),
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
type: 'query',
|
|
106
|
-
firstLevel: 3,
|
|
107
|
-
category: 'messages',
|
|
108
|
-
label: 'Query',
|
|
109
|
-
resourceKey: 'message',
|
|
110
|
-
createData: () => ({ mode: 'full', message: resource('NewQuery', 'Asks for some information.') }),
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
type: 'channel',
|
|
114
|
-
firstLevel: 3,
|
|
115
|
-
category: 'architecture',
|
|
116
|
-
label: 'Channel',
|
|
117
|
-
resourceKey: 'channel',
|
|
118
|
-
createData: () => ({ mode: 'full', channel: { ...resource('new.channel', 'Where messages are routed.'), protocols: [] } }),
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
type: 'data',
|
|
122
|
-
firstLevel: 2,
|
|
123
|
-
category: 'architecture',
|
|
124
|
-
label: 'Data Store',
|
|
125
|
-
resourceKey: 'data',
|
|
126
|
-
createData: () => ({ mode: 'full', data: { ...resource('New Database', 'Stores data.'), container_type: 'database' } }),
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
type: 'view',
|
|
130
|
-
firstLevel: 2,
|
|
131
|
-
category: 'architecture',
|
|
132
|
-
label: 'View',
|
|
133
|
-
resourceKey: 'view',
|
|
134
|
-
createData: () => ({ mode: 'full', view: resource('New View', 'What the user sees.') }),
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
type: 'externalSystem',
|
|
138
|
-
firstLevel: 1,
|
|
139
|
-
category: 'architecture',
|
|
140
|
-
unlisted: true,
|
|
141
|
-
label: 'External System',
|
|
142
|
-
resourceKey: 'externalSystem',
|
|
143
|
-
createData: () => ({ mode: 'full', externalSystem: resource('External System', 'A third party system.') }),
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
type: 'agent',
|
|
147
|
-
firstLevel: 2,
|
|
148
|
-
category: 'architecture',
|
|
149
|
-
label: 'Agent',
|
|
150
|
-
resourceKey: 'agent',
|
|
151
|
-
createData: () => ({ mode: 'full', agent: { ...resource('New Agent', 'What this agent does.'), sends: [], receives: [] } }),
|
|
152
|
-
},
|
|
153
|
-
{
|
|
154
|
-
type: 'actor',
|
|
155
|
-
firstLevel: 1,
|
|
156
|
-
category: 'other',
|
|
157
|
-
label: 'Actor',
|
|
158
|
-
// Drawn like the visualiser's actors (by name only); the summary is for agents and the properties panel
|
|
159
|
-
createData: () => ({ mode: 'full', name: 'Customer', summary: '' }),
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
type: 'text',
|
|
163
|
-
firstLevel: 2,
|
|
164
|
-
category: 'other',
|
|
165
|
-
label: 'Text',
|
|
166
|
-
defaultSize: { width: 240, height: 40 },
|
|
167
|
-
autoHeight: true,
|
|
168
|
-
createData: () => ({ text: 'Add some text' }),
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
type: 'note',
|
|
172
|
-
firstLevel: 1,
|
|
173
|
-
category: 'other',
|
|
174
|
-
label: 'Sticky Note',
|
|
175
|
-
// About square, like a post-it (taller if more is written on it)
|
|
176
|
-
defaultSize: { width: 200, height: 176 },
|
|
177
|
-
autoHeight: true,
|
|
178
|
-
createData: () => ({ text: '', color: 'yellow' }),
|
|
179
|
-
},
|
|
180
|
-
];
|
|
181
|
-
|
|
182
|
-
/** The types people and agents can add (not the unlisted ones) */
|
|
183
|
-
export const COMPONENT_TYPES = nodeDefinitions
|
|
184
|
-
.filter((definition) => !definition.unlisted)
|
|
185
|
-
.map((definition) => definition.type) as [string, ...string[]];
|
|
186
|
-
|
|
187
|
-
const definitionsByType = new Map(nodeDefinitions.map((definition) => [definition.type, definition]));
|
|
188
|
-
|
|
189
|
-
// Node types that only come from the catalog (not in the components list)
|
|
190
|
-
const catalogNodeTypes: Record<string, { label: string; resourceKey: string; firstLevel: 1 | 2 | 3 }> = {
|
|
191
|
-
system: { label: 'System', resourceKey: 'system', firstLevel: 1 },
|
|
192
|
-
'context-domain': { label: 'Domain', resourceKey: 'domain', firstLevel: 1 },
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
export const getNodeDefinition = (type?: string) => (type ? definitionsByType.get(type) : undefined);
|
|
196
|
-
|
|
197
|
-
export const getNodeLabel = (type?: string) => getNodeDefinition(type)?.label ?? catalogNodeTypes[type ?? '']?.label ?? 'Node';
|
|
198
|
-
|
|
199
|
-
// About the size the visualiser's resource cards render at
|
|
200
|
-
export const DEFAULT_NODE_SIZE = { width: 240, height: 112 };
|
|
201
|
-
|
|
202
|
-
/** The size a new node of a type starts at (or, for cards, about the size it renders at) */
|
|
203
|
-
export const getNodeSize = (type?: string) => getNodeDefinition(type)?.defaultSize ?? DEFAULT_NODE_SIZE;
|
|
204
|
-
|
|
205
|
-
const getResourceKey = (type?: string) => getNodeDefinition(type)?.resourceKey ?? catalogNodeTypes[type ?? '']?.resourceKey;
|
|
206
|
-
|
|
207
|
-
/** The resource object for a node (e.g. data.service), or the data itself for flat nodes */
|
|
208
|
-
export const getResource = (type: string | undefined, data: Record<string, unknown>): Record<string, unknown> => {
|
|
209
|
-
const key = getResourceKey(type);
|
|
210
|
-
return key ? ((data[key] as Record<string, unknown>) ?? {}) : data;
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
/** Sticky notes sit on the canvas wherever they're put, never inside a container */
|
|
214
|
-
export const canGoInContainer = (type?: string) => type !== 'note';
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* The level (L1, L2 or L3) a sticky note was added on: it's only shown there. Notes from before levels had notes
|
|
218
|
-
* are on L3, the canvas people edit. Other nodes are on every level they fit in, so they have none.
|
|
219
|
-
*/
|
|
220
|
-
export const getNoteLevel = (node: { type?: string; data: Record<string, unknown> }): 1 | 2 | 3 | undefined =>
|
|
221
|
-
node.type === 'note' ? ((node.data.level as 1 | 2 | undefined) ?? 3) : undefined;
|
|
222
|
-
|
|
223
|
-
/** Whether a node is on a level's canvas as itself: everything on L3, but notes only on the level they were added on */
|
|
224
|
-
export const isOnLevel = (node: { type?: string; data: Record<string, unknown> }, level: 1 | 2 | 3) =>
|
|
225
|
-
node.type !== 'note' || getNoteLevel(node) === level;
|
|
226
|
-
|
|
227
|
-
/** Whether a level of detail shows a kind of node (so it can be added there, and stay there) */
|
|
228
|
-
export const isShownOnLevel = (type: string | undefined, level: 1 | 2 | 3) =>
|
|
229
|
-
level >= (getNodeDefinition(type)?.firstLevel ?? catalogNodeTypes[type ?? '']?.firstLevel ?? 3);
|
|
230
|
-
|
|
231
|
-
/** Notes and text: written on the canvas itself (their text is their data, not a resource with a name) */
|
|
232
|
-
export const isWrittenOnCanvas = (type?: string) => type === 'note' || type === 'text';
|
|
233
|
-
|
|
234
|
-
/** Whether a node's resource has a version (actors and notes don't) */
|
|
235
|
-
export const isVersioned = (type?: string) => !!getResourceKey(type);
|
|
236
|
-
|
|
237
|
-
/** Returns new node data with fields of the resource changed */
|
|
238
|
-
export const updateResource = (type: string | undefined, data: Record<string, unknown>, fields: Record<string, unknown>) => {
|
|
239
|
-
const key = getResourceKey(type);
|
|
240
|
-
return key ? { ...data, [key]: { ...getResource(type, data), ...fields } } : { ...data, ...fields };
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
/** A node's name, as people and agents see it */
|
|
244
|
-
export const getNodeName = (type: string | undefined, data: Record<string, unknown>, fallback = 'node') =>
|
|
245
|
-
String(getResource(type, data).name ?? data.text ?? fallback);
|