@eventcatalog/core 5.0.0-beta.7 → 5.0.0-beta.9
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/LICENSE +227 -128
- package/dist/analytics/analytics.cjs +18 -3
- package/dist/analytics/analytics.d.cts +1 -1
- package/dist/analytics/analytics.d.ts +1 -1
- package/dist/analytics/analytics.js +3 -2
- package/dist/analytics/log-build.cjs +70 -34
- package/dist/analytics/log-build.js +5 -4
- package/dist/analytics/telemetry.cjs +42 -0
- package/dist/analytics/telemetry.d.cts +20 -0
- package/dist/analytics/telemetry.d.ts +20 -0
- package/dist/analytics/telemetry.js +8 -0
- package/dist/{chunk-3VBB2ZMP.js → chunk-4CFZUHGG.js} +9 -3
- package/dist/{chunk-PIYJM642.js → chunk-AWOO63ME.js} +70 -44
- package/dist/chunk-CPW6G62V.js +17 -0
- package/dist/{chunk-MDWB5HLO.js → chunk-ENAG6UH7.js} +5 -0
- package/dist/{chunk-FARBIZ7Y.js → chunk-EUNQJO3J.js} +1 -1
- package/dist/{chunk-S7ODHVII.js → chunk-GAHKCOQM.js} +1 -1
- package/dist/{chunk-BOKAG5XC.js → chunk-JQMYHZWI.js} +58 -35
- package/dist/{chunk-SJJKWZYZ.js → chunk-TZVDMWQX.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +207 -123
- package/dist/eventcatalog.config.d.cts +55 -0
- package/dist/eventcatalog.config.d.ts +55 -0
- package/dist/eventcatalog.js +39 -19
- package/dist/features.cjs +8 -2
- package/dist/features.d.cts +3 -1
- package/dist/features.d.ts +3 -1
- package/dist/features.js +5 -3
- package/dist/federation/federate.js +4 -4
- 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/dist/utils/license-status.cjs +70 -44
- package/dist/utils/license-status.d.cts +14 -5
- package/dist/utils/license-status.d.ts +14 -5
- package/dist/utils/license-status.js +1 -1
- package/eventcatalog/integrations/studio-server.mjs +15 -3
- package/eventcatalog/src/components/Header.astro +3 -3
- package/eventcatalog/src/features/integrations/eventcatalog-features.ts +10 -2
- package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +9 -1
- package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +9 -2
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +70 -69
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +69 -69
- package/eventcatalog/src/features/mcp/mcp-server.ts +5 -3
- package/eventcatalog/src/features/studio/agent-choreography.ts +8 -5
- package/eventcatalog/src/features/studio/canvas-actions.ts +148 -21
- package/eventcatalog/src/features/studio/canvas-doc.ts +138 -9
- package/eventcatalog/src/features/studio/canvas-preview.ts +53 -0
- package/eventcatalog/src/features/studio/catalog-resources.ts +5 -2
- package/eventcatalog/src/features/studio/clipboard.ts +97 -0
- package/eventcatalog/src/features/studio/components/CanvasControls.tsx +4 -3
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +39 -22
- package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +117 -0
- package/eventcatalog/src/features/studio/components/Comments.tsx +345 -30
- package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +140 -76
- package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +144 -0
- package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +1 -1
- package/eventcatalog/src/features/studio/components/DragGhost.tsx +115 -0
- package/eventcatalog/src/features/studio/components/JoinForm.tsx +0 -24
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +131 -35
- package/eventcatalog/src/features/studio/components/Picture.tsx +21 -0
- package/eventcatalog/src/features/studio/components/Presence.tsx +2 -0
- package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +187 -50
- package/eventcatalog/src/features/studio/components/ShareDialog.tsx +260 -0
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +442 -40
- package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +198 -22
- package/eventcatalog/src/features/studio/components/icons.ts +6 -1
- package/eventcatalog/src/features/studio/components/status.ts +30 -1
- package/eventcatalog/src/features/studio/edges.ts +2 -0
- package/eventcatalog/src/features/studio/grouping.ts +95 -2
- package/eventcatalog/src/features/studio/hooks/presence-store.ts +5 -1
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +13 -3
- package/eventcatalog/src/features/studio/hooks/use-comments.ts +8 -1
- package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +7 -2
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +174 -27
- package/eventcatalog/src/features/studio/identity.ts +35 -0
- package/eventcatalog/src/features/studio/layout.ts +7 -4
- package/eventcatalog/src/features/studio/levels.ts +138 -72
- package/eventcatalog/src/features/studio/node-types.ts +35 -3
- package/eventcatalog/src/features/studio/pages/[id].astro +7 -2
- package/eventcatalog/src/features/studio/pages/index.astro +555 -2
- package/eventcatalog/src/features/studio/pages/new.astro +24 -0
- package/eventcatalog/src/features/studio/pages/wake.ts +15 -0
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +48 -13
- package/eventcatalog/src/features/studio/server/runtime.ts +193 -14
- package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
- package/eventcatalog/src/features/studio/server/storage.ts +110 -0
- package/eventcatalog/src/features/studio/studio.css +184 -0
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +1 -1
- package/eventcatalog/src/utils/feature.ts +7 -3
- package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
- package/package.json +3 -3
- package/dist/{chunk-AYNZXUB5.js → chunk-ZEY2XUJN.js} +7 -7
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { MarkerType, type Edge, type Node } from '@xyflow/react';
|
|
2
2
|
import { getMessagesLabel } from '@eventcatalog/visualiser/layout';
|
|
3
|
-
import {
|
|
4
|
-
import { GROUP_TYPES, isGroupType } from './node-types';
|
|
3
|
+
import { MESSAGE_COLLECTIONS } from './edges';
|
|
4
|
+
import { GROUP_TYPES, isGroupType, isWrittenOnCanvas } from './node-types';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Levels of detail, the same as EventCatalog's diagrams:
|
|
@@ -23,22 +23,43 @@ export const LEVELS: { level: Level; description: string }[] = [
|
|
|
23
23
|
// Domains and systems: as cards (what they contain is connected with "contains") or containers (it's inside)
|
|
24
24
|
const CONTAINER_TYPES = new Set(['system', 'context-domain', GROUP_TYPES.system, GROUP_TYPES.domain]);
|
|
25
25
|
const isSystem = (node: Node) => node.type === 'system' || node.type === GROUP_TYPES.system;
|
|
26
|
-
const
|
|
26
|
+
const isDomain = (node: Node) => node.type === 'context-domain' || node.type === GROUP_TYPES.domain;
|
|
27
27
|
|
|
28
28
|
const isMessage = (node: Node) => !!MESSAGE_COLLECTIONS[node.type ?? ''];
|
|
29
29
|
const isCarrier = (node: Node) => isMessage(node) || node.type === 'channel';
|
|
30
30
|
const isContainer = (node: Node) => CONTAINER_TYPES.has(node.type ?? '');
|
|
31
31
|
const messageName = (node: Node) => String((node.data as { message?: { name?: string } }).message?.name ?? node.id);
|
|
32
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
|
|
35
|
+
const EDGE_STROKE = 'var(--ec-edge-stroke, #6b7280)';
|
|
36
|
+
const MUTED = 'rgb(var(--ec-page-text-muted))';
|
|
37
|
+
const FOLDED_EDGE = {
|
|
38
|
+
type: 'smoothstep',
|
|
39
|
+
style: { strokeWidth: 1, stroke: EDGE_STROKE },
|
|
40
|
+
markerEnd: { type: MarkerType.ArrowClosed, width: 20, height: 20 },
|
|
41
|
+
} satisfies Partial<Edge>;
|
|
42
|
+
const BRIDGED_EDGE = {
|
|
43
|
+
type: 'smoothstep',
|
|
44
|
+
style: { strokeWidth: 1.5, stroke: MUTED, strokeDasharray: '5 5' },
|
|
45
|
+
markerEnd: { type: MarkerType.ArrowClosed, width: 20, height: 20, color: MUTED },
|
|
46
|
+
} satisfies Partial<Edge>;
|
|
47
|
+
|
|
33
48
|
/** What a node becomes at a level: kept (as itself or another node), carried through (messages, channels) or dropped */
|
|
34
49
|
type Representative = { kind: 'node'; id: string } | { kind: 'carrier' } | { kind: 'drop' };
|
|
35
50
|
|
|
36
51
|
/**
|
|
37
|
-
* Collapse a graph: each node is kept, replaced by another node (e.g. a service by its system),
|
|
38
|
-
*
|
|
39
|
-
*
|
|
52
|
+
* Collapse a graph: each node is kept, replaced by another node (e.g. a service by its system), carried through
|
|
53
|
+
* (messages and channels: the nodes either side get connected directly, "bridged") or dropped. Edges between two
|
|
54
|
+
* nodes that are kept as they are stay as they were (`keepDirect`, for L2); the rest are merged per pair and kind
|
|
55
|
+
* (folded or bridged), labelled with the messages they carry.
|
|
40
56
|
*/
|
|
41
|
-
const collapseGraph = (
|
|
57
|
+
const collapseGraph = (
|
|
58
|
+
nodes: Node[],
|
|
59
|
+
edges: Edge[],
|
|
60
|
+
representativeOf: (node: Node) => Representative,
|
|
61
|
+
{ keepDirect = false }: { keepDirect?: boolean } = {}
|
|
62
|
+
) => {
|
|
42
63
|
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
43
64
|
// Whether one node is inside another (e.g. a system in its domain): they're not connected, one holds the other
|
|
44
65
|
const isInside = (id: string, containerId: string) => {
|
|
@@ -49,8 +70,9 @@ const collapseGraph = (nodes: Node[], edges: Edge[], representativeOf: (node: No
|
|
|
49
70
|
const outgoing = new Map<string, Edge[]>();
|
|
50
71
|
edges.forEach((edge) => outgoing.set(edge.source, [...(outgoing.get(edge.source) ?? []), edge]));
|
|
51
72
|
|
|
52
|
-
type Merged = { source: string; target: string; messages: Map<string, string>; labels: string[] };
|
|
73
|
+
type Merged = { source: string; target: string; bridged: boolean; messages: Map<string, string>; labels: string[] };
|
|
53
74
|
const merged = new Map<string, Merged>();
|
|
75
|
+
const kept: Edge[] = [];
|
|
54
76
|
const addMessage = (messages: Map<string, string>, node: Node) => {
|
|
55
77
|
if (isMessage(node)) messages.set(messageName(node), MESSAGE_COLLECTIONS[node.type!]);
|
|
56
78
|
};
|
|
@@ -58,8 +80,10 @@ const collapseGraph = (nodes: Node[], edges: Edge[], representativeOf: (node: No
|
|
|
58
80
|
edges.forEach((edge) => {
|
|
59
81
|
const from = representatives.get(edge.source);
|
|
60
82
|
if (from?.kind !== 'node') return;
|
|
83
|
+
const to = representatives.get(edge.target);
|
|
84
|
+
if (keepDirect && from.id === edge.source && to?.kind === 'node' && to.id === edge.target) return kept.push(edge);
|
|
61
85
|
// Follow the edge through carried nodes to the node(s) it reaches, collecting the messages on the way
|
|
62
|
-
const stack = [{ id: edge.target, messages: new Map<string, string>(), labels: [String(edge.label ?? '')] }];
|
|
86
|
+
const stack = [{ id: edge.target, messages: new Map<string, string>(), labels: [String(edge.label ?? '')], bridged: false }];
|
|
63
87
|
const visited = new Set<string>();
|
|
64
88
|
addMessage(stack[0].messages, byId.get(edge.source)!);
|
|
65
89
|
while (stack.length) {
|
|
@@ -67,19 +91,27 @@ const collapseGraph = (nodes: Node[], edges: Edge[], representativeOf: (node: No
|
|
|
67
91
|
if (visited.has(current.id)) continue;
|
|
68
92
|
visited.add(current.id);
|
|
69
93
|
const node = byId.get(current.id);
|
|
70
|
-
const
|
|
71
|
-
if (!node || !
|
|
94
|
+
const reached = representatives.get(current.id);
|
|
95
|
+
if (!node || !reached || reached.kind === 'drop') continue;
|
|
72
96
|
const messages = new Map(current.messages);
|
|
73
97
|
addMessage(messages, node);
|
|
74
|
-
if (
|
|
75
|
-
(outgoing.get(current.id) ?? []).forEach((next) =>
|
|
98
|
+
if (reached.kind === 'carrier') {
|
|
99
|
+
(outgoing.get(current.id) ?? []).forEach((next) =>
|
|
100
|
+
stack.push({ id: next.target, messages, labels: [] as string[], bridged: true })
|
|
101
|
+
);
|
|
76
102
|
continue;
|
|
77
103
|
}
|
|
78
|
-
// Edges inside what was collapsed disappear, and so do edges to what a node is in (e.g. a
|
|
79
|
-
//
|
|
80
|
-
if (from.id ===
|
|
81
|
-
const key = `${from.id}
|
|
82
|
-
const entry: Merged = merged.get(key) ?? {
|
|
104
|
+
// Edges inside what was collapsed disappear, and so do edges to what a node is in (e.g. a service loose in a
|
|
105
|
+
// domain, folded into it, and a system in the same domain)
|
|
106
|
+
if (from.id === reached.id || isInside(from.id, reached.id) || isInside(reached.id, from.id)) continue;
|
|
107
|
+
const key = `${current.bridged ? 'bridged' : 'level'}-${from.id}-${reached.id}`;
|
|
108
|
+
const entry: Merged = merged.get(key) ?? {
|
|
109
|
+
source: from.id,
|
|
110
|
+
target: reached.id,
|
|
111
|
+
bridged: current.bridged,
|
|
112
|
+
messages: new Map(),
|
|
113
|
+
labels: [],
|
|
114
|
+
};
|
|
83
115
|
messages.forEach((collection, name) => entry.messages.set(name, collection));
|
|
84
116
|
entry.labels.push(...current.labels.filter(Boolean));
|
|
85
117
|
merged.set(key, entry);
|
|
@@ -87,21 +119,19 @@ const collapseGraph = (nodes: Node[], edges: Edge[], representativeOf: (node: No
|
|
|
87
119
|
});
|
|
88
120
|
|
|
89
121
|
const keptIds = new Set([...representatives.values()].flatMap((rep) => (rep.kind === 'node' ? [rep.id] : [])));
|
|
122
|
+
// Actors' connections are relationships, labelled as they were (like the visualiser's context relationships)
|
|
123
|
+
const isActor = (id: string) => byId.get(id)?.type === 'actor';
|
|
90
124
|
return {
|
|
91
125
|
nodes: nodes.filter((node) => keptIds.has(node.id)),
|
|
92
|
-
edges: [
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
target,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
markerEnd: EDGE_MARKER,
|
|
102
|
-
data: collection ? { message: { collection } } : {},
|
|
103
|
-
};
|
|
104
|
-
}),
|
|
126
|
+
edges: [
|
|
127
|
+
...kept,
|
|
128
|
+
...[...merged.entries()].map(([id, { source, target, bridged, messages, labels }]): Edge => {
|
|
129
|
+
const label =
|
|
130
|
+
getMessagesLabel(messages) ??
|
|
131
|
+
(!bridged && (isActor(source) || isActor(target)) && labels.length ? [...new Set(labels)].join(', ') : undefined);
|
|
132
|
+
return { id, source, target, ...(bridged ? BRIDGED_EDGE : FOLDED_EDGE), ...(label && { label }) };
|
|
133
|
+
}),
|
|
134
|
+
],
|
|
105
135
|
};
|
|
106
136
|
};
|
|
107
137
|
|
|
@@ -135,51 +165,83 @@ const getContainers = (nodes: Node[], edges: Edge[]) => {
|
|
|
135
165
|
};
|
|
136
166
|
|
|
137
167
|
/**
|
|
138
|
-
* Containers
|
|
139
|
-
*
|
|
168
|
+
* Containers are sized by the layout: to fit what's in them, or (empty) the size the visualiser lays empty groups
|
|
169
|
+
* out at. Same ids, so switching levels grows and shrinks them.
|
|
140
170
|
*/
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (parents.has(node.id)) return { ...node, width: undefined, height: undefined, measured: undefined };
|
|
148
|
-
return { ...node, ...COLLAPSED_GROUP_SIZE, measured: undefined };
|
|
149
|
-
}),
|
|
150
|
-
};
|
|
151
|
-
};
|
|
171
|
+
const sizedByLayout = ({ nodes, edges }: { nodes: Node[]; edges: Edge[] }) => ({
|
|
172
|
+
edges,
|
|
173
|
+
nodes: nodes.map((node) =>
|
|
174
|
+
isGroupType(node.type) ? { ...node, width: undefined, height: undefined, measured: undefined } : node
|
|
175
|
+
),
|
|
176
|
+
});
|
|
152
177
|
|
|
153
178
|
/** L2: messages and channels hidden, the nodes either side connected directly */
|
|
154
179
|
const level2 = (nodes: Node[], edges: Edge[]) =>
|
|
155
|
-
collapseGraph(nodes, edges, (node) => (isCarrier(node) ? { kind: 'carrier' } : { kind: 'node', id: node.id })
|
|
180
|
+
collapseGraph(nodes, edges, (node) => (isCarrier(node) ? { kind: 'carrier' } : { kind: 'node', id: node.id }), {
|
|
181
|
+
keepDirect: true,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
/** Whether a node is inside a container, at any depth */
|
|
185
|
+
const isWithin = (node: Node, containerId: string, byId: Map<string, Node>) => {
|
|
186
|
+
for (let parent = node.parentId; parent; parent = byId.get(parent)?.parentId) if (parent === containerId) return true;
|
|
187
|
+
return false;
|
|
188
|
+
};
|
|
156
189
|
|
|
157
190
|
/**
|
|
158
|
-
* L1 as EventCatalog's diagrams draw it: systems as cards
|
|
159
|
-
*
|
|
191
|
+
* L1 as EventCatalog's diagrams draw it: systems as cards with what's in them counted, and domains with no system in
|
|
192
|
+
* them (or in their subdomains) as domain cards
|
|
160
193
|
*/
|
|
161
194
|
const asSystemsLevel = (
|
|
162
195
|
{ nodes, edges }: { nodes: Node[]; edges: Edge[] },
|
|
163
|
-
{ all, containers }: { all: Node[]; containers: ReturnType<typeof getContainers> }
|
|
196
|
+
{ all, allEdges, containers }: { all: Node[]; allEdges: Edge[]; containers: ReturnType<typeof getContainers> }
|
|
164
197
|
) => {
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
198
|
+
const byId = new Map(all.map((node) => [node.id, node]));
|
|
199
|
+
const count = (id: string, matches: (node: Node) => boolean) =>
|
|
200
|
+
all.filter((node) => matches(node) && containers.get(node.id)?.system === id).length;
|
|
201
|
+
// The messages a system's services send and receive
|
|
202
|
+
const messagesOf = (id: string) => {
|
|
203
|
+
const services = new Set(all.filter((node) => containers.get(node.id)?.system === id).map((node) => node.id));
|
|
204
|
+
const messages = new Set<string>();
|
|
205
|
+
for (const edge of allEdges) {
|
|
206
|
+
const [inside, other] = services.has(edge.source) ? [edge.source, edge.target] : [edge.target, edge.source];
|
|
207
|
+
if (services.has(inside) && byId.get(other) && isMessage(byId.get(other)!)) messages.add(other);
|
|
208
|
+
}
|
|
209
|
+
return messages.size;
|
|
210
|
+
};
|
|
170
211
|
return {
|
|
212
|
+
edges,
|
|
171
213
|
nodes: nodes.map((node): Node => {
|
|
172
|
-
if (node.type !== GROUP_TYPES.system) return node;
|
|
173
214
|
const { id, parentId, position, data } = node;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
215
|
+
if (node.type === GROUP_TYPES.system)
|
|
216
|
+
return {
|
|
217
|
+
id,
|
|
218
|
+
type: 'system',
|
|
219
|
+
position,
|
|
220
|
+
...(parentId && { parentId }),
|
|
221
|
+
data: {
|
|
222
|
+
mode: 'full',
|
|
223
|
+
...data,
|
|
224
|
+
servicesCount: count(id, (child) => child.type === 'service' || child.type === 'agent'),
|
|
225
|
+
containersCount: count(id, (child) => child.type === 'data'),
|
|
226
|
+
messagesCount: messagesOf(id),
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
if (node.type === GROUP_TYPES.domain && !all.some((other) => isSystem(other) && isWithin(other, id, byId)))
|
|
230
|
+
return {
|
|
231
|
+
id,
|
|
232
|
+
type: 'context-domain',
|
|
233
|
+
position,
|
|
234
|
+
...(parentId && { parentId }),
|
|
235
|
+
data: {
|
|
236
|
+
mode: 'full',
|
|
237
|
+
...data,
|
|
238
|
+
subdomain: !!parentId,
|
|
239
|
+
systemsCount: 0,
|
|
240
|
+
servicesCount: all.filter((other) => other.type === 'service' && isWithin(other, id, byId)).length,
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
return node;
|
|
181
244
|
}),
|
|
182
|
-
edges: edges.map(({ type: _, data: __, ...edge }): Edge => ({ ...edge, type: 'default' })),
|
|
183
245
|
};
|
|
184
246
|
};
|
|
185
247
|
|
|
@@ -192,25 +254,29 @@ const level1 = (nodes: Node[], edges: Edge[]) => {
|
|
|
192
254
|
if (isContainer(node) && (isSystem(node) || !container?.system)) return { kind: 'node', id: node.id };
|
|
193
255
|
if (container?.system) return { kind: 'node', id: container.system };
|
|
194
256
|
// Messages and channels connect the systems either side, even when they sit in a domain rather than a system
|
|
257
|
+
// (the visualiser has them outside its domains; catalog domains on a canvas hold their messages)
|
|
195
258
|
if (isCarrier(node)) return { kind: 'carrier' };
|
|
196
259
|
if (container?.domain) return { kind: 'node', id: container.domain };
|
|
197
|
-
if (node.type === 'data' || node.type
|
|
260
|
+
if (node.type === 'data' || isWrittenOnCanvas(node.type)) return { kind: 'drop' };
|
|
198
261
|
return { kind: 'node', id: node.id };
|
|
199
262
|
});
|
|
200
|
-
return asSystemsLevel(collapsed, { all: nodes, containers });
|
|
263
|
+
return asSystemsLevel(collapsed, { all: nodes, allEdges: edges, containers });
|
|
201
264
|
};
|
|
202
265
|
|
|
203
|
-
/**
|
|
266
|
+
/**
|
|
267
|
+
* The graph for a level (not laid out: positions come from the layout). Notes aren't in it: the ones added on the
|
|
268
|
+
* level are shown where they were put, over its layout.
|
|
269
|
+
*/
|
|
204
270
|
export const getLevelGraph = (nodes: Node[], edges: Edge[], level: Level) => {
|
|
205
|
-
if (level ===
|
|
206
|
-
|
|
207
|
-
return
|
|
271
|
+
if (level === 3) return { nodes, edges };
|
|
272
|
+
const withoutNotes = nodes.filter((node) => node.type !== 'note');
|
|
273
|
+
return sizedByLayout(level === 1 ? level1(withoutNotes, edges) : level2(withoutNotes, edges));
|
|
208
274
|
};
|
|
209
275
|
|
|
210
276
|
/** Why a level can't be shown for this canvas, if it can't */
|
|
211
277
|
export const getLevelUnavailableReason = (nodes: Node[], level: Level): string | undefined => {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
278
|
+
// Like the visualiser: L1 needs systems, or domains to relate (one domain on its own has nothing to show)
|
|
279
|
+
if (level === 1 && !nodes.some(isSystem) && nodes.filter(isDomain).length < 2)
|
|
280
|
+
return 'Not available: add a system, or more than one domain, to see how they relate';
|
|
215
281
|
return undefined;
|
|
216
282
|
};
|
|
@@ -25,8 +25,10 @@ export type NodeDefinition = {
|
|
|
25
25
|
unlisted?: boolean;
|
|
26
26
|
/** Where the resource lives inside node.data (e.g. `service` for data.service). Undefined = data root */
|
|
27
27
|
resourceKey?: string;
|
|
28
|
-
/** Nodes that are sized on the canvas (containers, notes) start at this size */
|
|
28
|
+
/** Nodes that are sized on the canvas (containers, notes, text) start at this size */
|
|
29
29
|
defaultSize?: { width: number; height: number };
|
|
30
|
+
/** Only its width is set: its height follows what's in it (text) */
|
|
31
|
+
autoHeight?: boolean;
|
|
30
32
|
createData: () => Record<string, unknown>;
|
|
31
33
|
};
|
|
32
34
|
|
|
@@ -122,12 +124,22 @@ export const nodeDefinitions: NodeDefinition[] = [
|
|
|
122
124
|
label: 'Actor',
|
|
123
125
|
createData: () => ({ mode: 'full', name: 'Customer', summary: 'A person using the system.' }),
|
|
124
126
|
},
|
|
127
|
+
{
|
|
128
|
+
type: 'text',
|
|
129
|
+
category: 'other',
|
|
130
|
+
label: 'Text',
|
|
131
|
+
defaultSize: { width: 240, height: 40 },
|
|
132
|
+
autoHeight: true,
|
|
133
|
+
createData: () => ({ text: 'Add some text' }),
|
|
134
|
+
},
|
|
125
135
|
{
|
|
126
136
|
type: 'note',
|
|
127
137
|
category: 'other',
|
|
128
138
|
label: 'Sticky Note',
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
// About square, like a post-it (taller if more is written on it)
|
|
140
|
+
defaultSize: { width: 200, height: 176 },
|
|
141
|
+
autoHeight: true,
|
|
142
|
+
createData: () => ({ text: '', color: 'yellow' }),
|
|
131
143
|
},
|
|
132
144
|
];
|
|
133
145
|
|
|
@@ -162,6 +174,26 @@ export const getResource = (type: string | undefined, data: Record<string, unkno
|
|
|
162
174
|
return key ? ((data[key] as Record<string, unknown>) ?? {}) : data;
|
|
163
175
|
};
|
|
164
176
|
|
|
177
|
+
/** Sticky notes sit on the canvas wherever they're put, never inside a container */
|
|
178
|
+
export const canGoInContainer = (type?: string) => type !== 'note';
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The level (L1, L2 or L3) a sticky note was added on: it's only shown there. Notes from before levels had notes
|
|
182
|
+
* are on L3, the canvas people edit. Other nodes are on every level they fit in, so they have none.
|
|
183
|
+
*/
|
|
184
|
+
export const getNoteLevel = (node: { type?: string; data: Record<string, unknown> }): 1 | 2 | 3 | undefined =>
|
|
185
|
+
node.type === 'note' ? ((node.data.level as 1 | 2 | undefined) ?? 3) : undefined;
|
|
186
|
+
|
|
187
|
+
/** Whether a node is on a level's canvas as itself: everything on L3, but notes only on the level they were added on */
|
|
188
|
+
export const isOnLevel = (node: { type?: string; data: Record<string, unknown> }, level: 1 | 2 | 3) =>
|
|
189
|
+
node.type !== 'note' || getNoteLevel(node) === level;
|
|
190
|
+
|
|
191
|
+
/** Notes and text: written on the canvas itself (their text is their data, not a resource with a name) */
|
|
192
|
+
export const isWrittenOnCanvas = (type?: string) => type === 'note' || type === 'text';
|
|
193
|
+
|
|
194
|
+
/** Whether a node's resource has a version (actors and notes don't) */
|
|
195
|
+
export const isVersioned = (type?: string) => !!getResourceKey(type);
|
|
196
|
+
|
|
165
197
|
/** Returns new node data with fields of the resource changed */
|
|
166
198
|
export const updateResource = (type: string | undefined, data: Record<string, unknown>, fields: Record<string, unknown>) => {
|
|
167
199
|
const key = getResourceKey(type);
|
|
@@ -6,7 +6,8 @@ import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
|
|
|
6
6
|
import { buildUrl } from '@utils/url-builder';
|
|
7
7
|
import StudioDesigner from '../components/StudioDesigner';
|
|
8
8
|
import { getCatalogResources } from '../catalog-resources';
|
|
9
|
-
import { STUDIO_SOCKET_PATH } from '../server/runtime';
|
|
9
|
+
import { startStudio, STUDIO_SOCKET_PATH } from '../server/runtime';
|
|
10
|
+
import { getSignedInUser, isSignedIn } from '../server/sign-in';
|
|
10
11
|
|
|
11
12
|
export const prerender = false;
|
|
12
13
|
|
|
@@ -17,6 +18,8 @@ if (!id || !/^[0-9a-f-]{36}$/i.test(id)) {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
const { resources, relations } = await getCatalogResources();
|
|
21
|
+
const keptInMemory = (await startStudio({ isSignedIn })).storage.type === 'memory';
|
|
22
|
+
const signedInAs = getSignedInUser(Astro.locals);
|
|
20
23
|
---
|
|
21
24
|
|
|
22
25
|
<VerticalSideBarLayout title="Studio - EventCatalog" showNestedSideBar={false}>
|
|
@@ -28,9 +31,11 @@ const { resources, relations } = await getCatalogResources();
|
|
|
28
31
|
client:only="react"
|
|
29
32
|
canvasId={id}
|
|
30
33
|
socketPath={buildUrl(STUDIO_SOCKET_PATH, true)}
|
|
31
|
-
newCanvasUrl={buildUrl('/studio')}
|
|
34
|
+
newCanvasUrl={`${buildUrl('/studio')}?new=1`}
|
|
32
35
|
mcpUrl={buildUrl('/docs/mcp', true)}
|
|
33
36
|
relayScriptPath={buildUrl('/webmcp-relay/embed.js', true)}
|
|
37
|
+
keptInMemory={keptInMemory}
|
|
38
|
+
signedInAs={signedInAs}
|
|
34
39
|
resources={resources}
|
|
35
40
|
relations={relations}
|
|
36
41
|
/>
|