@eventcatalog/core 5.0.0-beta.7 → 5.0.0-beta.8

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.
Files changed (85) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +3 -1
  4. package/dist/analytics/log-build.js +4 -4
  5. package/dist/{chunk-BOKAG5XC.js → chunk-4W55ZOOS.js} +2 -2
  6. package/dist/{chunk-PIYJM642.js → chunk-AWOO63ME.js} +70 -44
  7. package/dist/{chunk-3VBB2ZMP.js → chunk-C6T2AAFR.js} +1 -1
  8. package/dist/{chunk-MDWB5HLO.js → chunk-ENAG6UH7.js} +5 -0
  9. package/dist/{chunk-S7ODHVII.js → chunk-PU25TOOJ.js} +1 -1
  10. package/dist/{chunk-FARBIZ7Y.js → chunk-RDIZJ7UT.js} +1 -1
  11. package/dist/{chunk-SJJKWZYZ.js → chunk-T4G3VVZ4.js} +1 -1
  12. package/dist/constants.cjs +1 -1
  13. package/dist/constants.js +1 -1
  14. package/dist/eventcatalog.cjs +138 -90
  15. package/dist/eventcatalog.config.d.cts +47 -0
  16. package/dist/eventcatalog.config.d.ts +47 -0
  17. package/dist/eventcatalog.js +30 -11
  18. package/dist/features.cjs +8 -2
  19. package/dist/features.d.cts +3 -1
  20. package/dist/features.d.ts +3 -1
  21. package/dist/features.js +5 -3
  22. package/dist/generate.cjs +1 -1
  23. package/dist/generate.js +3 -3
  24. package/dist/utils/cli-logger.cjs +1 -1
  25. package/dist/utils/cli-logger.js +2 -2
  26. package/dist/utils/license-status.cjs +70 -44
  27. package/dist/utils/license-status.d.cts +14 -5
  28. package/dist/utils/license-status.d.ts +14 -5
  29. package/dist/utils/license-status.js +1 -1
  30. package/eventcatalog/integrations/studio-server.mjs +15 -3
  31. package/eventcatalog/src/components/Header.astro +3 -3
  32. package/eventcatalog/src/features/integrations/eventcatalog-features.ts +10 -2
  33. package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +9 -1
  34. package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +9 -2
  35. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +70 -69
  36. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +69 -69
  37. package/eventcatalog/src/features/mcp/mcp-server.ts +5 -3
  38. package/eventcatalog/src/features/studio/agent-choreography.ts +8 -5
  39. package/eventcatalog/src/features/studio/canvas-actions.ts +148 -21
  40. package/eventcatalog/src/features/studio/canvas-doc.ts +138 -9
  41. package/eventcatalog/src/features/studio/canvas-preview.ts +53 -0
  42. package/eventcatalog/src/features/studio/catalog-resources.ts +5 -2
  43. package/eventcatalog/src/features/studio/clipboard.ts +97 -0
  44. package/eventcatalog/src/features/studio/components/CanvasControls.tsx +4 -3
  45. package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +39 -22
  46. package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +117 -0
  47. package/eventcatalog/src/features/studio/components/Comments.tsx +345 -30
  48. package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +140 -76
  49. package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +144 -0
  50. package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +1 -1
  51. package/eventcatalog/src/features/studio/components/DragGhost.tsx +115 -0
  52. package/eventcatalog/src/features/studio/components/JoinForm.tsx +0 -24
  53. package/eventcatalog/src/features/studio/components/LeftPanel.tsx +131 -35
  54. package/eventcatalog/src/features/studio/components/Picture.tsx +21 -0
  55. package/eventcatalog/src/features/studio/components/Presence.tsx +2 -0
  56. package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +187 -50
  57. package/eventcatalog/src/features/studio/components/ShareDialog.tsx +260 -0
  58. package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +442 -40
  59. package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +198 -22
  60. package/eventcatalog/src/features/studio/components/icons.ts +6 -1
  61. package/eventcatalog/src/features/studio/components/status.ts +30 -1
  62. package/eventcatalog/src/features/studio/edges.ts +2 -0
  63. package/eventcatalog/src/features/studio/grouping.ts +95 -2
  64. package/eventcatalog/src/features/studio/hooks/presence-store.ts +5 -1
  65. package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +13 -3
  66. package/eventcatalog/src/features/studio/hooks/use-comments.ts +8 -1
  67. package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +7 -2
  68. package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +174 -27
  69. package/eventcatalog/src/features/studio/identity.ts +35 -0
  70. package/eventcatalog/src/features/studio/layout.ts +7 -4
  71. package/eventcatalog/src/features/studio/levels.ts +138 -72
  72. package/eventcatalog/src/features/studio/node-types.ts +35 -3
  73. package/eventcatalog/src/features/studio/pages/[id].astro +7 -2
  74. package/eventcatalog/src/features/studio/pages/index.astro +555 -2
  75. package/eventcatalog/src/features/studio/pages/new.astro +24 -0
  76. package/eventcatalog/src/features/studio/pages/wake.ts +15 -0
  77. package/eventcatalog/src/features/studio/server/canvas-mcp.ts +48 -13
  78. package/eventcatalog/src/features/studio/server/runtime.ts +193 -14
  79. package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
  80. package/eventcatalog/src/features/studio/server/storage.ts +110 -0
  81. package/eventcatalog/src/features/studio/studio.css +184 -0
  82. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +1 -1
  83. package/eventcatalog/src/utils/feature.ts +7 -3
  84. package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
  85. package/package.json +4 -4
@@ -1,7 +1,7 @@
1
- import type { Edge, Node } from '@xyflow/react';
1
+ import { MarkerType, type Edge, type Node } from '@xyflow/react';
2
2
  import { getMessagesLabel } from '@eventcatalog/visualiser/layout';
3
- import { EDGE_MARKER, MESSAGE_COLLECTIONS } from './edges';
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 COLLAPSED_GROUP_SIZE = { width: 280, height: 110 };
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
- * carried through (messages and channels: the nodes either side get connected directly) or dropped.
39
- * Edges between the same two nodes are merged, labelled with the messages they carry.
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 = (nodes: Node[], edges: Edge[], representativeOf: (node: Node) => Representative) => {
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 to = representatives.get(current.id);
71
- if (!node || !to || to.kind === 'drop') continue;
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 (to.kind === 'carrier') {
75
- (outgoing.get(current.id) ?? []).forEach((next) => stack.push({ id: next.target, messages, labels: [] as string[] }));
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 system and the
79
- // messages left loose in its domain, which fold into the domain)
80
- if (from.id === to.id || isInside(from.id, to.id) || isInside(to.id, from.id)) continue;
81
- const key = `${from.id}->${to.id}`;
82
- const entry: Merged = merged.get(key) ?? { source: from.id, target: to.id, messages: new Map(), labels: [] };
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: [...merged.values()].map(({ source, target, messages, labels }): Edge => {
93
- const collections = new Set(messages.values());
94
- const collection = collections.size === 1 ? [...collections][0] : undefined;
95
- return {
96
- id: `level-${source}-${target}`,
97
- source,
98
- target,
99
- type: collection ? 'animated' : 'smoothstep',
100
- label: getMessagesLabel(messages) ?? [...new Set(labels)].join(', '),
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 left with nothing inside them at a level become compact boxes (same id, so switching levels
139
- * grows and shrinks them); containers with something inside are sized to fit by the layout
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 compactEmptyGroups = ({ nodes, edges }: { nodes: Node[]; edges: Edge[] }) => {
142
- const parents = new Set(nodes.map((node) => node.parentId).filter(Boolean));
143
- return {
144
- edges,
145
- nodes: nodes.map((node) => {
146
- if (!isGroupType(node.type)) return node;
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 (with how many services they hold), connected by plain
159
- * edges labelled with the messages between them
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 servicesIn = new Map<string, number>();
166
- for (const node of all) {
167
- const system = node.type === 'service' ? containers.get(node.id)?.system : undefined;
168
- if (system) servicesIn.set(system, (servicesIn.get(system) ?? 0) + 1);
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
- return {
175
- id,
176
- type: 'system',
177
- position,
178
- ...(parentId && { parentId }),
179
- data: { mode: 'full', ...data, servicesCount: servicesIn.get(id) ?? 0 },
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 === 'note') return { kind: 'drop' };
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
- /** The graph for a level (not laid out: positions come from the layout) */
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 === 1) return compactEmptyGroups(level1(nodes, edges));
206
- if (level === 2) return compactEmptyGroups(level2(nodes, edges));
207
- return { nodes, edges };
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
- if (level === 1 && !nodes.some(isContainer))
213
- return 'Not available: add a domain or system from the catalog to see how they relate';
214
- if (level === 2 && !nodes.some(isCarrier)) return 'Not available: there are no messages or channels to hide';
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
- defaultSize: { width: 200, height: 160 },
130
- createData: () => ({ text: 'Double-click to edit...', color: 'yellow' }),
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
  />