@eventcatalog/core 5.0.0-beta.2 → 5.0.0-beta.21
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 +234 -134
- package/README.md +5 -5
- 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 +71 -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/catalog-runtime.cjs +1 -1
- package/dist/{chunk-TQLRG35T.js → chunk-AWOO63ME.js} +71 -45
- package/dist/chunk-CPW6G62V.js +17 -0
- package/dist/{chunk-KIDISU6X.js → chunk-I7F7CPDC.js} +56 -33
- package/dist/{chunk-HGRFXTFC.js → chunk-OVWDUANF.js} +1 -1
- package/dist/{chunk-62EV4IJA.js → chunk-RIRRWE7D.js} +1 -1
- package/dist/chunk-S5GWAUG5.js +29 -0
- package/dist/{chunk-4J2ZM2HX.js → chunk-W6KDW6ZC.js} +1 -1
- package/dist/{chunk-XARPIXWF.js → chunk-WHYRRLHE.js} +9 -3
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +245 -131
- package/dist/eventcatalog.config.d.cts +59 -1
- package/dist/eventcatalog.config.d.ts +59 -1
- package/dist/eventcatalog.js +45 -15
- package/dist/federation/federate.js +2 -2
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio-fonts.css.mjs +3 -0
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.css.mjs +3 -0
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.mjs +3 -0
- package/dist/runtime-dependencies/@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/htmlparser2.mjs +3 -0
- package/dist/runtime-dependencies/manifest.json +13 -1
- package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
- package/dist/runtime-dependencies/react-syntax-highlighter__dist__cjs__create-element.js.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/child-process-signals.cjs +53 -0
- package/dist/utils/child-process-signals.d.cts +19 -0
- package/dist/utils/child-process-signals.d.ts +19 -0
- package/dist/utils/child-process-signals.js +6 -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 +23 -1
- package/eventcatalog/integrations/skip-startup-reload.mjs +31 -0
- package/eventcatalog/integrations/stable-dependency-order.mjs +29 -0
- package/eventcatalog/integrations/storage.mjs +20 -0
- package/eventcatalog/integrations/studio-server.mjs +32 -0
- package/eventcatalog/src/components/CatalogGraph/CatalogForceGraph.tsx +39 -0
- package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +2 -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/MDX/NodeGraph/AstroNodeGraph.tsx +42 -3
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -1
- package/eventcatalog/src/components/NavigationProgress.astro +7 -0
- package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +2 -0
- package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +2 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +2 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +2 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +12 -4
- package/eventcatalog/src/components/Search/SearchDataLoader.astro +16 -28
- package/eventcatalog/src/components/Search/SearchModal.tsx +2 -10
- package/eventcatalog/src/components/Search/search-utils.ts +26 -0
- package/eventcatalog/src/components/Seo.astro +5 -1
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +53 -83
- package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +2 -0
- package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +131 -0
- package/eventcatalog/src/components/SideNav/SideNav.astro +9 -1
- 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/error.astro +4 -2
- package/eventcatalog/src/features/auth/login.astro +2 -1
- package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
- package/eventcatalog/src/features/auth/unauthorized.astro +1 -1
- package/eventcatalog/src/features/fields/pages/fields.astro +1 -1
- package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
- package/eventcatalog/src/features/mcp/apps/architecture-diagram/node-chat.tsx +6 -45
- package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
- package/eventcatalog/src/features/mcp/apps/canvas/styles.css +24 -0
- package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +278 -0
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +272 -0
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +77 -76
- 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 +384 -0
- package/eventcatalog/src/features/studio/agents.ts +28 -0
- package/eventcatalog/src/features/studio/api/canvases-api.ts +219 -0
- package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
- package/eventcatalog/src/features/studio/api/client.ts +40 -0
- package/eventcatalog/src/features/studio/canvas-actions.ts +540 -0
- package/eventcatalog/src/features/studio/canvas-preview.ts +52 -0
- package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
- package/eventcatalog/src/features/studio/catalog.ts +173 -0
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +518 -0
- package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +118 -0
- package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +434 -0
- package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +143 -0
- package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +65 -0
- package/eventcatalog/src/features/studio/components/Dialog.tsx +60 -0
- package/eventcatalog/src/features/studio/components/JoinForm.tsx +109 -0
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +203 -0
- package/eventcatalog/src/features/studio/components/NodeDetails.tsx +183 -0
- package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +79 -0
- package/eventcatalog/src/features/studio/components/Presence.tsx +293 -0
- package/eventcatalog/src/features/studio/components/ShareDialog.tsx +396 -0
- package/eventcatalog/src/features/studio/components/ShortcutsDialog.tsx +41 -0
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +2091 -0
- package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
- package/eventcatalog/src/features/studio/document-name.ts +7 -0
- package/eventcatalog/src/features/studio/from-visualiser.ts +138 -0
- package/eventcatalog/src/features/studio/hooks/presence-sender.ts +63 -0
- package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +353 -0
- package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
- package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +109 -0
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +782 -0
- package/eventcatalog/src/features/studio/identity.ts +59 -0
- package/eventcatalog/src/features/studio/limits.ts +2 -0
- package/eventcatalog/src/features/studio/pages/[id].astro +69 -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/server/canvas-mcp.ts +495 -0
- package/eventcatalog/src/features/studio/server/runtime.ts +498 -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/tool-sync.ts +75 -0
- package/eventcatalog/src/layouts/BaseLayout.astro +4 -4
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +13 -7
- package/eventcatalog/src/pages/_index.astro +1 -1
- 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/[type]/[id]/index.astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +13 -0
- package/eventcatalog/src/pages/index.astro +1 -1
- package/eventcatalog/src/pages/schemas/explorer/index.astro +1 -1
- package/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +1 -1
- 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/types/react-syntax-highlighter.d.ts +10 -0
- 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/link-validation.ts +25 -19
- package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +3 -1
- package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +3 -3
- package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
- package/eventcatalog/src/utils/node-graphs/layout-node-graph.ts +20 -1
- package/eventcatalog/src/utils/node-graphs/node-resource.ts +67 -0
- package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +1 -1
- package/eventcatalog/src/utils/page-title.ts +15 -0
- package/eventcatalog/src/utils/syntax-highlighter-renderer.ts +64 -0
- package/eventcatalog/src/utils/trial.ts +1 -1
- package/package.json +22 -16
- 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,310 @@
|
|
|
1
|
+
import { collectionToResourceMap, resourceKey, uniqueResources } from '@utils/collections/util';
|
|
2
|
+
|
|
3
|
+
// What the Resources pages show: which resources each owner (domain, system, service, flow,
|
|
4
|
+
// channel or message) lists, how each relates to the owner, and the links for each row.
|
|
5
|
+
// Loading owners and their related resources lives in ./resource-owners.
|
|
6
|
+
|
|
7
|
+
/** The kinds of resource a Resources page can list. */
|
|
8
|
+
export type ResourceCollection =
|
|
9
|
+
| 'domains'
|
|
10
|
+
| 'systems'
|
|
11
|
+
| 'agents'
|
|
12
|
+
| 'services'
|
|
13
|
+
| 'flows'
|
|
14
|
+
| 'entities'
|
|
15
|
+
| 'data-products'
|
|
16
|
+
| 'containers'
|
|
17
|
+
| 'events'
|
|
18
|
+
| 'commands'
|
|
19
|
+
| 'queries'
|
|
20
|
+
| 'channels'
|
|
21
|
+
| 'adrs';
|
|
22
|
+
|
|
23
|
+
/** How a resource relates to the page's owner: which way a message flows, how a data store is used, or how it belongs to the owner. */
|
|
24
|
+
export type ResourceRelationship =
|
|
25
|
+
| 'receives'
|
|
26
|
+
| 'sends'
|
|
27
|
+
| 'sends-and-receives'
|
|
28
|
+
| 'reads'
|
|
29
|
+
| 'writes'
|
|
30
|
+
| 'reads-and-writes'
|
|
31
|
+
| 'contains'
|
|
32
|
+
| 'owns'
|
|
33
|
+
| 'appears-in'
|
|
34
|
+
| 'governed-by'
|
|
35
|
+
| 'includes'
|
|
36
|
+
| 'transports'
|
|
37
|
+
| 'produces-messages'
|
|
38
|
+
| 'receives-messages'
|
|
39
|
+
| 'produces-and-receives-messages'
|
|
40
|
+
| 'produces'
|
|
41
|
+
| 'consumes'
|
|
42
|
+
| 'produces-and-consumes'
|
|
43
|
+
| 'triggers'
|
|
44
|
+
| 'triggered-by'
|
|
45
|
+
| 'triggers-and-triggered-by';
|
|
46
|
+
|
|
47
|
+
/** Any catalog entry a Resources page reads: a collection entry, possibly hydrated with extra fields. */
|
|
48
|
+
export type ResourceEntry = {
|
|
49
|
+
collection: string;
|
|
50
|
+
data: { id: string; version: string; name?: string; summary?: string; [key: string]: any };
|
|
51
|
+
[key: string]: any;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type ResourceOwnerCollection = 'systems' | 'domains' | 'services' | 'flows' | 'channels' | MessageOwnerCollection;
|
|
55
|
+
type MessageOwnerCollection = 'events' | 'commands' | 'queries';
|
|
56
|
+
const isMessageOwner = (collection: ResourceOwnerCollection): collection is MessageOwnerCollection =>
|
|
57
|
+
collection === 'events' || collection === 'commands' || collection === 'queries';
|
|
58
|
+
|
|
59
|
+
/** Resources that point at the owner, rather than being listed in the owner's own frontmatter. */
|
|
60
|
+
export interface RelatedResources {
|
|
61
|
+
/** Decision records whose `appliesTo` includes the owner. */
|
|
62
|
+
adrs?: ResourceEntry[];
|
|
63
|
+
/** Flows with a step that points at the owner (services only). */
|
|
64
|
+
stepFlows?: ResourceEntry[];
|
|
65
|
+
/** The resources a flow's steps point at (flows only). */
|
|
66
|
+
stepResources?: ResourceEntry[];
|
|
67
|
+
/** Services and agents that send to the channel, those that receive from it, and the messages on it (channels only). */
|
|
68
|
+
channelProducers?: ResourceEntry[];
|
|
69
|
+
channelConsumers?: ResourceEntry[];
|
|
70
|
+
channelMessages?: ResourceEntry[];
|
|
71
|
+
/** The channels, flows and messages a message connects to (messages only; its producers and consumers are on `data`). */
|
|
72
|
+
messageChannels?: ResourceEntry[];
|
|
73
|
+
messageFlows?: ResourceEntry[];
|
|
74
|
+
messageTriggers?: ResourceEntry[];
|
|
75
|
+
messageTriggeredBy?: ResourceEntry[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A resource owner as the Resources page receives it: the collection entry with its related resources attached. */
|
|
79
|
+
export type ResourceOwner = ResourceEntry & RelatedResources;
|
|
80
|
+
|
|
81
|
+
export interface ResourceGroup {
|
|
82
|
+
collection: ResourceCollection;
|
|
83
|
+
items: ResourceEntry[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const getMessages = (data: any) =>
|
|
87
|
+
uniqueResources([...(data.sends || []), ...(data.receives || [])].filter((message) => message?.collection));
|
|
88
|
+
|
|
89
|
+
const messageGroups = (messages: any[]): ResourceGroup[] => [
|
|
90
|
+
{ collection: 'events', items: messages.filter((message) => message.collection === 'events') },
|
|
91
|
+
{ collection: 'commands', items: messages.filter((message) => message.collection === 'commands') },
|
|
92
|
+
{ collection: 'queries', items: messages.filter((message) => message.collection === 'queries') },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
// The collections a flow step can point at, in the order the page groups them.
|
|
96
|
+
const FLOW_STEP_COLLECTIONS: ResourceCollection[] = [
|
|
97
|
+
'services',
|
|
98
|
+
'agents',
|
|
99
|
+
'flows',
|
|
100
|
+
'containers',
|
|
101
|
+
'data-products',
|
|
102
|
+
'events',
|
|
103
|
+
'commands',
|
|
104
|
+
'queries',
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
export const getResourceGroups = (
|
|
108
|
+
data: any,
|
|
109
|
+
ownerCollection: ResourceOwnerCollection,
|
|
110
|
+
related: RelatedResources = {}
|
|
111
|
+
): ResourceGroup[] => {
|
|
112
|
+
const adrGroup: ResourceGroup = { collection: 'adrs', items: related.adrs || [] };
|
|
113
|
+
|
|
114
|
+
// A message's resources are what produces or consumes it, the channels it travels on, the
|
|
115
|
+
// flows it appears in, and the messages it triggers or is triggered by.
|
|
116
|
+
if (isMessageOwner(ownerCollection)) {
|
|
117
|
+
const endpoints = uniqueResources([...(data.producers || []), ...(data.consumers || [])]);
|
|
118
|
+
const endpointGroup = (collection: ResourceCollection): ResourceGroup => ({
|
|
119
|
+
collection,
|
|
120
|
+
items: endpoints.filter((endpoint) => endpoint.collection === collection),
|
|
121
|
+
});
|
|
122
|
+
return [
|
|
123
|
+
endpointGroup('services'),
|
|
124
|
+
endpointGroup('agents'),
|
|
125
|
+
endpointGroup('data-products'),
|
|
126
|
+
{ collection: 'channels', items: related.messageChannels || [] },
|
|
127
|
+
{ collection: 'flows', items: related.messageFlows || [] },
|
|
128
|
+
...messageGroups(uniqueResources([...(related.messageTriggers || []), ...(related.messageTriggeredBy || [])])),
|
|
129
|
+
adrGroup,
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// A channel's resources are the services and agents that send to or receive from it, and the messages on it.
|
|
134
|
+
if (ownerCollection === 'channels') {
|
|
135
|
+
const endpoints = uniqueResources([...(related.channelProducers || []), ...(related.channelConsumers || [])]);
|
|
136
|
+
return [
|
|
137
|
+
{ collection: 'services', items: endpoints.filter((endpoint) => endpoint.collection === 'services') },
|
|
138
|
+
{ collection: 'agents', items: endpoints.filter((endpoint) => endpoint.collection === 'agents') },
|
|
139
|
+
...messageGroups(related.channelMessages || []),
|
|
140
|
+
adrGroup,
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// A flow's resources are what its steps point at.
|
|
145
|
+
if (ownerCollection === 'flows') {
|
|
146
|
+
const stepResources = related.stepResources || [];
|
|
147
|
+
return [
|
|
148
|
+
...FLOW_STEP_COLLECTIONS.map((collection) => ({
|
|
149
|
+
collection,
|
|
150
|
+
items: stepResources.filter((resource) => resource.collection === collection),
|
|
151
|
+
})),
|
|
152
|
+
adrGroup,
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// A service's resources are what it uses: the messages it sends and receives, its
|
|
157
|
+
// entities, the data stores it reads from or writes to, and the flows it appears in.
|
|
158
|
+
if (ownerCollection === 'services') {
|
|
159
|
+
return [
|
|
160
|
+
{ collection: 'flows', items: uniqueResources([...(data.flows || []), ...(related.stepFlows || [])]) },
|
|
161
|
+
{ collection: 'entities', items: data.entities || [] },
|
|
162
|
+
{ collection: 'containers', items: uniqueResources([...(data.writesTo || []), ...(data.readsFrom || [])]) },
|
|
163
|
+
...messageGroups(getMessages(data)),
|
|
164
|
+
adrGroup,
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const domainOnlyGroups: ResourceGroup[] =
|
|
169
|
+
ownerCollection === 'domains'
|
|
170
|
+
? [
|
|
171
|
+
{ collection: 'domains', items: data.domains || [] },
|
|
172
|
+
{ collection: 'systems', items: data.systems || [] },
|
|
173
|
+
{ collection: 'agents', items: data.agents || [] },
|
|
174
|
+
{ collection: 'data-products', items: data['data-products'] || [] },
|
|
175
|
+
]
|
|
176
|
+
: [];
|
|
177
|
+
|
|
178
|
+
return [
|
|
179
|
+
...domainOnlyGroups,
|
|
180
|
+
{ collection: 'services', items: data.services || [] },
|
|
181
|
+
{ collection: 'flows', items: data.flows || [] },
|
|
182
|
+
{ collection: 'entities', items: data.entities || [] },
|
|
183
|
+
{ collection: 'containers', items: data.containers || [] },
|
|
184
|
+
...messageGroups(ownerCollection === 'domains' ? getMessages(data) : []),
|
|
185
|
+
adrGroup,
|
|
186
|
+
];
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// The page loader attaches the RelatedResources fields to the owner (see _index.data.ts).
|
|
190
|
+
export const hasResources = (owner: any, ownerCollection: ResourceOwnerCollection): boolean =>
|
|
191
|
+
getResourceGroups(owner?.data ?? {}, ownerCollection, owner ?? {}).some(({ items }) => items.length > 0);
|
|
192
|
+
|
|
193
|
+
const includesResource = (resources: any[] | undefined, item: any) =>
|
|
194
|
+
(resources || []).some((resource) => resource?.data && resourceKey(resource) === resourceKey(item));
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* How a resource relates to the page's owner: which way a message flows, how a data store
|
|
198
|
+
* is used, or whether the owner contains, owns or appears in it.
|
|
199
|
+
*/
|
|
200
|
+
export const getResourceRelationship = (
|
|
201
|
+
data: any,
|
|
202
|
+
ownerCollection: ResourceOwnerCollection,
|
|
203
|
+
collection: ResourceCollection,
|
|
204
|
+
item: any,
|
|
205
|
+
related: RelatedResources = {}
|
|
206
|
+
): ResourceRelationship | undefined => {
|
|
207
|
+
if (collection === 'adrs') return 'governed-by';
|
|
208
|
+
if (ownerCollection === 'flows') return 'includes';
|
|
209
|
+
if (isMessageOwner(ownerCollection)) {
|
|
210
|
+
if (collection === 'channels') return 'transports';
|
|
211
|
+
if (collection === 'flows') return 'appears-in';
|
|
212
|
+
if (collection === 'events' || collection === 'commands' || collection === 'queries') {
|
|
213
|
+
const triggers = includesResource(related.messageTriggers, item);
|
|
214
|
+
const triggeredBy = includesResource(related.messageTriggeredBy, item);
|
|
215
|
+
if (triggers && triggeredBy) return 'triggers-and-triggered-by';
|
|
216
|
+
if (triggers) return 'triggers';
|
|
217
|
+
if (triggeredBy) return 'triggered-by';
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
const produces = includesResource(data.producers, item);
|
|
221
|
+
const consumes = includesResource(data.consumers, item);
|
|
222
|
+
if (produces && consumes) return 'produces-and-consumes';
|
|
223
|
+
if (produces) return 'produces';
|
|
224
|
+
if (consumes) return 'consumes';
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
// A channel transports messages; services and agents produce messages onto it or receive them from it.
|
|
228
|
+
if (ownerCollection === 'channels') {
|
|
229
|
+
if (collection !== 'services' && collection !== 'agents') return 'transports';
|
|
230
|
+
const produces = includesResource(related.channelProducers, item);
|
|
231
|
+
const receives = includesResource(related.channelConsumers, item);
|
|
232
|
+
if (produces && receives) return 'produces-and-receives-messages';
|
|
233
|
+
if (produces) return 'produces-messages';
|
|
234
|
+
if (receives) return 'receives-messages';
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
if (collection === 'events' || collection === 'commands' || collection === 'queries') {
|
|
238
|
+
const sends = includesResource(data.sends, item);
|
|
239
|
+
const receives = includesResource(data.receives, item);
|
|
240
|
+
if (sends && receives) return 'sends-and-receives';
|
|
241
|
+
if (sends) return 'sends';
|
|
242
|
+
if (receives) return 'receives';
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
if (collection === 'entities') return 'owns';
|
|
246
|
+
if (ownerCollection === 'services') {
|
|
247
|
+
if (collection === 'flows') return 'appears-in';
|
|
248
|
+
if (collection === 'containers') {
|
|
249
|
+
const writes = includesResource(data.writesTo, item);
|
|
250
|
+
const reads = includesResource(data.readsFrom, item);
|
|
251
|
+
if (writes && reads) return 'reads-and-writes';
|
|
252
|
+
if (writes) return 'writes';
|
|
253
|
+
if (reads) return 'reads';
|
|
254
|
+
}
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
// Everything else on a domain or system page is part of it.
|
|
258
|
+
return 'contains';
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The schema page for a resource, matching the sidebar's Schema link: messages with an entry
|
|
263
|
+
* in the schemas collection (keyed `collection:id:version`), and data products with an output
|
|
264
|
+
* contract. Undefined for everything else.
|
|
265
|
+
*/
|
|
266
|
+
export const getSchemaPath = (collection: ResourceCollection, item: any, schemaKeys: Set<string>) => {
|
|
267
|
+
const { id, version } = item.data;
|
|
268
|
+
if (collection === 'events' || collection === 'commands' || collection === 'queries') {
|
|
269
|
+
return schemaKeys.has(`${collection}:${id}:${version}`) ? `/schemas/${collection}/${id}/${version}` : undefined;
|
|
270
|
+
}
|
|
271
|
+
if (collection === 'data-products') {
|
|
272
|
+
const contract = (item.data.outputs || []).find((output: any) => output.contract)?.contract;
|
|
273
|
+
return contract ? `/schemas/data-products/${id}/${version}?contract=${encodeURIComponent(contract.path)}` : undefined;
|
|
274
|
+
}
|
|
275
|
+
return undefined;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
export const getDocsPath = (collection: ResourceCollection, item: any) =>
|
|
279
|
+
`/docs/${collection}/${item.data.id}/${item.data.version}`;
|
|
280
|
+
|
|
281
|
+
// Collections the visualiser has a page for (domains and systems through their own routes).
|
|
282
|
+
// Entities and decision records have none.
|
|
283
|
+
const VISUALISER_COLLECTIONS = new Set<ResourceCollection>([
|
|
284
|
+
'domains',
|
|
285
|
+
'systems',
|
|
286
|
+
'agents',
|
|
287
|
+
'services',
|
|
288
|
+
'flows',
|
|
289
|
+
'containers',
|
|
290
|
+
'channels',
|
|
291
|
+
'data-products',
|
|
292
|
+
'events',
|
|
293
|
+
'commands',
|
|
294
|
+
'queries',
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
/** The resource's visualiser page, unless the visualiser is off, has no page for it, or the resource opts out. */
|
|
298
|
+
export const getVisualiserPath = (collection: ResourceCollection, item: any, visualiserEnabled: boolean) => {
|
|
299
|
+
if (!visualiserEnabled || !VISUALISER_COLLECTIONS.has(collection) || item.data.visualiser === false) return undefined;
|
|
300
|
+
return `/visualiser/${collection}/${item.data.id}/${item.data.version}`;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/** Same key and badge as the favorite button on the resource's docs page. */
|
|
304
|
+
export const getFavorite = (collection: ResourceCollection, item: any) => {
|
|
305
|
+
const resourceType = collectionToResourceMap[collection];
|
|
306
|
+
return {
|
|
307
|
+
nodeKey: `${resourceType}:${item.data.id}:${item.data.version}`,
|
|
308
|
+
badge: resourceType.charAt(0).toUpperCase() + resourceType.slice(1),
|
|
309
|
+
};
|
|
310
|
+
};
|
|
@@ -291,6 +291,37 @@ export const createVersionedMap = <T extends { data: { id: string; version?: str
|
|
|
291
291
|
return map;
|
|
292
292
|
};
|
|
293
293
|
|
|
294
|
+
const versionedMapCache = new WeakMap<object, Map<string, any[]>>();
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* createVersionedMap, built once per list. Use it in helpers that search the same list once per
|
|
298
|
+
* resource, so a catalog-wide pass does not rebuild the map every time.
|
|
299
|
+
*/
|
|
300
|
+
export const getVersionedMap = <T extends { data: { id: string; version?: string } }>(items: T[]): Map<string, T[]> => {
|
|
301
|
+
let map = versionedMapCache.get(items);
|
|
302
|
+
if (!map) {
|
|
303
|
+
map = createVersionedMap(items);
|
|
304
|
+
versionedMapCache.set(items, map);
|
|
305
|
+
}
|
|
306
|
+
return map as Map<string, T[]>;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
type KeyedResource = { collection: string; data: { id: string; version?: string } };
|
|
310
|
+
|
|
311
|
+
/** Identifies a resource across collections: a service and an agent can share an id and version. */
|
|
312
|
+
export const resourceKey = (resource: KeyedResource) => `${resource.collection}:${resource.data.id}:${resource.data.version}`;
|
|
313
|
+
|
|
314
|
+
/** The resources without repeats (by collection, id and version), keeping the first of each in order. */
|
|
315
|
+
export const uniqueResources = <T extends KeyedResource>(resources: T[]): T[] => {
|
|
316
|
+
const seen = new Map<string, T>();
|
|
317
|
+
for (const resource of resources) {
|
|
318
|
+
if (!resource?.data?.id) continue;
|
|
319
|
+
const key = resourceKey(resource);
|
|
320
|
+
if (!seen.has(key)) seen.set(key, resource);
|
|
321
|
+
}
|
|
322
|
+
return [...seen.values()];
|
|
323
|
+
};
|
|
324
|
+
|
|
294
325
|
// Merge as many given maps as you want
|
|
295
326
|
export const mergeMaps = <T>(...maps: Map<string, T[]>[]): Map<string, T[]> => {
|
|
296
327
|
return maps.reduce((acc, map) => {
|
|
@@ -27,4 +27,12 @@ export const isAuthEnabled = () =>
|
|
|
27
27
|
(config?.auth?.enabled ?? false) && fs.existsSync(join(projectDirectory(), 'eventcatalog.auth.js')) && isSSR();
|
|
28
28
|
|
|
29
29
|
export const isEventCatalogMCPEnabled = () => isSSR() && (config?.mcp?.enabled ?? true);
|
|
30
|
+
|
|
31
|
+
// Collaborative canvases (EventCatalog Studio, /studio) are an experiment: on the dev server (unless studio.enabled is
|
|
32
|
+
// false), and in production (server output) when studio.enabled is set. With sign-in on, only signed-in people can
|
|
33
|
+
// open them.
|
|
34
|
+
export const isCanvasEnabled = () => {
|
|
35
|
+
const enabled = config?.studio?.enabled;
|
|
36
|
+
return enabled !== false && (isDevMode() || (isSSR() && enabled === true));
|
|
37
|
+
};
|
|
30
38
|
export const isEventCatalogMCPAuthEnabled = () => isEventCatalogMCPEnabled() && (config?.mcp?.auth?.enabled ?? false);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import shell from 'shelljs';
|
|
5
4
|
|
|
6
5
|
type Commit = { timestamp: number; author: string };
|
|
7
6
|
|
|
@@ -9,10 +8,6 @@ const COMMIT_MARKER = '\x1e';
|
|
|
9
8
|
|
|
10
9
|
const newestCommitsByRepository = new Map<string, Map<string, Commit> | undefined>();
|
|
11
10
|
|
|
12
|
-
export function hasGit() {
|
|
13
|
-
return !!shell.which('git');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
11
|
const runGit = (args: string[]) => {
|
|
17
12
|
try {
|
|
18
13
|
return execFileSync('git', ['-c', 'log.showSignature=false', ...args], {
|
|
@@ -25,6 +20,17 @@ const runGit = (args: string[]) => {
|
|
|
25
20
|
}
|
|
26
21
|
};
|
|
27
22
|
|
|
23
|
+
// Checked once per PATH, as git history is read for every page
|
|
24
|
+
const gitInstalledByPath = new Map<string | undefined, boolean>();
|
|
25
|
+
|
|
26
|
+
export function hasGit() {
|
|
27
|
+
const searchPath = process.env.PATH;
|
|
28
|
+
if (!gitInstalledByPath.has(searchPath)) {
|
|
29
|
+
gitInstalledByPath.set(searchPath, runGit(['--version']) !== undefined);
|
|
30
|
+
}
|
|
31
|
+
return gitInstalledByPath.get(searchPath)!;
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
const parseCommit = (header: string): Commit => {
|
|
29
35
|
const [timestamp, ...author] = header.split(',');
|
|
30
36
|
return { timestamp: Number(timestamp) * 1_000, author: author.join(',') };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { Parser } from 'htmlparser2';
|
|
4
4
|
import picomatch from 'picomatch';
|
|
5
5
|
|
|
6
6
|
export type LinkValidationSeverity = 'warn' | 'error' | 'ignore';
|
|
@@ -42,30 +42,36 @@ const decodeUrlPart = (value: string): string => {
|
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
+
// Elements whose contents browsers don't render as markup (<noscript> when scripts run)
|
|
46
|
+
const INERT_ELEMENTS = new Set(['template', 'noscript']);
|
|
47
|
+
|
|
45
48
|
// Parse HTML rather than searching markup with a regex: attributes can contain
|
|
46
49
|
// entities, quoted > characters, or single/unquoted values. Scripts and inert
|
|
47
50
|
// template contents must not be mistaken for rendered links or anchor targets.
|
|
51
|
+
// A streaming parser, not a document tree: pages can be large (island props),
|
|
52
|
+
// and building a tree of each one made link validation a large part of a build.
|
|
48
53
|
const readPage = (html: string, source: string): PageLinks => {
|
|
49
54
|
const page: PageLinks = { source, anchors: new Set(), links: new Set() };
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
let inert = 0;
|
|
56
|
+
const parser = new Parser({
|
|
57
|
+
onopentag(tagName, attrs) {
|
|
58
|
+
if (inert === 0) {
|
|
59
|
+
if (attrs.id) page.anchors.add(attrs.id);
|
|
60
|
+
if (tagName === 'a' && attrs.name) page.anchors.add(attrs.name);
|
|
61
|
+
const href = attrs.href;
|
|
62
|
+
if (href !== undefined) {
|
|
63
|
+
// Parsed in document order, so the first <base href> wins
|
|
64
|
+
if (tagName === 'base' && page.baseHref === undefined) page.baseHref = href;
|
|
65
|
+
if (tagName === 'a' || tagName === 'area') page.links.add(href);
|
|
66
|
+
}
|
|
62
67
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
}
|
|
68
|
+
if (INERT_ELEMENTS.has(tagName)) inert++;
|
|
69
|
+
},
|
|
70
|
+
onclosetag(tagName) {
|
|
71
|
+
if (INERT_ELEMENTS.has(tagName) && inert > 0) inert--;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
parser.end(html);
|
|
69
75
|
return page;
|
|
70
76
|
};
|
|
71
77
|
|
|
@@ -212,7 +212,9 @@ export const getNodesAndEdges = async ({ id, version, entities, type = 'domains'
|
|
|
212
212
|
height: 20,
|
|
213
213
|
},
|
|
214
214
|
});
|
|
215
|
-
} else {
|
|
215
|
+
} else if (!entityMap.has(referencedEntityId)) {
|
|
216
|
+
// Only a reference to an entity missing from the catalog is broken. An external entity's own
|
|
217
|
+
// references can point further away than this map shows: those edges are left out.
|
|
216
218
|
console.warn(`Referenced entity "${referencedEntityId}" not found for ${entity.data.name}.${referenceProperty.name}`);
|
|
217
219
|
}
|
|
218
220
|
}
|
|
@@ -267,7 +267,7 @@ export const buildDomainGraph = ({
|
|
|
267
267
|
draggable: false,
|
|
268
268
|
selectable: false,
|
|
269
269
|
data: {
|
|
270
|
-
domain: { name: d.data.name, version: d.data.version },
|
|
270
|
+
domain: { id: d.data.id, name: d.data.name, version: d.data.version },
|
|
271
271
|
// The domain viewed may itself be a subdomain of another
|
|
272
272
|
subdomain: !!parentId || parentDomainOf.has(d.data.id),
|
|
273
273
|
isFocused: !parentId,
|
|
@@ -309,7 +309,7 @@ export const buildDomainGraph = ({
|
|
|
309
309
|
position: { x: 0, y: 0 },
|
|
310
310
|
...(parentId ? { parentId } : {}),
|
|
311
311
|
data: {
|
|
312
|
-
domain: { name: other.data.name, version: other.data.version },
|
|
312
|
+
domain: { id: other.data.id, name: other.data.name, version: other.data.version },
|
|
313
313
|
subdomain: !!parentId,
|
|
314
314
|
isFocused: false,
|
|
315
315
|
otherDomain: true,
|
|
@@ -375,7 +375,7 @@ export const buildDomainGraph = ({
|
|
|
375
375
|
type: 'system-group',
|
|
376
376
|
parentId: domainGroup,
|
|
377
377
|
position: { x: 0, y: 0 },
|
|
378
|
-
data: { system: { name: system.data.name, version: system.data.version } },
|
|
378
|
+
data: { system: { id: system.data.id, name: system.data.name, version: system.data.version } },
|
|
379
379
|
});
|
|
380
380
|
}
|
|
381
381
|
addNodes(graph.nodes);
|
|
@@ -38,7 +38,6 @@ const NODE_SHAPE_MAP: Record<string, [string, string]> = {
|
|
|
38
38
|
'context-actor': ['((', '))'], // circle
|
|
39
39
|
flows: ['([', '])'], // stadium (rounded)
|
|
40
40
|
flow: ['([', '])'],
|
|
41
|
-
systems: ['[[', ']]'],
|
|
42
41
|
system: ['[[', ']]'],
|
|
43
42
|
step: ['[', ']'], // rectangle
|
|
44
43
|
user: ['((', '))'], // circle
|
|
@@ -79,7 +78,6 @@ const NODE_STYLE_CLASSES: Record<string, string> = {
|
|
|
79
78
|
domains: 'fill:#eab308,stroke:#a16207,color:#000',
|
|
80
79
|
domain: 'fill:#eab308,stroke:#a16207,color:#000',
|
|
81
80
|
'context-domain': 'fill:#eab308,stroke:#a16207,color:#000',
|
|
82
|
-
systems: 'fill:#7c3aed,stroke:#5b21b6,color:#fff',
|
|
83
81
|
'context-actor': 'fill:#8b5cf6,stroke:#6d28d9,color:#fff',
|
|
84
82
|
flows: 'fill:#14b8a6,stroke:#0f766e,color:#fff',
|
|
85
83
|
flow: 'fill:#14b8a6,stroke:#0f766e,color:#fff',
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { hideMessageNodes, layoutWithElk } from '@eventcatalog/visualiser/layout';
|
|
2
|
+
import { isSSR } from '@utils/feature';
|
|
2
3
|
|
|
3
4
|
type Graph = { nodes: any[]; edges: any[] };
|
|
4
5
|
|
|
6
|
+
// A build renders one route at a time (every message's docs page, then every schema page), so the
|
|
7
|
+
// cache has to outlast a whole route to be reused. A server keeps running as the catalog changes and
|
|
8
|
+
// renders pages as they're asked for, so it keeps the most recent layouts only.
|
|
9
|
+
const MAX_CACHED_LAYOUTS = isSSR() ? 200 : 5000;
|
|
10
|
+
const layouts = new Map<string, ReturnType<typeof layoutWithElk>>();
|
|
11
|
+
|
|
5
12
|
/**
|
|
6
13
|
* Lays a node graph out for the visualiser (with ELK, left to right). Builders
|
|
7
14
|
* create their nodes and edges without positions and lay the finished graph
|
|
@@ -9,7 +16,19 @@ type Graph = { nodes: any[]; edges: any[] };
|
|
|
9
16
|
* (nodes with children) are sized to fit them, and edges get the route they're
|
|
10
17
|
* drawn along (`data.route`), with room for their labels.
|
|
11
18
|
*/
|
|
12
|
-
export const layoutNodeGraph = async <G extends Graph>(graph: G): Promise<G> =>
|
|
19
|
+
export const layoutNodeGraph = async <G extends Graph>(graph: G): Promise<G> => {
|
|
20
|
+
// A build lays the same graph out on several pages (a message's docs, visualiser and schema pages):
|
|
21
|
+
// reuse the layout. Each caller gets its own copy, so it can't change the one kept.
|
|
22
|
+
const key = JSON.stringify(graph);
|
|
23
|
+
let layout = layouts.get(key);
|
|
24
|
+
if (!layout) {
|
|
25
|
+
layout = layoutWithElk(graph);
|
|
26
|
+
layouts.set(key, layout);
|
|
27
|
+
layout.catch(() => layouts.delete(key));
|
|
28
|
+
if (layouts.size > MAX_CACHED_LAYOUTS) layouts.delete(layouts.keys().next().value!);
|
|
29
|
+
}
|
|
30
|
+
return structuredClone(await layout) as unknown as G;
|
|
31
|
+
};
|
|
13
32
|
|
|
14
33
|
/**
|
|
15
34
|
* Lays out the graphs of a levels page: level 1 (`overview`: domains, systems
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The catalog resource a diagram node shows, for the node types the node graphs build. Has no imports so it can
|
|
3
|
+
* be shared by the server and the browser (the visualiser's "Open in Studio", the MCP App views).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// The collection each node type shows a resource of
|
|
7
|
+
const NODE_COLLECTIONS: Record<string, string> = {
|
|
8
|
+
service: 'services',
|
|
9
|
+
services: 'services',
|
|
10
|
+
agent: 'agents',
|
|
11
|
+
agents: 'agents',
|
|
12
|
+
event: 'events',
|
|
13
|
+
events: 'events',
|
|
14
|
+
command: 'commands',
|
|
15
|
+
commands: 'commands',
|
|
16
|
+
query: 'queries',
|
|
17
|
+
queries: 'queries',
|
|
18
|
+
channel: 'channels',
|
|
19
|
+
channels: 'channels',
|
|
20
|
+
flow: 'flows',
|
|
21
|
+
flows: 'flows',
|
|
22
|
+
data: 'containers',
|
|
23
|
+
containers: 'containers',
|
|
24
|
+
'data-products': 'data-products',
|
|
25
|
+
domain: 'domains',
|
|
26
|
+
domains: 'domains',
|
|
27
|
+
'context-domain': 'domains',
|
|
28
|
+
'domain-group': 'domains',
|
|
29
|
+
system: 'systems',
|
|
30
|
+
systems: 'systems',
|
|
31
|
+
'system-group': 'systems',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Where each node type keeps the resource it shows
|
|
35
|
+
const RESOURCE_DATA_KEYS = [
|
|
36
|
+
'service',
|
|
37
|
+
'agent',
|
|
38
|
+
'message',
|
|
39
|
+
'channel',
|
|
40
|
+
'data',
|
|
41
|
+
'dataProduct',
|
|
42
|
+
'flow',
|
|
43
|
+
'entity',
|
|
44
|
+
'domain',
|
|
45
|
+
'system',
|
|
46
|
+
'custom',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
type DiagramNode = { type?: string; data?: Record<string, unknown> };
|
|
50
|
+
type ResourceDetails = { id?: unknown; version?: unknown; [key: string]: unknown };
|
|
51
|
+
|
|
52
|
+
/** The resource a node shows, as in its data: the fields shown, or the catalog entry's data (if it holds the entry) */
|
|
53
|
+
export const getNodeResourceDetails = (node: DiagramNode): ResourceDetails | undefined => {
|
|
54
|
+
const data = node.data ?? {};
|
|
55
|
+
const resource = RESOURCE_DATA_KEYS.map((key) => data[key]).find(Boolean) as
|
|
56
|
+
| (ResourceDetails & { data?: ResourceDetails })
|
|
57
|
+
| undefined;
|
|
58
|
+
return resource?.data ?? resource;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** The catalog resource a node shows (its collection, id and version), if it shows one */
|
|
62
|
+
export const getNodeCatalogResource = (node: DiagramNode): { collection: string; id: string; version?: string } | undefined => {
|
|
63
|
+
const collection = NODE_COLLECTIONS[node.type ?? ''];
|
|
64
|
+
const details = getNodeResourceDetails(node);
|
|
65
|
+
if (!collection || typeof details?.id !== 'string') return undefined;
|
|
66
|
+
return { collection, id: details.id, ...(typeof details.version === 'string' && { version: details.version }) };
|
|
67
|
+
};
|
|
@@ -142,7 +142,7 @@ const wrapNodesInSystemGroup = (systemNodes: any[], system: any) => {
|
|
|
142
142
|
draggable: false,
|
|
143
143
|
selectable: false,
|
|
144
144
|
data: {
|
|
145
|
-
system: { name: system.data.name, version: system.data.version },
|
|
145
|
+
system: { id: system.data.id, name: system.data.name, version: system.data.version },
|
|
146
146
|
// The system is the resource being viewed on its own diagram
|
|
147
147
|
isFocused: true,
|
|
148
148
|
},
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import config from '@config';
|
|
2
|
+
|
|
3
|
+
/** The catalog's name: `title` in eventcatalog.config.js, or EventCatalog when it has none */
|
|
4
|
+
export const getCatalogTitle = (): string => config?.title?.trim() || 'EventCatalog';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A page's title, as browser tabs and link previews show it: the page's own title, then the
|
|
8
|
+
* catalog's (`Domains | Ordering | Acme Inc`), so tabs show the page first. A page without a title
|
|
9
|
+
* of its own (the homepage) is just the catalog's.
|
|
10
|
+
*/
|
|
11
|
+
export const formatPageTitle = (title?: string): string => {
|
|
12
|
+
const catalogTitle = getCatalogTitle();
|
|
13
|
+
const pageTitle = title?.trim();
|
|
14
|
+
return pageTitle && pageTitle !== catalogTitle ? `${pageTitle} | ${catalogTitle}` : catalogTitle;
|
|
15
|
+
};
|