@eventcatalog/core 5.0.0-beta.3 → 5.0.0-beta.30
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 +160 -77
- package/dist/analytics/log-build.d.cts +2 -2
- package/dist/analytics/log-build.d.ts +2 -2
- package/dist/analytics/log-build.js +7 -5
- package/dist/analytics/studio-usage.cjs +90 -0
- package/dist/analytics/studio-usage.d.cts +5 -0
- package/dist/analytics/studio-usage.d.ts +5 -0
- package/dist/analytics/studio-usage.js +10 -0
- 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-RFIXKATC.js → chunk-4G7FSRPN.js} +35 -42
- package/dist/{chunk-6OC46WQJ.js → chunk-55QETQZ3.js} +66 -35
- package/dist/{chunk-TQLRG35T.js → chunk-AD7CL772.js} +78 -45
- package/dist/chunk-CA6WCQ2T.js +54 -0
- package/dist/chunk-CPW6G62V.js +17 -0
- package/dist/{chunk-423S2KHN.js → chunk-FLAZ2JVN.js} +4 -2
- package/dist/{chunk-BH2F5WPN.js → chunk-GQI5UYXZ.js} +1 -1
- package/dist/{chunk-GYIVJXOJ.js → chunk-NA7OTBWW.js} +1 -1
- package/dist/{chunk-MDWB5HLO.js → chunk-NKNOXQ62.js} +1 -1
- package/dist/{chunk-3PKS2IIP.js → chunk-OX6TSVHY.js} +1 -1
- package/dist/{chunk-36RYTYT5.js → chunk-PD7KTSYN.js} +1 -1
- package/dist/chunk-S5GWAUG5.js +29 -0
- package/dist/{chunk-NDJOAITI.js → chunk-T3OROMAU.js} +9 -3
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog-config-file-utils.cjs +34 -41
- package/dist/eventcatalog-config-file-utils.d.cts +56 -58
- package/dist/eventcatalog-config-file-utils.d.ts +56 -58
- package/dist/eventcatalog-config-file-utils.js +1 -1
- package/dist/eventcatalog.cjs +531 -356
- package/dist/eventcatalog.config.d.cts +59 -1
- package/dist/eventcatalog.config.d.ts +59 -1
- package/dist/eventcatalog.js +63 -25
- package/dist/features.cjs +34 -41
- package/dist/features.js +2 -2
- package/dist/federation/federate.cjs +37 -44
- package/dist/federation/federate.js +2 -2
- package/dist/generate.cjs +37 -42
- package/dist/generate.js +4 -4
- package/dist/resolve-catalog-dependencies.cjs +34 -41
- package/dist/resolve-catalog-dependencies.js +2 -2
- 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 +78 -45
- package/dist/utils/license-status.d.cts +15 -5
- package/dist/utils/license-status.d.ts +15 -5
- package/dist/utils/license-status.js +1 -1
- package/eventcatalog/astro.config.mjs +23 -1
- package/eventcatalog/integrations/config-loader.mjs +56 -58
- 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/Header.astro +3 -3
- package/eventcatalog/src/components/Homepage/CatalogDashboard.astro +342 -0
- package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +60 -4
- 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/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/Settings/AssistantSettingsForm.tsx +53 -48
- package/eventcatalog/src/components/Settings/GeneralSettingsForm.tsx +183 -178
- package/eventcatalog/src/components/Settings/LicenseSettings.tsx +140 -31
- package/eventcatalog/src/components/Settings/LlmAccessSettingsForm.tsx +40 -35
- package/eventcatalog/src/components/Settings/LogoUpload.tsx +1 -1
- package/eventcatalog/src/components/Settings/McpSettingsForm.tsx +9 -8
- package/eventcatalog/src/components/Settings/Row.tsx +7 -4
- package/eventcatalog/src/components/Settings/SettingsShared.tsx +3 -3
- 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/TrialBadge.astro +20 -9
- package/eventcatalog/src/content.config.ts +19 -2
- 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 +65 -65
- 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 +206 -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/SettingsLayout.astro +2 -2
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +13 -5
- package/eventcatalog/src/pages/_index.astro +5 -342
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +2 -3
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +3 -1
- package/eventcatalog/src/pages/diagrams/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/diagrams/[id]/[version]/embed.astro +1 -0
- package/eventcatalog/src/pages/diagrams/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/directory/[type]/_index.data.ts +5 -7
- package/eventcatalog/src/pages/directory/[type]/index.astro +3 -1
- package/eventcatalog/src/pages/discover/[type]/_index.data.ts +5 -7
- package/eventcatalog/src/pages/discover/[type]/index.astro +3 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +3 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/_[filename].data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/_[...filename].data.ts +0 -4
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +3 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/_[filename].data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +2 -3
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/_[filename].data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/index.astro +2 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/language/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +1 -0
- package/eventcatalog/src/pages/docs/custom/[...path]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/custom/[...path]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/services/[id]/[docType]/[docId]/index.astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +1 -0
- package/eventcatalog/src/pages/docs/services/[id]/asyncapi/[filename].astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/changelog/index.astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/graphql/[filename].astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/spec/[filename].astro +1 -1
- package/eventcatalog/src/pages/docs/teams/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/teams/[id]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/users/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/users/[id]/index.astro +1 -0
- package/eventcatalog/src/pages/index.astro +4 -2
- package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/schemas/explorer/_index.data.ts +0 -7
- package/eventcatalog/src/pages/schemas/explorer/index.astro +4 -2
- package/eventcatalog/src/pages/settings/license.astro +9 -1
- package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/_index.data.ts +38 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +18 -41
- package/eventcatalog/src/pages/visualiser/designs/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +2 -1
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/types/react-syntax-highlighter.d.ts +10 -0
- package/eventcatalog/src/utils/collections/util.ts +8 -4
- package/eventcatalog/src/utils/feature.ts +8 -0
- package/eventcatalog/src/utils/git.ts +22 -5
- package/eventcatalog/src/utils/homepage-dashboard.ts +143 -0
- package/eventcatalog/src/utils/license.ts +1 -1
- 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/field-node-graph.ts +4 -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-loaders/hybrid-page.ts +4 -15
- 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 +14 -1
- package/package.json +19 -13
- package/dist/runtime-dependencies/shelljs.mjs +0 -3
- package/eventcatalog/public/studio-bg.png +0 -0
- package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
- package/eventcatalog/src/pages/studio.astro +0 -316
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ApiCanvas } from './canvases-api';
|
|
2
|
+
import type { DiagramLevelsSpec, NewCanvasEdges, NodeSpecs } from '../canvas-actions';
|
|
3
|
+
|
|
4
|
+
/** Using the Studio API from the browser (Studio's pages), at `apiUrl` (its address on this catalog) */
|
|
5
|
+
|
|
6
|
+
const errorOf = async (response: Response, fallback: string) => {
|
|
7
|
+
const body = (await response.json().catch(() => null)) as { error?: string } | null;
|
|
8
|
+
return new Error(body?.error ?? `${fallback} (${response.status})`);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Creates a canvas, starting with the nodes and edges given, if any */
|
|
12
|
+
export const createCanvasThroughApi = async (
|
|
13
|
+
apiUrl: string,
|
|
14
|
+
canvas: { title?: string; createdBy?: string; nodes?: NodeSpecs; edges?: NewCanvasEdges; levels?: DiagramLevelsSpec }
|
|
15
|
+
) => {
|
|
16
|
+
const response = await fetch(apiUrl, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
19
|
+
body: JSON.stringify(canvas),
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) throw await errorOf(response, 'Could not create the canvas');
|
|
22
|
+
return ((await response.json()) as { canvas: ApiCanvas }).canvas;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Copies a canvas's design to a new draft */
|
|
26
|
+
export const copyCanvasThroughApi = async (apiUrl: string, canvasId: string, copy: { createdBy?: string } = {}) => {
|
|
27
|
+
const response = await fetch(`${apiUrl}/${canvasId}/copy`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
30
|
+
body: JSON.stringify(copy),
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) throw await errorOf(response, 'Could not copy the canvas');
|
|
33
|
+
return ((await response.json()) as { canvas: ApiCanvas }).canvas;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Deletes a canvas (one that's already gone counts as deleted) */
|
|
37
|
+
export const deleteCanvasThroughApi = async (apiUrl: string, canvasId: string) => {
|
|
38
|
+
const response = await fetch(`${apiUrl}/${canvasId}`, { method: 'DELETE', headers: { Accept: 'application/json' } });
|
|
39
|
+
if (!response.ok && response.status !== 404) throw await errorOf(response, 'Could not delete the canvas');
|
|
40
|
+
};
|
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import type { Edge, Node, XYPosition } from '@xyflow/react';
|
|
2
|
+
import type * as Y from 'yjs';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import {
|
|
5
|
+
buildNode,
|
|
6
|
+
getCanvasMaps,
|
|
7
|
+
getCanvasStatus,
|
|
8
|
+
readCanvas,
|
|
9
|
+
updateNodeData,
|
|
10
|
+
type CanvasContent,
|
|
11
|
+
containerFor,
|
|
12
|
+
getAbsolutePosition,
|
|
13
|
+
sizeOf,
|
|
14
|
+
placeNodes,
|
|
15
|
+
type Link,
|
|
16
|
+
canGoInContainer,
|
|
17
|
+
CATALOG_COLLECTIONS,
|
|
18
|
+
COMPONENT_TYPES,
|
|
19
|
+
getNodeDefinition,
|
|
20
|
+
getNodeLabel,
|
|
21
|
+
getNodeName,
|
|
22
|
+
getNoteLevel,
|
|
23
|
+
getResource,
|
|
24
|
+
isGroupType,
|
|
25
|
+
isOnLevel,
|
|
26
|
+
isVersioned,
|
|
27
|
+
isWrittenOnCanvas,
|
|
28
|
+
updateResource,
|
|
29
|
+
} from '@eventcatalog/visualiser/studio';
|
|
30
|
+
import type { CatalogRelation, CatalogResource } from './catalog-resources';
|
|
31
|
+
import {
|
|
32
|
+
buildContainer,
|
|
33
|
+
canBeContainer,
|
|
34
|
+
catalogNodeData,
|
|
35
|
+
containerTypeFor,
|
|
36
|
+
getCatalogLink,
|
|
37
|
+
getRelatedEdges,
|
|
38
|
+
indexRelations,
|
|
39
|
+
} from './catalog';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* What agents can do on a canvas, shared by the MCP tools (on the server) and the WebMCP tools (in the
|
|
43
|
+
* browser), so both describe and change a canvas the same way.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
// ---- Inputs ----
|
|
47
|
+
|
|
48
|
+
export const nodeSpecsSchema = z
|
|
49
|
+
.array(
|
|
50
|
+
z.object({
|
|
51
|
+
ref: z.string().optional().describe('Your name for this node, to use in edges'),
|
|
52
|
+
resource: z
|
|
53
|
+
.object({ collection: z.enum(CATALOG_COLLECTIONS), id: z.string() })
|
|
54
|
+
.optional()
|
|
55
|
+
.describe('A catalog resource to add (latest version)'),
|
|
56
|
+
type: z.enum(COMPONENT_TYPES).optional().describe('For a new component: what it is'),
|
|
57
|
+
name: z.string().optional().describe('For a new component: its name'),
|
|
58
|
+
summary: z.string().optional().describe('For a new component: what it does. For a note or text: its text'),
|
|
59
|
+
version: z.string().optional().describe('For a new component: its version, e.g. 1.0.0 (defaults to 0.0.1)'),
|
|
60
|
+
container: z
|
|
61
|
+
.boolean()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe(
|
|
64
|
+
'For a domain or system from the catalog: add it as a container that other nodes sit inside, rather than a card'
|
|
65
|
+
),
|
|
66
|
+
inside: z
|
|
67
|
+
.string()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('A ref (or node id) of a container (a domain or system added as one) to put this node inside'),
|
|
70
|
+
x: z.number().optional(),
|
|
71
|
+
y: z.number().optional(),
|
|
72
|
+
width: z
|
|
73
|
+
.number()
|
|
74
|
+
.positive()
|
|
75
|
+
.optional()
|
|
76
|
+
.describe('For a container: its width to start with (it grows to fit what is put in it)'),
|
|
77
|
+
height: z
|
|
78
|
+
.number()
|
|
79
|
+
.positive()
|
|
80
|
+
.optional()
|
|
81
|
+
.describe('For a container: its height to start with (it grows to fit what is put in it)'),
|
|
82
|
+
})
|
|
83
|
+
)
|
|
84
|
+
.min(1);
|
|
85
|
+
|
|
86
|
+
export const edgeSpecsSchema = z.array(
|
|
87
|
+
z.object({
|
|
88
|
+
from: z.string().describe('A ref from nodes, or the id of a node already on the canvas'),
|
|
89
|
+
to: z.string().describe('A ref from nodes, or the id of a node already on the canvas'),
|
|
90
|
+
label: z.string().optional().describe('Defaults to EventCatalog wording, e.g. "publishes event"'),
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const pointSchema = z.object({ x: z.number(), y: z.number() });
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Edges for a new canvas (through the Studio API): as add_to_canvas takes them, and how each is drawn, if known
|
|
98
|
+
* (e.g. copied from a diagram): the points it goes through (canvas coordinates, at right angles) and where its
|
|
99
|
+
* label goes. It follows its nodes when they're moved.
|
|
100
|
+
*/
|
|
101
|
+
const routeSchema = z.object({ points: z.array(pointSchema).min(2).max(200), label: pointSchema.optional() });
|
|
102
|
+
export const newCanvasEdgesSchema = z.array(edgeSpecsSchema.element.extend({ route: routeSchema.optional() }));
|
|
103
|
+
export type NewCanvasEdges = z.output<typeof newCanvasEdgesSchema>;
|
|
104
|
+
|
|
105
|
+
const MAX_LEVEL_NODES = 1000;
|
|
106
|
+
const levelSchema = z.object({
|
|
107
|
+
nodes: z
|
|
108
|
+
.array(
|
|
109
|
+
z.object({
|
|
110
|
+
ref: z.string().describe("The ref of the node it is on the canvas (or its own id, if it's only on this level)"),
|
|
111
|
+
type: z.string(),
|
|
112
|
+
data: z.record(z.string(), z.unknown()),
|
|
113
|
+
parent: z.string().optional().describe('The ref of the node it is shown in on this level'),
|
|
114
|
+
x: z.number().describe('Its top-left corner (relative to its parent)'),
|
|
115
|
+
y: z.number(),
|
|
116
|
+
width: z.number().positive().optional().describe('For a container: its size'),
|
|
117
|
+
height: z.number().positive().optional(),
|
|
118
|
+
})
|
|
119
|
+
)
|
|
120
|
+
.max(MAX_LEVEL_NODES),
|
|
121
|
+
edges: z
|
|
122
|
+
.array(
|
|
123
|
+
z.object({
|
|
124
|
+
from: z.string(),
|
|
125
|
+
to: z.string(),
|
|
126
|
+
label: z.string().optional(),
|
|
127
|
+
type: z.string().optional(),
|
|
128
|
+
style: z.record(z.string(), z.unknown()).optional(),
|
|
129
|
+
markerEnd: z.unknown().optional(),
|
|
130
|
+
data: z.record(z.string(), z.unknown()).optional().describe("The edge's data, e.g. its route"),
|
|
131
|
+
})
|
|
132
|
+
)
|
|
133
|
+
.max(MAX_LEVEL_NODES * 2),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* L1 and L2 of the diagram a canvas is opened from, laid out as the diagram showed them (by refs of the nodes given
|
|
138
|
+
* for the canvas): shown as they are until the canvas's structure changes
|
|
139
|
+
*/
|
|
140
|
+
const unavailableReason = z.string().max(300).optional();
|
|
141
|
+
export const diagramLevelsSchema = z.object({
|
|
142
|
+
1: levelSchema.optional(),
|
|
143
|
+
2: levelSchema.optional(),
|
|
144
|
+
unavailable: z
|
|
145
|
+
.object({ 1: unavailableReason, 2: unavailableReason })
|
|
146
|
+
.optional()
|
|
147
|
+
.describe("Levels the diagram doesn't have, and why"),
|
|
148
|
+
});
|
|
149
|
+
export type DiagramLevelsSpec = z.output<typeof diagramLevelsSchema>;
|
|
150
|
+
|
|
151
|
+
/** A diagram's level, with refs swapped for the ids of the nodes made for them */
|
|
152
|
+
const toDiagramLevel = (level: z.output<typeof levelSchema>, idOf: (ref: string) => string) => {
|
|
153
|
+
const shown = new Set(level.nodes.map((node) => node.ref));
|
|
154
|
+
return {
|
|
155
|
+
nodes: level.nodes.map(
|
|
156
|
+
({ ref, type, data, parent, x, y, width, height }): Node => ({
|
|
157
|
+
id: idOf(ref),
|
|
158
|
+
type,
|
|
159
|
+
data,
|
|
160
|
+
position: { x, y },
|
|
161
|
+
...(parent && shown.has(parent) && { parentId: idOf(parent) }),
|
|
162
|
+
...(width && height && { style: { width, height } }),
|
|
163
|
+
})
|
|
164
|
+
),
|
|
165
|
+
edges: level.edges
|
|
166
|
+
.filter((edge) => shown.has(edge.from) && shown.has(edge.to))
|
|
167
|
+
.map(({ from, to, ...edge }, index) => ({ ...edge, id: `diagram-${index}`, source: idOf(from), target: idOf(to) }) as Edge),
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export const ADD_TO_CANVAS_DESCRIPTION = [
|
|
172
|
+
'Adds nodes (and connections between them) to the canvas, live for everyone on it.',
|
|
173
|
+
'A node is either a catalog resource (resource: { collection, id }), shown with its real name and summary and automatically connected to the related resources already on the canvas (e.g. a service to the events it publishes), or a new component (type, name, summary) for things that are not in the catalog yet, like a proposed service.',
|
|
174
|
+
'Give each node a ref to connect new nodes to each other in edges.',
|
|
175
|
+
'Domains and systems can be containers (container: true for catalog ones, or type domain-group / system-group for new ones): put services, messages and data stores inside one with inside (its ref or node id), e.g. a domain container with its system containers inside, and their services inside those.',
|
|
176
|
+
'Leave out x/y and each node is placed like an architect would: next to what it connects to (in edges, or as a related catalog resource), following the flow left to right (producers on the left of their messages, consumers on the right), lined up with it, and stacked when several connect to the same node. Nothing already on the canvas moves, and nodes connected to nothing go below what is there.',
|
|
177
|
+
"Give x/y (a node's centre, like the positions from reading the canvas) only to put a node somewhere specific. Nodes are about 240 wide and 110 tall: leave about 200 between connected nodes for the labels on their connections.",
|
|
178
|
+
].join(' ');
|
|
179
|
+
|
|
180
|
+
// ---- Catalog ----
|
|
181
|
+
|
|
182
|
+
export type CatalogIndex = { resourcesByKey: Map<string, CatalogResource>; relationsByKey: Map<string, CatalogRelation[]> };
|
|
183
|
+
|
|
184
|
+
export const indexCatalog = (resources: CatalogResource[], relations: CatalogRelation[]): CatalogIndex => ({
|
|
185
|
+
resourcesByKey: new Map(resources.map((resource) => [resource.key, resource])),
|
|
186
|
+
relationsByKey: indexRelations(relations),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// ---- Reading ----
|
|
190
|
+
|
|
191
|
+
type NodeLookup = Map<string, Node>;
|
|
192
|
+
const lookupOf = (node: Node, lookup?: NodeLookup) => lookup ?? new Map([[node.id, node]]);
|
|
193
|
+
|
|
194
|
+
/** Something on the canvas, as an agent sees it */
|
|
195
|
+
export const describeNode = (node: Node, lookup?: NodeLookup) => {
|
|
196
|
+
const resource = getResource(node.type, node.data);
|
|
197
|
+
const link = getCatalogLink(node);
|
|
198
|
+
const version = link?.version ?? (typeof resource.version === 'string' ? resource.version : undefined);
|
|
199
|
+
return {
|
|
200
|
+
id: node.id,
|
|
201
|
+
type: getNodeLabel(node.type),
|
|
202
|
+
...(!isWrittenOnCanvas(node.type) && { name: resource.name }),
|
|
203
|
+
...(resource.summary ? { summary: resource.summary } : {}),
|
|
204
|
+
...(isWrittenOnCanvas(node.type) ? { text: node.data.text } : {}),
|
|
205
|
+
// Notes added on L1 or L2 are only shown there (people see the rest at L3)
|
|
206
|
+
...(getNoteLevel(node) !== undefined && getNoteLevel(node) !== 3 && { onLevel: `L${getNoteLevel(node)}` }),
|
|
207
|
+
...(link && { catalogResource: link.key.replace(':', '/') }),
|
|
208
|
+
...(version && { version }),
|
|
209
|
+
/** The node's centre on the canvas, like the x/y agents give */
|
|
210
|
+
position: (({ x, y }) => ({ x: Math.round(x), y: Math.round(y) }))(nodeCenter(node, lookup)),
|
|
211
|
+
size: sizeOf(node),
|
|
212
|
+
...(isGroupType(node.type) && { container: true }),
|
|
213
|
+
...(node.parentId && { inside: node.parentId }),
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/** A node's centre on the canvas (nodes in containers are positioned relative to them) */
|
|
218
|
+
export const nodeCenter = (node: Node, lookup?: NodeLookup) => {
|
|
219
|
+
const { width, height } = sizeOf(node);
|
|
220
|
+
const position = getAbsolutePosition(node, lookupOf(node, lookup));
|
|
221
|
+
return { x: position.x + width / 2, y: position.y + height / 2 };
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/** Where a node's position goes for it to be centred on a canvas point (agents give centres) */
|
|
225
|
+
export const topLeftFor = (node: Node, center: XYPosition, lookup?: NodeLookup) => {
|
|
226
|
+
const { width, height } = sizeOf(node);
|
|
227
|
+
const parent = node.parentId ? lookup?.get(node.parentId) : undefined;
|
|
228
|
+
const parentPosition = parent ? getAbsolutePosition(parent, lookupOf(parent, lookup)) : { x: 0, y: 0 };
|
|
229
|
+
return { x: center.x - width / 2 - parentPosition.x, y: center.y - height / 2 - parentPosition.y };
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/** Everything on a canvas, as an agent sees it */
|
|
233
|
+
export const describeCanvas = (doc: Y.Doc) => {
|
|
234
|
+
const canvas = readCanvas(doc);
|
|
235
|
+
const lookup = new Map(canvas.nodes.map((node) => [node.id, node]));
|
|
236
|
+
const names = new Map(canvas.nodes.map((node) => [node.id, getNodeName(node.type, node.data, node.id)]));
|
|
237
|
+
const lastChange = canvas.meta.statusHistory?.at(-1);
|
|
238
|
+
return {
|
|
239
|
+
title: canvas.meta.title,
|
|
240
|
+
status: getCanvasStatus(canvas.meta),
|
|
241
|
+
...(lastChange && {
|
|
242
|
+
statusChanged: {
|
|
243
|
+
by: lastChange.by.name,
|
|
244
|
+
at: new Date(lastChange.at).toISOString(),
|
|
245
|
+
...(lastChange.note && { note: lastChange.note }),
|
|
246
|
+
},
|
|
247
|
+
}),
|
|
248
|
+
nodes: canvas.nodes.map((node) => describeNode(node, lookup)),
|
|
249
|
+
connections: canvas.edges.map((edge) => ({
|
|
250
|
+
id: edge.id,
|
|
251
|
+
from: edge.source,
|
|
252
|
+
to: edge.target,
|
|
253
|
+
description: `${names.get(edge.source)} ${String(edge.label || '→').replace(/\s+/g, ' ')} ${names.get(edge.target)}`,
|
|
254
|
+
})),
|
|
255
|
+
comments: canvas.threads.map((thread) => ({
|
|
256
|
+
threadId: thread.id,
|
|
257
|
+
...(thread.nodeId ? { onNode: thread.nodeId } : { position: thread.position }),
|
|
258
|
+
resolved: thread.resolved,
|
|
259
|
+
messages: thread.messages.map((message) => ({ from: message.author.name, text: message.text })),
|
|
260
|
+
})),
|
|
261
|
+
};
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// ---- Changing ----
|
|
265
|
+
|
|
266
|
+
export type NodeSpecs = z.output<typeof nodeSpecsSchema>;
|
|
267
|
+
export type EdgeSpecs = z.output<typeof edgeSpecsSchema>;
|
|
268
|
+
/**
|
|
269
|
+
* A node to add: built and placed, with the catalog resource it stands for (to connect it to related ones).
|
|
270
|
+
* Its node's position is relative to its container, as the canvas is now; `canvasPosition` is where it goes on
|
|
271
|
+
* the canvas, which stays the same when containers grow to fit what's added (and move what's in them).
|
|
272
|
+
*/
|
|
273
|
+
export type PlannedNode = { ref?: string; node: Node; name: string; resource?: CatalogResource; canvasPosition: XYPosition };
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Builds the nodes to add (catalog resources, as cards or containers, or new components), and places them:
|
|
277
|
+
* where asked, or next to what they connect to (the edges asked for, and related catalog resources)
|
|
278
|
+
*/
|
|
279
|
+
export const planNodes = (existing: Node[], specs: NodeSpecs, catalog: CatalogIndex, edges: EdgeSpecs = []) => {
|
|
280
|
+
const lookup: NodeLookup = new Map(existing.map((node) => [node.id, node]));
|
|
281
|
+
const refs = new Map<string, Node>();
|
|
282
|
+
const planned: PlannedNode[] = [];
|
|
283
|
+
const toPlace: string[] = [];
|
|
284
|
+
const errors: string[] = [];
|
|
285
|
+
const hasPosition = (spec: { x?: number; y?: number }) => spec.x !== undefined && spec.y !== undefined;
|
|
286
|
+
const centerOf = (spec: { x?: number; y?: number }) => (hasPosition(spec) ? { x: spec.x!, y: spec.y! } : { x: 0, y: 0 });
|
|
287
|
+
// A container at the size asked for, still centred where asked
|
|
288
|
+
const sized = (node: Node, spec: { x?: number; y?: number; width?: number; height?: number }): Node => {
|
|
289
|
+
if (!isGroupType(node.type) || !spec.width || !spec.height) return node;
|
|
290
|
+
const center = centerOf(spec);
|
|
291
|
+
const { width, height } = spec;
|
|
292
|
+
return { ...node, width, height, position: { x: center.x - width / 2, y: center.y - height / 2 } };
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Inside a container, at the canvas point asked for (relative to the container)
|
|
296
|
+
const putInside = (node: Node, parent: Node, spec: { x?: number; y?: number }): Node => {
|
|
297
|
+
if (!hasPosition(spec)) return { ...node, parentId: parent.id };
|
|
298
|
+
const { width, height } = sizeOf(node);
|
|
299
|
+
const parentPosition = getAbsolutePosition(parent, lookup);
|
|
300
|
+
return {
|
|
301
|
+
...node,
|
|
302
|
+
parentId: parent.id,
|
|
303
|
+
position: { x: spec.x! - width / 2 - parentPosition.x, y: spec.y! - height / 2 - parentPosition.y },
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
for (const spec of specs) {
|
|
308
|
+
let built: { node: Node; name: string; resource?: CatalogResource } | undefined;
|
|
309
|
+
if (spec.resource) {
|
|
310
|
+
const resource = catalog.resourcesByKey.get(`${spec.resource.collection}:${spec.resource.id}`);
|
|
311
|
+
if (!resource) {
|
|
312
|
+
errors.push(`No ${spec.resource.collection} resource "${spec.resource.id}" in the catalog`);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const asContainer = spec.container && canBeContainer(resource);
|
|
316
|
+
const node = asContainer
|
|
317
|
+
? sized(buildContainer(resource, centerOf(spec)), spec)
|
|
318
|
+
: buildNode(resource.node.type, catalogNodeData(resource), centerOf(spec));
|
|
319
|
+
built = { node, name: resource.name, resource };
|
|
320
|
+
} else {
|
|
321
|
+
const definition = getNodeDefinition(spec.type);
|
|
322
|
+
if (!definition) {
|
|
323
|
+
errors.push(`Give each node a resource, or a type (one of ${COMPONENT_TYPES.join(', ')})`);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const data = isWrittenOnCanvas(definition.type)
|
|
327
|
+
? { ...definition.createData(), text: spec.summary ?? spec.name ?? '' }
|
|
328
|
+
: updateResource(definition.type, definition.createData(), {
|
|
329
|
+
...(spec.name && { name: spec.name }),
|
|
330
|
+
...(spec.summary && { summary: spec.summary }),
|
|
331
|
+
...(spec.version && isVersioned(definition.type) && { version: spec.version }),
|
|
332
|
+
});
|
|
333
|
+
const node = sized(buildNode(definition.type, data, centerOf(spec)), spec);
|
|
334
|
+
built = { node, name: String(getResource(node.type, node.data).name ?? definition.label) };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (spec.inside && !canGoInContainer(built.node.type)) {
|
|
338
|
+
errors.push(`Notes aren't put in containers: "${spec.ref ?? built.name}" was put on the canvas instead`);
|
|
339
|
+
} else if (spec.inside) {
|
|
340
|
+
const parent = refs.get(spec.inside) ?? lookup.get(spec.inside);
|
|
341
|
+
if (parent && isGroupType(parent.type)) built.node = putInside(built.node, parent, spec);
|
|
342
|
+
else errors.push(`"${spec.inside}" is not a container on the canvas (a domain or system added as a container)`);
|
|
343
|
+
}
|
|
344
|
+
if (!hasPosition(spec)) toPlace.push(built.node.id);
|
|
345
|
+
lookup.set(built.node.id, built.node);
|
|
346
|
+
if (spec.ref) refs.set(spec.ref, built.node);
|
|
347
|
+
planned.push({ ref: spec.ref, ...built, canvasPosition: getAbsolutePosition(built.node, lookup) });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// What the new nodes connect to: the edges asked for, and related catalog resources (as they'll be connected)
|
|
351
|
+
const links = new Map<string, Link>();
|
|
352
|
+
const link = (source: string, target: string) => links.set(`${source}>${target}`, { source, target });
|
|
353
|
+
const nodeIdOf = (refOrId: string) => refs.get(refOrId)?.id ?? refOrId;
|
|
354
|
+
edges.forEach((edge) => link(nodeIdOf(edge.from), nodeIdOf(edge.to)));
|
|
355
|
+
const nodesByKey = new Map<string, Node[]>();
|
|
356
|
+
for (const node of lookup.values()) {
|
|
357
|
+
const key = getCatalogLink(node)?.key;
|
|
358
|
+
if (key) nodesByKey.set(key, [...(nodesByKey.get(key) ?? []), node]);
|
|
359
|
+
}
|
|
360
|
+
for (const { node, resource } of planned) {
|
|
361
|
+
for (const relation of resource ? (catalog.relationsByKey.get(resource.key) ?? []) : []) {
|
|
362
|
+
const outgoing = relation.source === resource?.key;
|
|
363
|
+
for (const other of nodesByKey.get(outgoing ? relation.target : relation.source) ?? []) {
|
|
364
|
+
// Containers hold what they contain, rather than connect to it
|
|
365
|
+
if (relation.label === 'contains' && (isGroupType(node.type) || isGroupType(other.type))) continue;
|
|
366
|
+
if (outgoing) link(node.id, other.id);
|
|
367
|
+
else link(other.id, node.id);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Placed around what's on the canvas people edit (notes on L1 and L2 aren't there)
|
|
373
|
+
const onCanvas = [...lookup.values()].filter((node) => isOnLevel(node, 3));
|
|
374
|
+
const positions = placeNodes(onCanvas, toPlace, [...links.values()]);
|
|
375
|
+
const canvasPositions = new Map(planned.map((entry) => [entry.node.id, positions.get(entry.node.id) ?? entry.canvasPosition]));
|
|
376
|
+
for (const entry of planned) {
|
|
377
|
+
const canvasPosition = positions.get(entry.node.id);
|
|
378
|
+
if (!canvasPosition) continue;
|
|
379
|
+
const { parentId } = entry.node;
|
|
380
|
+
const parent = parentId ? lookup.get(parentId) : undefined;
|
|
381
|
+
const origin = parentId
|
|
382
|
+
? (canvasPositions.get(parentId) ?? (parent ? getAbsolutePosition(parent, lookup) : { x: 0, y: 0 }))
|
|
383
|
+
: { x: 0, y: 0 };
|
|
384
|
+
entry.canvasPosition = canvasPosition;
|
|
385
|
+
entry.node = { ...entry.node, position: { x: canvasPosition.x - origin.x, y: canvasPosition.y - origin.y } };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return { planned, errors };
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
/** Edges given by refs (or ids of nodes already on the canvas), as node ids */
|
|
392
|
+
const refIds = (planned: PlannedNode[]) =>
|
|
393
|
+
new Map(planned.flatMap((entry) => (entry.ref ? [[entry.ref, entry.node.id] as const] : [])));
|
|
394
|
+
|
|
395
|
+
export const resolveEdgeRefs = <Spec extends EdgeSpecs[number]>(edges: Spec[], planned: PlannedNode[]) => {
|
|
396
|
+
const refs = refIds(planned);
|
|
397
|
+
return edges.map((edge) => ({ ...edge, from: refs.get(edge.from) ?? edge.from, to: refs.get(edge.to) ?? edge.to }));
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* A new canvas's content from nodes and edges given as add_to_canvas takes them. Only the connections asked for
|
|
402
|
+
* are made: related catalog resources aren't connected as well, so the canvas shows just what was asked for (e.g.
|
|
403
|
+
* a diagram's connections, which may go through channels). Says what's wrong if anything can't be followed.
|
|
404
|
+
*/
|
|
405
|
+
export const planCanvas = (
|
|
406
|
+
specs: NodeSpecs,
|
|
407
|
+
edges: NewCanvasEdges,
|
|
408
|
+
catalog: CatalogIndex,
|
|
409
|
+
levels?: DiagramLevelsSpec
|
|
410
|
+
): { content: CanvasContent; errors: string[] } => {
|
|
411
|
+
const { planned, errors } = planNodes([], specs, catalog, edges);
|
|
412
|
+
const ids = new Set(planned.map((entry) => entry.node.id));
|
|
413
|
+
const connections = resolveEdgeRefs(edges, planned);
|
|
414
|
+
for (const end of connections.flatMap((connection) => [connection.from, connection.to])) {
|
|
415
|
+
if (!ids.has(end)) errors.push(`Edges connect nodes by their ref: no node has the ref "${end}"`);
|
|
416
|
+
}
|
|
417
|
+
const refs = refIds(planned);
|
|
418
|
+
const idOf = (ref: string) => refs.get(ref) ?? ref;
|
|
419
|
+
const diagramLevels = levels && {
|
|
420
|
+
...(levels[1] && { 1: toDiagramLevel(levels[1], idOf) }),
|
|
421
|
+
...(levels[2] && { 2: toDiagramLevel(levels[2], idOf) }),
|
|
422
|
+
...(levels.unavailable && { unavailable: levels.unavailable }),
|
|
423
|
+
};
|
|
424
|
+
return {
|
|
425
|
+
content: { nodes: planned.map((entry) => entry.node), connections, ...(diagramLevels && { levels: diagramLevels }) },
|
|
426
|
+
errors: [...new Set(errors)],
|
|
427
|
+
};
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
export type NodeUpdate = { nodeId: string; name?: string; summary?: string; version?: string };
|
|
431
|
+
|
|
432
|
+
/** Renames, re-describes or re-versions a node. Catalog resources keep their catalog name, summary and version. */
|
|
433
|
+
export const updateCanvasNode = (doc: Y.Doc, { nodeId, name, summary, version }: NodeUpdate) =>
|
|
434
|
+
doc.transact(() => {
|
|
435
|
+
const node = getCanvasMaps(doc).nodes.get(nodeId);
|
|
436
|
+
if (!node) return { error: `No node "${nodeId}" on the canvas` };
|
|
437
|
+
if (!getCatalogLink(node)) {
|
|
438
|
+
updateNodeData(doc, nodeId, (data) =>
|
|
439
|
+
isWrittenOnCanvas(node.type)
|
|
440
|
+
? { ...data, text: summary ?? name ?? data.text }
|
|
441
|
+
: updateResource(node.type, data, {
|
|
442
|
+
...(name !== undefined && { name }),
|
|
443
|
+
...(summary !== undefined && { summary }),
|
|
444
|
+
...(version !== undefined && isVersioned(node.type) && { version }),
|
|
445
|
+
})
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
return { updated: nodeId };
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// ---- A catalog resource with its connections ----
|
|
452
|
+
|
|
453
|
+
const MESSAGE_COLLECTIONS = new Set(['events', 'commands', 'queries']);
|
|
454
|
+
|
|
455
|
+
/** What a group of connections is: what a service sends, receives and stores, or which services send or receive a message */
|
|
456
|
+
export type ConnectionGroupId = 'receives' | 'sends' | 'dataStores' | 'senders' | 'receivers';
|
|
457
|
+
export type ConnectionGroup = { id: ConnectionGroupId; label: string; resources: CatalogResource[] };
|
|
458
|
+
|
|
459
|
+
const GROUP_LABELS: Record<ConnectionGroupId, string> = {
|
|
460
|
+
receives: 'Messages it receives',
|
|
461
|
+
sends: 'Messages it sends',
|
|
462
|
+
dataStores: 'Data stores it reads or writes',
|
|
463
|
+
senders: 'Services that send it',
|
|
464
|
+
receivers: 'Services that receive it',
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
/** Which group a related resource is in, for a service or a message (or none: not brought with it) */
|
|
468
|
+
const groupOf = (resource: CatalogResource, other: CatalogResource, outgoing: boolean): ConnectionGroupId | undefined => {
|
|
469
|
+
if (resource.collection === 'services') {
|
|
470
|
+
if (other.collection === 'containers') return 'dataStores';
|
|
471
|
+
if (MESSAGE_COLLECTIONS.has(other.collection)) return outgoing ? 'sends' : 'receives';
|
|
472
|
+
}
|
|
473
|
+
if (MESSAGE_COLLECTIONS.has(resource.collection) && other.collection === 'services') return outgoing ? 'receivers' : 'senders';
|
|
474
|
+
return undefined;
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* What a catalog service or message connects to that isn't on the canvas yet, in groups (only the groups with
|
|
479
|
+
* something in them): a service's messages and data stores, or the services that send and receive a message
|
|
480
|
+
*/
|
|
481
|
+
export const getCatalogConnections = (
|
|
482
|
+
resource: CatalogResource,
|
|
483
|
+
catalog: CatalogIndex,
|
|
484
|
+
keysOnCanvas: ReadonlySet<string>
|
|
485
|
+
): ConnectionGroup[] => {
|
|
486
|
+
const groups = new Map<ConnectionGroupId, CatalogResource[]>();
|
|
487
|
+
const seen = new Set<string>();
|
|
488
|
+
for (const relation of catalog.relationsByKey.get(resource.key) ?? []) {
|
|
489
|
+
const outgoing = relation.source === resource.key;
|
|
490
|
+
const other = catalog.resourcesByKey.get(outgoing ? relation.target : relation.source);
|
|
491
|
+
if (!other || seen.has(other.key) || keysOnCanvas.has(other.key)) continue;
|
|
492
|
+
const group = groupOf(resource, other, outgoing);
|
|
493
|
+
if (!group) continue;
|
|
494
|
+
groups.set(group, [...(groups.get(group) ?? []), other]);
|
|
495
|
+
seen.add(other.key);
|
|
496
|
+
}
|
|
497
|
+
// In reading order: what comes in, then what goes out
|
|
498
|
+
return (['receives', 'senders', 'sends', 'receivers', 'dataStores'] as const).flatMap((id) => {
|
|
499
|
+
const resources = groups.get(id);
|
|
500
|
+
return resources ? [{ id, label: GROUP_LABELS[id], resources }] : [];
|
|
501
|
+
});
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* A catalog resource centred on a canvas point (inside the container asked for, else the one there, if any), with
|
|
506
|
+
* the resources it connects to placed around it like an architect would (what comes in before it, what goes out
|
|
507
|
+
* after it), and the connections between them and to what's already on the canvas. A service's data stores go in its container
|
|
508
|
+
* with it. Positions are relative to the containers they're in, as the canvas is now.
|
|
509
|
+
*/
|
|
510
|
+
export const planWithConnections = (
|
|
511
|
+
existing: Node[],
|
|
512
|
+
resource: CatalogResource,
|
|
513
|
+
center: XYPosition,
|
|
514
|
+
include: CatalogResource[],
|
|
515
|
+
catalog: CatalogIndex,
|
|
516
|
+
inside?: string
|
|
517
|
+
): { nodes: Node[]; edges: Edge[] } => {
|
|
518
|
+
const container = containerFor(center, existing, inside);
|
|
519
|
+
const specOf = (entry: CatalogResource) => ({
|
|
520
|
+
resource: { collection: entry.collection as (typeof CATALOG_COLLECTIONS)[number], id: entry.id },
|
|
521
|
+
});
|
|
522
|
+
const { planned } = planNodes(
|
|
523
|
+
existing,
|
|
524
|
+
[
|
|
525
|
+
{ ...specOf(resource), x: center.x, y: center.y, ...(container && { inside: container.id }) },
|
|
526
|
+
...include.map((entry) => ({
|
|
527
|
+
...specOf(entry),
|
|
528
|
+
...(container && entry.collection === 'containers' && { inside: container.id }),
|
|
529
|
+
})),
|
|
530
|
+
],
|
|
531
|
+
catalog
|
|
532
|
+
);
|
|
533
|
+
const placed = [...existing];
|
|
534
|
+
const edges: Edge[] = [];
|
|
535
|
+
for (const { node, resource: entry } of planned) {
|
|
536
|
+
if (entry) edges.push(...getRelatedEdges(entry, node.id, placed, catalog.relationsByKey, node.type));
|
|
537
|
+
placed.push(node);
|
|
538
|
+
}
|
|
539
|
+
return { nodes: planned.map((entry) => entry.node), edges };
|
|
540
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Edge, Node } from '@xyflow/react';
|
|
2
|
+
import { getAbsolutePosition, sizeOf, isGroupType } from '@eventcatalog/visualiser/studio';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A canvas drawn small, for lists of canvases: each node as a box where it is on the canvas (containers as outlines
|
|
6
|
+
* behind what's in them) and each connection as a line between the boxes' middles. In the canvas's own coordinates,
|
|
7
|
+
* starting at 0,0, to draw in an SVG with a viewBox of its width and height.
|
|
8
|
+
*/
|
|
9
|
+
export type CanvasPreview = {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
shapes: { x: number; y: number; width: number; height: number; type: string; container: boolean }[];
|
|
13
|
+
lines: { x1: number; y1: number; x2: number; y2: number }[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Big canvases are drawn from their first few hundred nodes and connections: enough for a thumbnail */
|
|
17
|
+
const MAX_SHAPES = 300;
|
|
18
|
+
const MAX_LINES = 300;
|
|
19
|
+
|
|
20
|
+
export const getCanvasPreview = (nodes: Node[], edges: Edge[]): CanvasPreview | null => {
|
|
21
|
+
if (nodes.length === 0) return null;
|
|
22
|
+
const lookup = new Map(nodes.map((node) => [node.id, node]));
|
|
23
|
+
const boxes = new Map(
|
|
24
|
+
nodes.slice(0, MAX_SHAPES).map((node) => {
|
|
25
|
+
const { x, y } = getAbsolutePosition(node, lookup);
|
|
26
|
+
return [node.id, { x, y, ...sizeOf(node), type: node.type ?? 'node', container: isGroupType(node.type) }];
|
|
27
|
+
})
|
|
28
|
+
);
|
|
29
|
+
const all = [...boxes.values()];
|
|
30
|
+
const left = Math.min(...all.map((box) => box.x));
|
|
31
|
+
const top = Math.min(...all.map((box) => box.y));
|
|
32
|
+
const right = Math.max(...all.map((box) => box.x + box.width));
|
|
33
|
+
const bottom = Math.max(...all.map((box) => box.y + box.height));
|
|
34
|
+
const middle = (id: string) => {
|
|
35
|
+
const box = boxes.get(id);
|
|
36
|
+
return box && { x: box.x - left + box.width / 2, y: box.y - top + box.height / 2 };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
width: Math.max(right - left, 1),
|
|
41
|
+
height: Math.max(bottom - top, 1),
|
|
42
|
+
// Containers first, so what's in them is drawn over them
|
|
43
|
+
shapes: all
|
|
44
|
+
.map((box) => ({ ...box, x: box.x - left, y: box.y - top }))
|
|
45
|
+
.sort((a, b) => Number(b.container) - Number(a.container)),
|
|
46
|
+
lines: edges.slice(0, MAX_LINES).flatMap((edge) => {
|
|
47
|
+
const from = middle(edge.source);
|
|
48
|
+
const to = middle(edge.target);
|
|
49
|
+
return from && to ? [{ x1: from.x, y1: from.y, x2: to.x, y2: to.y }] : [];
|
|
50
|
+
}),
|
|
51
|
+
};
|
|
52
|
+
};
|