@eventcatalog/core 5.0.0-beta.0 → 5.0.0-beta.10
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 +19 -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 +72 -35
- 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-ZHKEARLJ.js → chunk-2LBDRHE4.js} +1 -1
- package/dist/{chunk-TQLRG35T.js → chunk-AWOO63ME.js} +71 -45
- package/dist/chunk-CPW6G62V.js +17 -0
- package/dist/{chunk-LR7AGRHN.js → chunk-CRKJBDN7.js} +56 -33
- package/dist/{chunk-CR6AK5LR.js → chunk-EM36OL6A.js} +1 -1
- package/dist/{chunk-TWN43V4T.js → chunk-IONYKKOY.js} +1 -1
- package/dist/{chunk-WAIADKQE.js → chunk-LJMXOEEC.js} +10 -3
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +205 -124
- package/dist/eventcatalog.config.d.cts +59 -1
- package/dist/eventcatalog.config.d.ts +59 -1
- package/dist/eventcatalog.js +30 -11
- package/dist/federation/federate.js +2 -2
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
- package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
- package/dist/runtime-dependencies/manifest.json +8 -1
- package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
- package/dist/runtime-dependencies/yjs.mjs +3 -0
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/dist/utils/license-status.cjs +71 -45
- 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/astro.config.mjs +5 -0
- package/eventcatalog/integrations/storage.mjs +20 -0
- package/eventcatalog/integrations/studio-server.mjs +32 -0
- package/eventcatalog/src/components/Grids/utils.tsx +0 -23
- package/eventcatalog/src/components/Header.astro +3 -3
- package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +6 -1
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +12 -4
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +16 -7
- package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +10 -0
- package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +4 -62
- package/eventcatalog/src/components/Tables/Discover/columns.tsx +17 -150
- package/eventcatalog/src/components/Tables/Resources/ResourcesTable.tsx +425 -0
- package/eventcatalog/src/components/Tables/RowActionsMenu.tsx +157 -0
- package/eventcatalog/src/components/Tables/TablePagination.tsx +82 -0
- package/eventcatalog/src/features/ai/chat-api.ts +3 -3
- package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
- package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
- package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
- package/eventcatalog/src/features/mcp/apps/canvas/styles.css +22 -0
- package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +276 -0
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +210 -0
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +75 -74
- package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
- package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
- package/eventcatalog/src/features/mcp/mcp-server.ts +36 -5
- package/eventcatalog/src/features/server/start.ts +25 -0
- package/eventcatalog/src/features/storage/database.ts +88 -0
- package/eventcatalog/src/features/storage/migrations.ts +85 -0
- package/eventcatalog/src/features/storage/sql/database.sql +5 -0
- package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
- package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
- package/eventcatalog/src/features/storage/sql.ts +28 -0
- package/eventcatalog/src/features/studio/agent-choreography.ts +381 -0
- package/eventcatalog/src/features/studio/api/canvases-api.ts +170 -0
- package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
- package/eventcatalog/src/features/studio/api/client.ts +35 -0
- package/eventcatalog/src/features/studio/canvas-actions.ts +414 -0
- package/eventcatalog/src/features/studio/canvas-doc.ts +414 -0
- package/eventcatalog/src/features/studio/canvas-preview.ts +53 -0
- package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
- package/eventcatalog/src/features/studio/catalog.ts +171 -0
- package/eventcatalog/src/features/studio/clipboard.ts +97 -0
- package/eventcatalog/src/features/studio/components/CanvasControls.tsx +154 -0
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +370 -0
- package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +117 -0
- package/eventcatalog/src/features/studio/components/Comments.tsx +770 -0
- package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +281 -0
- package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +144 -0
- package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +145 -0
- package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +81 -0
- package/eventcatalog/src/features/studio/components/DragGhost.tsx +115 -0
- package/eventcatalog/src/features/studio/components/JoinForm.tsx +34 -0
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +314 -0
- package/eventcatalog/src/features/studio/components/Picture.tsx +21 -0
- package/eventcatalog/src/features/studio/components/Presence.tsx +286 -0
- package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +212 -0
- package/eventcatalog/src/features/studio/components/ShareDialog.tsx +260 -0
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +1216 -0
- package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
- package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +255 -0
- package/eventcatalog/src/features/studio/components/icons.ts +53 -0
- package/eventcatalog/src/features/studio/components/status.ts +53 -0
- package/eventcatalog/src/features/studio/edges.ts +56 -0
- package/eventcatalog/src/features/studio/flow-state.ts +92 -0
- package/eventcatalog/src/features/studio/grouping.ts +226 -0
- package/eventcatalog/src/features/studio/hooks/presence-sender.ts +46 -0
- package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +347 -0
- package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
- package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +104 -0
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +589 -0
- package/eventcatalog/src/features/studio/identity.ts +37 -0
- package/eventcatalog/src/features/studio/layout.ts +42 -0
- package/eventcatalog/src/features/studio/levels.ts +282 -0
- package/eventcatalog/src/features/studio/node-types.ts +205 -0
- package/eventcatalog/src/features/studio/pages/[id].astro +68 -0
- package/eventcatalog/src/features/studio/pages/index.astro +784 -0
- package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
- package/eventcatalog/src/features/studio/placement.ts +197 -0
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +510 -0
- package/eventcatalog/src/features/studio/server/runtime.ts +491 -0
- package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
- package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
- package/eventcatalog/src/features/studio/server/storage.ts +90 -0
- package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
- package/eventcatalog/src/features/studio/studio.css +273 -0
- package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +10 -5
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +52 -63
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +34 -46
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +11 -29
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_ResourcesPage.astro +79 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +32 -14
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +63 -58
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +2 -62
- package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +13 -0
- package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +24 -42
- package/eventcatalog/src/stores/sidebar-store/builders/flow.ts +2 -0
- package/eventcatalog/src/stores/sidebar-store/builders/message.ts +2 -0
- package/eventcatalog/src/stores/sidebar-store/builders/service.ts +23 -23
- package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +13 -0
- package/eventcatalog/src/stores/sidebar-store/builders/system.ts +18 -27
- package/eventcatalog/src/stores/sidebar-store/state.ts +5 -0
- package/eventcatalog/src/styles/tailwind.css +2 -2
- package/eventcatalog/src/utils/collections/channels.ts +56 -1
- package/eventcatalog/src/utils/collections/flows.ts +50 -1
- package/eventcatalog/src/utils/collections/message-connections.ts +60 -0
- package/eventcatalog/src/utils/collections/message-triggers.ts +2 -1
- package/eventcatalog/src/utils/collections/resource-owners.ts +193 -0
- package/eventcatalog/src/utils/collections/resources.ts +310 -0
- package/eventcatalog/src/utils/collections/util.ts +31 -0
- package/eventcatalog/src/utils/feature.ts +8 -0
- package/eventcatalog/src/utils/git.ts +11 -5
- package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
- package/eventcatalog/src/utils/trial.ts +1 -1
- package/package.json +17 -12
- package/dist/runtime-dependencies/shelljs.mjs +0 -3
- package/eventcatalog/public/studio-bg.png +0 -0
- package/eventcatalog/src/components/Grids/DomainGrid.tsx +0 -838
- package/eventcatalog/src/components/Grids/MessageGrid.tsx +0 -344
- package/eventcatalog/src/components/Grids/SystemGrid.tsx +0 -185
- package/eventcatalog/src/components/Grids/components.tsx +0 -170
- package/eventcatalog/src/components/Grids/message-link.ts +0 -38
- package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
- package/eventcatalog/src/components/Tables/SystemResources/SystemResourcesTable.tsx +0 -294
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.spec.ts +0 -65
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.ts +0 -46
- package/eventcatalog/src/pages/studio.astro +0 -316
- package/dist/{chunk-AYNZXUB5.js → chunk-KHACCTW2.js} +3 -3
|
@@ -0,0 +1,282 @@
|
|
|
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
|
|
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
|
+
|
|
48
|
+
/** What a node becomes at a level: kept (as itself or another node), carried through (messages, channels) or dropped */
|
|
49
|
+
type Representative = { kind: 'node'; id: string } | { kind: 'carrier' } | { kind: 'drop' };
|
|
50
|
+
|
|
51
|
+
/**
|
|
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.
|
|
56
|
+
*/
|
|
57
|
+
const collapseGraph = (
|
|
58
|
+
nodes: Node[],
|
|
59
|
+
edges: Edge[],
|
|
60
|
+
representativeOf: (node: Node) => Representative,
|
|
61
|
+
{ keepDirect = false }: { keepDirect?: boolean } = {}
|
|
62
|
+
) => {
|
|
63
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
64
|
+
// Whether one node is inside another (e.g. a system in its domain): they're not connected, one holds the other
|
|
65
|
+
const isInside = (id: string, containerId: string) => {
|
|
66
|
+
for (let node = byId.get(id); node?.parentId; node = byId.get(node.parentId)) if (node.parentId === containerId) return true;
|
|
67
|
+
return false;
|
|
68
|
+
};
|
|
69
|
+
const representatives = new Map(nodes.map((node) => [node.id, representativeOf(node)]));
|
|
70
|
+
const outgoing = new Map<string, Edge[]>();
|
|
71
|
+
edges.forEach((edge) => outgoing.set(edge.source, [...(outgoing.get(edge.source) ?? []), edge]));
|
|
72
|
+
|
|
73
|
+
type Merged = { source: string; target: string; bridged: boolean; messages: Map<string, string>; labels: string[] };
|
|
74
|
+
const merged = new Map<string, Merged>();
|
|
75
|
+
const kept: Edge[] = [];
|
|
76
|
+
const addMessage = (messages: Map<string, string>, node: Node) => {
|
|
77
|
+
if (isMessage(node)) messages.set(messageName(node), MESSAGE_COLLECTIONS[node.type!]);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
edges.forEach((edge) => {
|
|
81
|
+
const from = representatives.get(edge.source);
|
|
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);
|
|
85
|
+
// Follow the edge through carried nodes to the node(s) it reaches, collecting the messages on the way
|
|
86
|
+
const stack = [{ id: edge.target, messages: new Map<string, string>(), labels: [String(edge.label ?? '')], bridged: false }];
|
|
87
|
+
const visited = new Set<string>();
|
|
88
|
+
addMessage(stack[0].messages, byId.get(edge.source)!);
|
|
89
|
+
while (stack.length) {
|
|
90
|
+
const current = stack.pop()!;
|
|
91
|
+
if (visited.has(current.id)) continue;
|
|
92
|
+
visited.add(current.id);
|
|
93
|
+
const node = byId.get(current.id);
|
|
94
|
+
const reached = representatives.get(current.id);
|
|
95
|
+
if (!node || !reached || reached.kind === 'drop') continue;
|
|
96
|
+
const messages = new Map(current.messages);
|
|
97
|
+
addMessage(messages, node);
|
|
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
|
+
);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
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
|
+
};
|
|
115
|
+
messages.forEach((collection, name) => entry.messages.set(name, collection));
|
|
116
|
+
entry.labels.push(...current.labels.filter(Boolean));
|
|
117
|
+
merged.set(key, entry);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
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';
|
|
124
|
+
return {
|
|
125
|
+
nodes: nodes.filter((node) => keptIds.has(node.id)),
|
|
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
|
+
],
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The system and domain each node is in: the containers it sits inside, or the domain and system cards
|
|
140
|
+
* connected to it with "contains"
|
|
141
|
+
*/
|
|
142
|
+
const getContainers = (nodes: Node[], edges: Edge[]) => {
|
|
143
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
144
|
+
const containers = new Map<string, { system?: string; domain?: string }>();
|
|
145
|
+
const add = (nodeId: string, container: Node) => {
|
|
146
|
+
const entry = containers.get(nodeId) ?? {};
|
|
147
|
+
if (isSystem(container)) entry.system ??= container.id;
|
|
148
|
+
else entry.domain ??= container.id;
|
|
149
|
+
containers.set(nodeId, entry);
|
|
150
|
+
};
|
|
151
|
+
for (const node of nodes) {
|
|
152
|
+
for (
|
|
153
|
+
let parent = node.parentId ? byId.get(node.parentId) : undefined;
|
|
154
|
+
parent;
|
|
155
|
+
parent = parent.parentId ? byId.get(parent.parentId) : undefined
|
|
156
|
+
) {
|
|
157
|
+
if (isContainer(parent)) add(node.id, parent);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const edge of edges) {
|
|
161
|
+
const container = byId.get(edge.source);
|
|
162
|
+
if (container && isContainer(container) && edge.label === 'contains') add(edge.target, container);
|
|
163
|
+
}
|
|
164
|
+
return containers;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
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.
|
|
170
|
+
*/
|
|
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
|
+
});
|
|
177
|
+
|
|
178
|
+
/** L2: messages and channels hidden, the nodes either side connected directly */
|
|
179
|
+
const level2 = (nodes: Node[], edges: Edge[]) =>
|
|
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
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
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
|
|
193
|
+
*/
|
|
194
|
+
const asSystemsLevel = (
|
|
195
|
+
{ nodes, edges }: { nodes: Node[]; edges: Edge[] },
|
|
196
|
+
{ all, allEdges, containers }: { all: Node[]; allEdges: Edge[]; containers: ReturnType<typeof getContainers> }
|
|
197
|
+
) => {
|
|
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
|
+
};
|
|
211
|
+
return {
|
|
212
|
+
edges,
|
|
213
|
+
nodes: nodes.map((node): Node => {
|
|
214
|
+
const { id, parentId, position, data } = node;
|
|
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;
|
|
244
|
+
}),
|
|
245
|
+
};
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/** L1: services folded into their system (or domain); actors, external systems and loose services stay */
|
|
249
|
+
const level1 = (nodes: Node[], edges: Edge[]) => {
|
|
250
|
+
const containers = getContainers(nodes, edges);
|
|
251
|
+
const collapsed = collapseGraph(nodes, edges, (node) => {
|
|
252
|
+
const container = containers.get(node.id);
|
|
253
|
+
// Systems stay (inside their domain's container, if they're in one); everything else folds into its system or domain
|
|
254
|
+
if (isContainer(node) && (isSystem(node) || !container?.system)) return { kind: 'node', id: node.id };
|
|
255
|
+
if (container?.system) return { kind: 'node', id: container.system };
|
|
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)
|
|
258
|
+
if (isCarrier(node)) return { kind: 'carrier' };
|
|
259
|
+
if (container?.domain) return { kind: 'node', id: container.domain };
|
|
260
|
+
if (node.type === 'data' || isWrittenOnCanvas(node.type)) return { kind: 'drop' };
|
|
261
|
+
return { kind: 'node', id: node.id };
|
|
262
|
+
});
|
|
263
|
+
return asSystemsLevel(collapsed, { all: nodes, allEdges: edges, containers });
|
|
264
|
+
};
|
|
265
|
+
|
|
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
|
+
*/
|
|
270
|
+
export const getLevelGraph = (nodes: Node[], edges: Edge[], level: Level) => {
|
|
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));
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/** Why a level can't be shown for this canvas, if it can't */
|
|
277
|
+
export const getLevelUnavailableReason = (nodes: Node[], level: Level): string | undefined => {
|
|
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';
|
|
281
|
+
return undefined;
|
|
282
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
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
|
+
/** Domains and systems as containers: boxes other nodes sit in */
|
|
16
|
+
export const GROUP_TYPES = { domain: 'domain-group', system: 'system-group' } as const;
|
|
17
|
+
export const isGroupType = (type?: string) => type === GROUP_TYPES.domain || type === GROUP_TYPES.system;
|
|
18
|
+
|
|
19
|
+
export type NodeDefinition = {
|
|
20
|
+
/** React Flow node type, matches the visualiser node component keys */
|
|
21
|
+
type: string;
|
|
22
|
+
label: string;
|
|
23
|
+
category: NodeCategory;
|
|
24
|
+
/** Not in the components list (still drawn, for canvases that have them) */
|
|
25
|
+
unlisted?: boolean;
|
|
26
|
+
/** Where the resource lives inside node.data (e.g. `service` for data.service). Undefined = data root */
|
|
27
|
+
resourceKey?: string;
|
|
28
|
+
/** Nodes that are sized on the canvas (containers, notes, text) start at this size */
|
|
29
|
+
defaultSize?: { width: number; height: number };
|
|
30
|
+
/** Only its width is set: its height follows what's in it (text) */
|
|
31
|
+
autoHeight?: boolean;
|
|
32
|
+
createData: () => Record<string, unknown>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const resource = (name: string, summary: string) => ({ name, version: '0.0.1', summary, owners: [] });
|
|
36
|
+
|
|
37
|
+
export const nodeDefinitions: NodeDefinition[] = [
|
|
38
|
+
{
|
|
39
|
+
type: GROUP_TYPES.domain,
|
|
40
|
+
label: 'Domain',
|
|
41
|
+
category: 'boundaries',
|
|
42
|
+
resourceKey: 'domain',
|
|
43
|
+
defaultSize: { width: 720, height: 460 },
|
|
44
|
+
createData: () => ({ domain: { name: 'New Domain', version: '0.0.1' } }),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: GROUP_TYPES.system,
|
|
48
|
+
label: 'System',
|
|
49
|
+
category: 'boundaries',
|
|
50
|
+
resourceKey: 'system',
|
|
51
|
+
defaultSize: { width: 560, height: 360 },
|
|
52
|
+
createData: () => ({ system: { name: 'New System', version: '0.0.1' } }),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
type: 'service',
|
|
56
|
+
category: 'architecture',
|
|
57
|
+
label: 'Service',
|
|
58
|
+
resourceKey: 'service',
|
|
59
|
+
createData: () => ({
|
|
60
|
+
mode: 'full',
|
|
61
|
+
service: { ...resource('New Service', 'Describe what this service does.'), sends: [], receives: [] },
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
type: 'event',
|
|
66
|
+
category: 'messages',
|
|
67
|
+
label: 'Event',
|
|
68
|
+
resourceKey: 'message',
|
|
69
|
+
createData: () => ({ mode: 'full', message: resource('NewEvent', 'Something happened.') }),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: 'command',
|
|
73
|
+
category: 'messages',
|
|
74
|
+
label: 'Command',
|
|
75
|
+
resourceKey: 'message',
|
|
76
|
+
createData: () => ({ mode: 'full', message: resource('NewCommand', 'Asks a service to do something.') }),
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: 'query',
|
|
80
|
+
category: 'messages',
|
|
81
|
+
label: 'Query',
|
|
82
|
+
resourceKey: 'message',
|
|
83
|
+
createData: () => ({ mode: 'full', message: resource('NewQuery', 'Asks for some information.') }),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: 'channel',
|
|
87
|
+
category: 'architecture',
|
|
88
|
+
label: 'Channel',
|
|
89
|
+
resourceKey: 'channel',
|
|
90
|
+
createData: () => ({ mode: 'full', channel: { ...resource('new.channel', 'Where messages are routed.'), protocols: [] } }),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
type: 'data',
|
|
94
|
+
category: 'architecture',
|
|
95
|
+
label: 'Data Store',
|
|
96
|
+
resourceKey: 'data',
|
|
97
|
+
createData: () => ({ mode: 'full', data: { ...resource('New Database', 'Stores data.'), container_type: 'database' } }),
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
type: 'view',
|
|
101
|
+
category: 'architecture',
|
|
102
|
+
label: 'View',
|
|
103
|
+
resourceKey: 'view',
|
|
104
|
+
createData: () => ({ mode: 'full', view: resource('New View', 'What the user sees.') }),
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
type: 'externalSystem',
|
|
108
|
+
category: 'architecture',
|
|
109
|
+
unlisted: true,
|
|
110
|
+
label: 'External System',
|
|
111
|
+
resourceKey: 'externalSystem',
|
|
112
|
+
createData: () => ({ mode: 'full', externalSystem: resource('External System', 'A third party system.') }),
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
type: 'agent',
|
|
116
|
+
category: 'architecture',
|
|
117
|
+
label: 'Agent',
|
|
118
|
+
resourceKey: 'agent',
|
|
119
|
+
createData: () => ({ mode: 'full', agent: { ...resource('New Agent', 'What this agent does.'), sends: [], receives: [] } }),
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
type: 'actor',
|
|
123
|
+
category: 'other',
|
|
124
|
+
label: 'Actor',
|
|
125
|
+
createData: () => ({ mode: 'full', name: 'Customer', summary: 'A person using the system.' }),
|
|
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
|
+
},
|
|
135
|
+
{
|
|
136
|
+
type: 'note',
|
|
137
|
+
category: 'other',
|
|
138
|
+
label: 'Sticky Note',
|
|
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' }),
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
/** The types people and agents can add (not the unlisted ones) */
|
|
147
|
+
export const COMPONENT_TYPES = nodeDefinitions
|
|
148
|
+
.filter((definition) => !definition.unlisted)
|
|
149
|
+
.map((definition) => definition.type) as [string, ...string[]];
|
|
150
|
+
|
|
151
|
+
const definitionsByType = new Map(nodeDefinitions.map((definition) => [definition.type, definition]));
|
|
152
|
+
|
|
153
|
+
// Node types that only come from the catalog (not in the components list)
|
|
154
|
+
const catalogNodeTypes: Record<string, { label: string; resourceKey: string }> = {
|
|
155
|
+
system: { label: 'System', resourceKey: 'system' },
|
|
156
|
+
'context-domain': { label: 'Domain', resourceKey: 'domain' },
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const getNodeDefinition = (type?: string) => (type ? definitionsByType.get(type) : undefined);
|
|
160
|
+
|
|
161
|
+
export const getNodeLabel = (type?: string) => getNodeDefinition(type)?.label ?? catalogNodeTypes[type ?? '']?.label ?? 'Node';
|
|
162
|
+
|
|
163
|
+
// About the size the visualiser's resource cards render at
|
|
164
|
+
export const DEFAULT_NODE_SIZE = { width: 240, height: 112 };
|
|
165
|
+
|
|
166
|
+
/** The size a new node of a type starts at (or, for cards, about the size it renders at) */
|
|
167
|
+
export const getNodeSize = (type?: string) => getNodeDefinition(type)?.defaultSize ?? DEFAULT_NODE_SIZE;
|
|
168
|
+
|
|
169
|
+
const getResourceKey = (type?: string) => getNodeDefinition(type)?.resourceKey ?? catalogNodeTypes[type ?? '']?.resourceKey;
|
|
170
|
+
|
|
171
|
+
/** The resource object for a node (e.g. data.service), or the data itself for flat nodes */
|
|
172
|
+
export const getResource = (type: string | undefined, data: Record<string, unknown>): Record<string, unknown> => {
|
|
173
|
+
const key = getResourceKey(type);
|
|
174
|
+
return key ? ((data[key] as Record<string, unknown>) ?? {}) : data;
|
|
175
|
+
};
|
|
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
|
+
|
|
197
|
+
/** Returns new node data with fields of the resource changed */
|
|
198
|
+
export const updateResource = (type: string | undefined, data: Record<string, unknown>, fields: Record<string, unknown>) => {
|
|
199
|
+
const key = getResourceKey(type);
|
|
200
|
+
return key ? { ...data, [key]: { ...getResource(type, data), ...fields } } : { ...data, ...fields };
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/** A node's name, as people and agents see it */
|
|
204
|
+
export const getNodeName = (type: string | undefined, data: Record<string, unknown>, fallback = 'node') =>
|
|
205
|
+
String(getResource(type, data).name ?? data.text ?? fallback);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Visualiser styles ship in the page head so ClientRouter navigations keep them (see NodeGraph.astro).
|
|
3
|
+
import '@eventcatalog/visualiser/styles-core.css';
|
|
4
|
+
import '../studio.css';
|
|
5
|
+
import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
|
|
6
|
+
import { buildUrl } from '@utils/url-builder';
|
|
7
|
+
import StudioDesigner from '../components/StudioDesigner';
|
|
8
|
+
import StudioNotice from '../components/StudioNotice.astro';
|
|
9
|
+
import { getCatalogResources } from '../catalog-resources';
|
|
10
|
+
import { startStudio, STUDIO_SOCKET_PATH } from '../server/runtime';
|
|
11
|
+
import { getSignedInUser, isSignedIn } from '../server/sign-in';
|
|
12
|
+
import { CANVASES_API_PATH } from '../api/canvases-api';
|
|
13
|
+
|
|
14
|
+
export const prerender = false;
|
|
15
|
+
|
|
16
|
+
const { id } = Astro.params;
|
|
17
|
+
|
|
18
|
+
if (!id || !/^[0-9a-f-]{36}$/i.test(id)) {
|
|
19
|
+
return Astro.redirect(buildUrl('/studio'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Studio can't start when its storage can't be used (e.g. the database was updated by a newer EventCatalog)
|
|
23
|
+
const started = await startStudio({ isSignedIn }).catch((error: Error) => error);
|
|
24
|
+
const unavailable = started instanceof Error ? started.message : undefined;
|
|
25
|
+
const runtime = started instanceof Error ? undefined : started;
|
|
26
|
+
// A deleted canvas's link (anyone who still had it open was told)
|
|
27
|
+
const deleted = runtime?.isDeleted(id) ?? false;
|
|
28
|
+
if (unavailable) Astro.response.status = 503;
|
|
29
|
+
else if (deleted) Astro.response.status = 410;
|
|
30
|
+
const { resources, relations } = runtime && !deleted ? await getCatalogResources() : { resources: [], relations: [] };
|
|
31
|
+
const keptInMemory = runtime?.storage.type === 'memory';
|
|
32
|
+
const signedInAs = getSignedInUser(Astro.locals);
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
<VerticalSideBarLayout title="Studio - EventCatalog" showNestedSideBar={false}>
|
|
36
|
+
<main
|
|
37
|
+
class="min-h-0 overflow-hidden bg-[rgb(var(--ec-page-bg))]"
|
|
38
|
+
style="margin-left: calc(var(--ec-app-content-padding-left, 5rem) * -1); margin-right: calc(var(--ec-app-content-padding-right, 5rem) * -1); height: calc(100dvh - 60px);"
|
|
39
|
+
>
|
|
40
|
+
{
|
|
41
|
+
unavailable ? (
|
|
42
|
+
<StudioNotice title="Studio isn't available" message={unavailable} />
|
|
43
|
+
) : deleted ? (
|
|
44
|
+
<StudioNotice
|
|
45
|
+
title="This canvas was deleted"
|
|
46
|
+
message="Someone deleted it, so it can't be opened any more."
|
|
47
|
+
href={buildUrl('/studio')}
|
|
48
|
+
linkText="Back to Studio"
|
|
49
|
+
/>
|
|
50
|
+
) : (
|
|
51
|
+
<StudioDesigner
|
|
52
|
+
client:only="react"
|
|
53
|
+
canvasId={id}
|
|
54
|
+
socketPath={buildUrl(STUDIO_SOCKET_PATH, true)}
|
|
55
|
+
newCanvasUrl={`${buildUrl('/studio')}?new=1`}
|
|
56
|
+
studioUrl={buildUrl('/studio')}
|
|
57
|
+
canvasesApiUrl={buildUrl(CANVASES_API_PATH, true)}
|
|
58
|
+
mcpUrl={buildUrl('/docs/mcp', true)}
|
|
59
|
+
relayScriptPath={buildUrl('/webmcp-relay/embed.js', true)}
|
|
60
|
+
keptInMemory={keptInMemory}
|
|
61
|
+
signedInAs={signedInAs}
|
|
62
|
+
resources={resources}
|
|
63
|
+
relations={relations}
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
</main>
|
|
68
|
+
</VerticalSideBarLayout>
|