@eventcatalog/core 5.9.1 → 5.11.0
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/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/catalog-runtime.cjs +1 -1
- package/dist/{chunk-YBM7MCOY.js → chunk-54GBEFRV.js} +1 -1
- package/dist/{chunk-A3256YCN.js → chunk-C4KPS65M.js} +1 -1
- package/dist/{chunk-JDSFD7IC.js → chunk-EJU3H6C4.js} +1 -1
- package/dist/{chunk-3PWCUWQC.js → chunk-EJXOHDUL.js} +1 -1
- package/dist/{chunk-KGFAIGOT.js → chunk-Y32K66EM.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/docs/development/01-getting-started/develop-and-build.md +19 -0
- package/dist/docs/development/guides/resources/entities/05-entity-maps.md +5 -0
- package/dist/docs/development/guides/resources/entities/06-global-entity-map.md +101 -0
- package/dist/docs/development/guides/resources/entities/{06-reference.md → 07-reference.md} +2 -1
- package/dist/eventcatalog.cjs +4 -4
- package/dist/eventcatalog.js +10 -10
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/MDX/EntityMap/CatalogEntityMap.tsx +301 -0
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +61 -61
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +72 -72
- package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +1 -1
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +2 -2
- package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +1 -1
- package/eventcatalog/src/features/studio/webmcp-relay/README.md +1 -1
- package/eventcatalog/src/features/studio/webmcp-relay/embed.js +1 -2
- package/eventcatalog/src/features/studio/webmcp-relay/widget.html +2 -1
- package/eventcatalog/src/features/studio/webmcp-relay/widget.js +2 -1
- package/eventcatalog/src/pages/visualiser/entity-map/index.astro +47 -0
- package/eventcatalog/src/pages/visualiser/graph/index.astro +2 -2
- package/eventcatalog/src/stores/sidebar-store/state.ts +10 -1
- package/eventcatalog/src/utils/node-graphs/catalog-entity-map-view.ts +82 -0
- package/eventcatalog/src/utils/node-graphs/catalog-entity-map.ts +113 -0
- package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +17 -61
- package/eventcatalog/src/utils/node-graphs/entity-layout.ts +53 -0
- package/eventcatalog/src/utils/node-graphs/flow-lanes.ts +2 -2
- package/package.json +14 -14
|
@@ -5,7 +5,7 @@ import { STATUS, ClaudeCodeLogo, ClaudeLogo, CursorLogo, OpenAILogo } from '@eve
|
|
|
5
5
|
|
|
6
6
|
type CanvasLink = { canvasId: string; title?: string; canvasUrl: string; mcpUrl: string };
|
|
7
7
|
|
|
8
|
-
export const RELAY_COMMAND = 'npx -y @mcp-b/webmcp-local-relay@
|
|
8
|
+
export const RELAY_COMMAND = 'npx -y @mcp-b/webmcp-local-relay@6.0.3';
|
|
9
9
|
const RELAY_KEY = 'eventcatalog-webmcp-relay';
|
|
10
10
|
const WEBMCP_KEY = 'eventcatalog-webmcp';
|
|
11
11
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import type * as Y from 'yjs';
|
|
4
|
-
import {
|
|
4
|
+
import { installWebMCP } from '@mcp-b/webmcp-polyfill';
|
|
5
5
|
import {
|
|
6
6
|
replyToThread,
|
|
7
7
|
setThreadResolved,
|
|
@@ -129,7 +129,7 @@ export function useCanvasWebMcp(options: CanvasWebMcpOptions) {
|
|
|
129
129
|
if (!options.doc) return;
|
|
130
130
|
if (!getModelContext()) {
|
|
131
131
|
if (!options.allowPolyfill) return;
|
|
132
|
-
|
|
132
|
+
installWebMCP();
|
|
133
133
|
usingPolyfill = true;
|
|
134
134
|
}
|
|
135
135
|
const modelContext = getModelContext();
|
|
@@ -6,7 +6,7 @@ import widgetJs from '../webmcp-relay/widget.js?raw';
|
|
|
6
6
|
/**
|
|
7
7
|
* Serves the WebMCP local relay's browser files (embed.js loads widget.html next to it), so the canvas page
|
|
8
8
|
* can hand its WebMCP tools to desktop MCP clients (Claude Code, Cursor...) running
|
|
9
|
-
* `npx -y @mcp-b/webmcp-local-relay@
|
|
9
|
+
* `npx -y @mcp-b/webmcp-local-relay@6.0.3`. The files are copies of that version's (see webmcp-relay/README.md),
|
|
10
10
|
* bundled with the page, so the version is pinned.
|
|
11
11
|
*/
|
|
12
12
|
const FILES: Record<string, { body: string; contentType: string }> = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# WebMCP local relay (browser files)
|
|
2
2
|
|
|
3
|
-
`embed.js`, `widget.html` and `widget.js` are the browser files of [`@mcp-b/webmcp-local-relay`](https://github.com/WebMCP-org/npm-packages/tree/main/packages/webmcp-local-relay) **
|
|
3
|
+
`embed.js`, `widget.html` and `widget.js` are the browser files of [`@mcp-b/webmcp-local-relay`](https://github.com/WebMCP-org/npm-packages/tree/main/packages/webmcp-local-relay) **6.0.3** (MIT, see `LICENSE`), copied as published. Studio serves them at `/webmcp-relay/*` so a canvas can hand its WebMCP tools to desktop MCP clients running `npx -y @mcp-b/webmcp-local-relay@6.0.3`.
|
|
4
4
|
|
|
5
5
|
They're copied rather than installed because the package's Node dependencies (which EventCatalog doesn't run) pin vulnerable versions of `@modelcontextprotocol/core` and `zod`. When moving to a new relay version, copy its `dist/browser` files here and update the version in `ConnectAgentDialog.tsx`.
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
(function(){function e(e){return
|
|
2
|
-
`)}function N(){D=!1;let e=O;T().then(t=>{if(e!==O)return;let n=M(t);n===A||!d||(A=n,d.postMessage({type:`webmcp.tools.changed`,tools:t},f.widgetOrigin))}).catch(e=>{g(`Failed to sync tool changes:`,e)})}function P(){O++,!D&&(D=!0,setTimeout(N,0))}function F(){k||(k=setInterval(P,2e3))}function I(){try{let e=document.modelContext;return e?(e.addEventListener(`toolchange`,P),!0):!1}catch(e){return g(`addEventListener on modelContext threw:`,e),!1}}function L(){if(F(),P(),I())return;let e=0,t=100,n=()=>{setTimeout(()=>{if(e++,!I()){if(e>=40){g(`Could not subscribe to tool changes after 40 retries. Dynamic tool updates will rely on polling.`);return}t=Math.min(Math.round(t*1.5),1e3),n()}},t)};n()}function R(e,t,n){!e||typeof e!=`object`||!(`postMessage`in e)||e.postMessage(n,t)}function z(e){return!s(e)||typeof e.requestId!=`string`||typeof e.type!=`string`?null:{requestId:e.requestId,type:e.type,toolName:e.toolName,args:e.args}}function B(e,t){T().then(n=>{R(t.source,t.origin,{type:`webmcp.tools.list.response`,requestId:e.requestId,tools:n})}).catch(n=>{g(`Failed to list tools:`,n),R(t.source,t.origin,{type:`webmcp.tools.list.response`,requestId:e.requestId,tools:[],error:`Failed to list tools: ${n instanceof Error?n.message:String(n)}`})})}function V(e,t){E(String(e.toolName??``),b(e.args)).then(n=>{R(t.source,t.origin,{type:`webmcp.tools.invoke.response`,requestId:e.requestId,result:s(n)?n:{}})}).catch(n=>{R(t.source,t.origin,{type:`webmcp.tools.invoke.error`,requestId:e.requestId,error:String(n instanceof Error?n.message:n)})})}async function H(e){if(document.querySelector(l))return;let t=new URLSearchParams;t.set(`tabId`,e.tabId),t.set(`hostOrigin`,window.location.origin);let n=new URL(window.location.href);n.search=``,n.hash=``,t.set(`hostUrl`,n.href),t.set(`hostTitle`,document.title||``),t.set(`relayHost`,e.relayHost),t.set(`relayPort`,e.relayPort),t.set(`autoConnect`,e.autoConnect?`true`:`false`),e.relayId&&t.set(`relayId`,e.relayId),e.relayWorkspace&&t.set(`relayWorkspace`,e.relayWorkspace),e.requestTimeout&&t.set(`requestTimeout`,e.requestTimeout);let r=await fetch(e.widgetUrl);if(!r.ok)throw Error(`Widget HTML request failed with status ${String(r.status)}`);let i=await r.text(),a=`<script>window.__WEBMCP_RELAY_CONFIG=${JSON.stringify(Object.fromEntries(t))};<\/script>`,o=URL.createObjectURL(new Blob([i.replace(`</head>`,`${a}</head>`)],{type:`text/html`}));e.widgetOrigin=window.location.origin;let s=document.createElement(`iframe`);s.src=o,s.style.display=`none`,s.setAttribute(`aria-hidden`,`true`),s.setAttribute(`data-webmcp-relay`,`1`),s.setAttribute(`allow`,`loopback-network; local-network; local-network-access`),document.body.appendChild(s),d=s.contentWindow,s.addEventListener(`load`,()=>{d=s.contentWindow,URL.revokeObjectURL(o)}),s.addEventListener(`error`,()=>{console.error(`[webmcp-relay-embed] Failed to load relay widget iframe from:`,s.src,`-- WebMCP tools will NOT be relayed. Check network connectivity and widget URL.`),URL.revokeObjectURL(o)})}if(!document.querySelector(l)){try{f=y(m)}catch(e){throw console.error(`[webmcp-relay-embed] Failed to initialize relay configuration:`,e),e}window.addEventListener(`message`,e=>{if(e.origin!==f.widgetOrigin||!d||e.source!==d)return;let t=e.data;if(s(t)&&t.type===`webmcp.reload`){window.location.reload();return}let n=z(e.data);if(n){if(n.type===`webmcp.tools.list.request`){B(n,e);return}n.type===`webmcp.tools.invoke.request`&&V(n,e)}});let e=()=>{H(f).catch(e=>{console.error(`[webmcp-relay-embed] Failed to inject relay widget:`,e)})};document.body?e():document.addEventListener(`DOMContentLoaded`,e,{once:!0}),L(),document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`visible`&&d&&d.postMessage({type:`webmcp.connect`},f.widgetOrigin)})}})();
|
|
1
|
+
(function(){function e(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}let t=`[data-webmcp-relay]`,n=`__webmcp_relay_tab_id`,r=null,i=document.currentScript instanceof HTMLScriptElement?document.currentScript:null,a=i?i.hasAttribute(`data-debug`):!1;function o(...e){a&&console.warn(`[webmcp-relay-embed]`,...e)}function s(){try{let e=sessionStorage.getItem(n);if(e)return e}catch(e){o(`sessionStorage read failed, tab ID will not persist:`,e)}let e=crypto.randomUUID();try{sessionStorage.setItem(n,e)}catch(e){o(`sessionStorage write failed:`,e)}return e}function c(e){if(!e?.src)throw Error(`The relay embed script must be loaded from a URL`);return{autoConnect:String(e.getAttribute(`data-auto-connect`)!==`false`),relayHost:e.getAttribute(`data-relay-host`)||`127.0.0.1`,relayPort:e.getAttribute(`data-relay-port`)||`9333`,relayId:e.getAttribute(`data-relay-id`)||void 0,relayWorkspace:e.getAttribute(`data-relay-workspace`)||void 0,requestTimeout:e.getAttribute(`data-request-timeout`)||void 0,tabId:s(),widgetUrl:new URL(`widget.html`,e.src).href}}async function l({widgetUrl:e,...n}){if(document.querySelector(t))return;let i=new URL(window.location.href);i.search=``,i.hash=``;let a=JSON.stringify({...n,hostOrigin:window.location.origin,hostUrl:i.href,hostTitle:document.title}).replace(/</g,`\\u003c`),o=await fetch(e);if(!o.ok)throw Error(`Widget HTML request failed with status ${String(o.status)}`);let s=await o.text(),c=`<script>window.__WEBMCP_RELAY_CONFIG=${a};<\/script>`,l=URL.createObjectURL(new Blob([s.replace(`</head>`,()=>`${c}</head>`)],{type:`text/html`})),u=document.createElement(`iframe`);u.src=l,u.style.display=`none`,u.setAttribute(`aria-hidden`,`true`),u.setAttribute(`data-webmcp-relay`,`1`),u.setAttribute(`allow`,`loopback-network; local-network; local-network-access`),document.body.appendChild(u),r=u.contentWindow,u.addEventListener(`load`,()=>{r=u.contentWindow,URL.revokeObjectURL(l)}),u.addEventListener(`error`,()=>{console.error(`[webmcp-relay-embed] Failed to load relay widget iframe from:`,u.src,`-- WebMCP tools will NOT be relayed. Check network connectivity and widget URL.`),URL.revokeObjectURL(l)})}if(!document.querySelector(t)){let t;try{t=c(i)}catch(e){throw console.error(`[webmcp-relay-embed] Failed to initialize relay configuration:`,e),e}window.addEventListener(`message`,t=>{t.origin!==window.location.origin||!r||t.source!==r||e(t.data)&&t.data.type===`webmcp.reload`&&window.location.reload()});let n=()=>{l(t).catch(e=>{console.error(`[webmcp-relay-embed] Failed to inject relay widget:`,e)})};document.body?n():document.addEventListener(`DOMContentLoaded`,n,{once:!0}),document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`visible`&&r&&r.postMessage({type:`webmcp.connect`},window.location.origin)})}})();
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
<title>WebMCP Relay Widget</title>
|
|
6
6
|
</head>
|
|
7
7
|
<body>
|
|
8
|
-
<script>(function(){let e=new Set([`127.0.0.1`,`localhost`,`::1`,`[::1]`]),
|
|
8
|
+
<script>(function(){function e(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function t(t){if(!e(t))return!1;let n=Object.getPrototypeOf(t);return n===Object.prototype||n===null}function n(e,r=new WeakSet){if(e===null||typeof e==`string`||typeof e==`boolean`)return!0;if(typeof e==`number`)return Number.isFinite(e);if(typeof e!=`object`||r.has(e))return!1;r.add(e);try{return(Array.isArray(e)?e:t(e)?Object.values(e):null)?.every(e=>n(e,r))??!1}catch{return!1}finally{r.delete(e)}}function r(t){if(!e(t))return!1;switch(t.type){case`text`:return typeof t.text==`string`;case`image`:case`audio`:return typeof t.data==`string`&&typeof t.mimeType==`string`;case`resource_link`:return typeof t.uri==`string`&&typeof t.name==`string`;case`resource`:{let n=t.resource;return e(n)&&typeof n.uri==`string`&&(typeof n.text==`string`||typeof n.blob==`string`)}default:return!1}}function i(t){return e(t)&&n(t)&&Array.isArray(t.content)&&t.content.every(r)&&(t.isError===void 0||typeof t.isError==`boolean`)&&(t._meta===void 0||e(t._meta))}function a(e){if(typeof e==`string`)return e;try{return JSON.stringify(e)??String(e)}catch{return String(e)}}function o(e){if(i(e))return e;let t={content:[{type:`text`,text:a(e)}],isError:!1};return n(e)&&(t.structuredContent=e),t}let s=new Set([`127.0.0.1`,`localhost`,`::1`,`[::1]`]),c=9333,l=new Set;function u(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function d(e){let t;try{t=JSON.parse(e)}catch{}return u(t)?t.resultType===`input_required`?{isError:!0,content:[{type:`text`,text:`The WebMCP local relay cannot forward MCP input_required results. Multi-round tool flows require direct McpServer registration.`}]}:o(t):o(typeof t==`string`?t:e)}function f(e,t){let n=new Map;for(let r of e){let e=n.get(r.name);e&&(l.has(r.name)||(l.add(r.name),console.warn(`[webmcp-relay-widget] More than one frame registered a tool named "${r.name}". Only one is relayed, preferring the host page's own tool.`)),e.window===t||r.window!==t)||n.set(r.name,r)}return[...n.values()]}function p(e){return s.has(e)}function m(e){return e.replace(/[\r\n]/g,``)}function h(e,t){try{e.readyState===WebSocket.OPEN&&e.send(t)}catch(e){console.warn(`[webmcp-relay] Failed to send message:`,e)}}function g(e){return`__webmcp_relay_endpoint:${[e.hostOrigin,e.relayId??``,e.workspace??``].map(e=>encodeURIComponent(e)).join(`:`)}`}let _=[1e4,2e4,3e4];function v(e=window.location.search){let t=new URLSearchParams(e),n=globalThis.__WEBMCP_RELAY_CONFIG;function r(e){return t.get(e)??n?.[e]??null}let i=r(`hostOrigin`);if(!i)return null;let a=r(`relayHost`)||`127.0.0.1`;if(!p(a))return console.error(`[webmcp-relay-widget] relayHost must be a loopback address, got:`,a),null;let o=r(`relayPort`),s=o?Number(o):c;if(!Number.isInteger(s)||s<1||s>65535)return console.error(`[webmcp-relay-widget] relayPort must be an integer between 1 and 65535, got:`,o),null;let l=r(`requestTimeout`),u=l?Number(l):6e4;return!Number.isInteger(u)||u<1?(console.error(`[webmcp-relay-widget] requestTimeout must be a positive integer (ms), got:`,l),null):{autoConnect:r(`autoConnect`)!==`false`,hostOrigin:i,hostTitle:r(`hostTitle`)||``,hostUrl:r(`hostUrl`)||i,relayHostHint:a,relayId:r(`relayId`)||void 0,relayPortHint:s,relayWorkspace:r(`relayWorkspace`)||void 0,requestTimeoutMs:u,tabId:r(`tabId`)||crypto.randomUUID()}}function y(){let e=v();if(!e){console.warn(`[webmcp-relay-widget] Missing required hostOrigin parameter. Widget will not start.`);return}j(e)}function b(e){if(!u(e)||e.type!==`server-hello`||e.service!==`webmcp-local-relay`||e.version!==1||typeof e.host!=`string`||typeof e.instanceId!=`string`||typeof e.port!=`number`)return null;let t={type:`server-hello`,service:`webmcp-local-relay`,version:1,host:e.host,instanceId:e.instanceId,port:e.port};return typeof e.label==`string`&&(t.label=e.label),typeof e.relayId==`string`&&(t.relayId=e.relayId),typeof e.workspace==`string`&&(t.workspace=e.workspace),t}function x(e){return!u(e)||e.type!==`hello/rejected`||typeof e.message!=`string`||typeof e.reason!=`string`?null:{type:`hello/rejected`,message:e.message,reason:e.reason}}function S(e){return g({hostOrigin:e.hostOrigin,relayId:e.relayId??null,workspace:e.relayWorkspace??null})}function C(e){try{let t=sessionStorage.getItem(S(e));if(!t)return null;let n=JSON.parse(t);return!u(n)||typeof n.host!=`string`||n.host.length===0||typeof n.port!=`number`||!Number.isInteger(n.port)||n.port<1||n.port>65535?null:{host:n.host,port:n.port}}catch{return null}}function w(e,t){try{sessionStorage.setItem(S(e),JSON.stringify({host:t.host,port:t.port}))}catch{}}function T(e){try{sessionStorage.removeItem(S(e))}catch{}}function E(e){let t=C(e),n=new Set,r=[],i=(e,t)=>{let i=`${e}:${String(t)}`;n.has(i)||(n.add(i),r.push({host:e,port:t}))};i(e.relayHostHint,e.relayPortHint),t&&i(t.host,t.port);for(let e of[`127.0.0.1`,`[::1]`])for(let t=c;t<=9348;t+=1)i(e,t);return r}async function D(e){let t=`ws://${e.host.includes(`:`)&&!e.host.startsWith(`[`)?`[${e.host}]`:e.host}:${String(e.port)}`;return new Promise(n=>{let r=!1,i=new WebSocket(t,[`webmcp-discovery.v1`,`webmcp.v1`]),a=e=>{if(!r){if(r=!0,clearTimeout(l),i.removeEventListener(`close`,s),i.removeEventListener(`error`,o),i.removeEventListener(`message`,c),e===null)try{i.close()}catch{}n(e)}},o=()=>{a(null)},s=()=>{a(null)},c=t=>{let n;try{n=JSON.parse(String(t.data))}catch{return}let r=b(n);r&&a({endpoint:{hello:r,host:e.host,port:e.port},socket:i})},l=setTimeout(()=>{a(null)},1e4);i.addEventListener(`open`,()=>{clearTimeout(l),l=setTimeout(()=>{a(null)},1200)},{once:!0}),i.addEventListener(`close`,s,{once:!0}),i.addEventListener(`error`,o,{once:!0}),i.addEventListener(`message`,c)})}function O(e){if(typeof e.inputSchema==`string`)return console.warn(`[webmcp-relay-widget] Tool "${e.name}" was not relayed because this browser's WebMCP predates Chrome 155. Update Chrome, or turn off the WebMCP testing flag to use the polyfill.`),null;if(e.inputSchema!==void 0&&!u(e.inputSchema))return console.warn(`[webmcp-relay-widget] Tool "${e.name}" was not relayed because its input schema is malformed.`),null;let t={name:e.name,title:e.title,description:e.description};return e.inputSchema!==void 0&&(t.inputSchema=e.inputSchema),e.annotations!==void 0&&(t.annotations=e.annotations),t}function k(){return window.parent.document.modelContext}async function A(){let e=k();return e?f(await e.getTools(),window.parent).flatMap(e=>{let t=O(e);return t?[{registered:e,descriptor:t}]:[]}):[]}function j(e){let t=[],n=[],r=0,i=``,a=null,o=null,s=!1,c=!1,l=null,f=null,p=`idle`,g=0,v=null,y=async()=>{for(;;){let e=r,a=await A();if(e!==r)continue;let l=a.map(({descriptor:e})=>e),u=l.map(e=>JSON.stringify(e)).sort().join(`
|
|
9
|
+
`);if(t=a,n=l,u===i)return;i=u,o&&s&&c&&h(o,JSON.stringify({type:`tools/changed`,tools:n}));return}},S=()=>{r++,y().catch(e=>{console.warn(`[webmcp-relay-widget] Failed to refresh WebMCP tools:`,e)})},O=k();typeof O?.addEventListener==`function`&&O.addEventListener(`toolchange`,S),y().catch(e=>{console.warn(`[webmcp-relay-widget] Failed to read WebMCP tools:`,e)});let j=(r,i)=>{let _=()=>{l&&(clearTimeout(l),l=null)},v=()=>{c=!0,h(r,JSON.stringify({type:`tools/list`,tools:n}))};f&&(clearTimeout(f),f=null);let S=new Map;a=i,o=r,s=!1;let C=!1;c=!1,p=`idle`,r.addEventListener(`message`,n=>{let o;try{o=JSON.parse(String(n.data))}catch(e){console.warn(`[webmcp-relay-widget] Failed to parse relay message:`,e);return}let c=b(o);if(c){a={hello:c,host:i.host,port:i.port};return}if(u(o)&&o.type===`hello/accepted`){_(),s=!0,g=0,w(e,i),y().then(v).catch(e=>{console.warn(`[webmcp-relay-widget] Failed to refresh WebMCP tools:`,e),v()});return}let l=x(o);if(l){_(),s=!1,C=!0,T(e),console.error(`[webmcp-relay-widget] Relay rejected browser hello:`,l.reason,l.message);try{r.close(1008,l.message)}catch{}return}if(!u(o)||typeof o.type!=`string`)return;let f=o;if(f.type===`ping`){h(r,JSON.stringify({type:`pong`}));return}if(f.type===`reload`){window.parent.postMessage({type:`webmcp.reload`},e.hostOrigin);return}if(f.type===`cancel`){if(typeof f.callId!=`string`)return;S.get(f.callId)?.abort(Error(`Tool execution cancelled`)),S.delete(f.callId);return}if(f.type!==`invoke`){console.debug(`[webmcp-relay-widget] Ignoring unrecognized message type:`,m(o.type));return}let{callId:p}=f;if(typeof p!=`string`||S.has(p))return;let E=String(f.toolName??``),D=new AbortController;S.set(p,D),(async()=>{await y(),D.signal.throwIfAborted();let n=t.find(({registered:e})=>e.name===E),i=k();if(!n||!i)throw Error(`Tool not found: ${E}`);let a;try{let t=new Promise((e,t)=>{D.signal.addEventListener(`abort`,()=>t(D.signal.reason),{once:!0})});a=setTimeout(()=>{D.abort(Error(`Tool execution timed out`))},e.requestTimeoutMs);let o=i.executeTool(n.registered,u(f.args)?f.args:{},{signal:D.signal}),s=await Promise.race([o,t]);h(r,JSON.stringify({type:`result`,callId:p,result:d(s)}))}finally{clearTimeout(a)}})().catch(e=>{S.get(p)===D&&h(r,JSON.stringify({type:`result`,callId:p,result:{isError:!0,content:[{type:`text`,text:String(e instanceof Error?e.message:e)}]}}))}).finally(()=>{S.get(p)===D&&S.delete(p)})}),r.addEventListener(`close`,()=>{for(let e of S.values())e.abort(Error(`Relay connection closed`));S.clear(),o===r&&(s=!1,o=null,_(),C?I():F())},{once:!0}),r.addEventListener(`error`,e=>{console.warn(`[webmcp-relay-widget] WebSocket error:`,e);try{r.close()}catch(e){console.warn(`[webmcp-relay-widget] Error closing socket after error:`,e)}}),y().catch(e=>{console.warn(`[webmcp-relay-widget] Failed to read WebMCP tools:`,e)}).then(()=>{h(r,JSON.stringify({type:`hello`,tabId:e.tabId,origin:e.hostOrigin,title:e.hostTitle||document.referrer||`Unknown page`,url:e.hostUrl})),l=setTimeout(()=>{l=null,!(o!==r||r.readyState!==WebSocket.OPEN||s)&&(console.warn(`[webmcp-relay-widget] Relay did not acknowledge browser hello`),C=!0,r.close(4e3,`Browser hello was not acknowledged`))},1e3)}).catch(e=>{console.warn(`[webmcp-relay-widget] Hello handshake failed:`,e);try{r.close()}catch{}})},M=t=>(!e.relayId||t.hello.relayId===e.relayId)&&(!e.relayWorkspace||t.hello.workspace===e.relayWorkspace),N=async e=>{let t=await D({host:e.host,port:e.port});return t?M(t.endpoint)?(j(t.socket,t.endpoint),!0):(t.socket.close(),!1):!1},P=async()=>{p=`discovering`;try{let[t,...n]=E(e);if(t&&await N(t))return!0;let r=await Promise.all(n.map(e=>D(e))),i=r.find(e=>e!==null&&M(e.endpoint));for(let e of r)e&&e!==i&&e.socket.close();return i?(j(i.socket,i.endpoint),!0):!1}finally{p===`discovering`&&(p=`idle`)}},F=()=>{if(!a||f)return;let e={host:a.host,port:a.port},t=Math.round(500*(.85+Math.random()*.3));f=setTimeout(()=>{f=null,N(e).then(e=>{e||I()})},t)},I=()=>{if(f)return;let e=_[g];if(e===void 0){z();return}f=setTimeout(()=>{f=null,g+=1,P().then(e=>{e||I()})},e)},L=()=>{document.visibilityState===`visible`&&p===`dormant`&&V()},R=()=>{document.removeEventListener(`visibilitychange`,L),v!==null&&(clearInterval(v),v=null)},z=()=>{p!==`dormant`&&(p=`dormant`,f&&(clearTimeout(f),f=null),document.addEventListener(`visibilitychange`,L),v=setInterval(()=>{B()},12e4))},B=async()=>{if(p!==`dormant`)return;let t=[{host:e.relayHostHint,port:e.relayPortHint}],n=C(e);n&&(n.host!==e.relayHostHint||n.port!==e.relayPortHint)&&t.push(n),p=`discovering`,R();for(let e of t)if(await N(e))return;z()},V=()=>{R(),g=0,P().then(e=>{e||z()})};window.addEventListener(`message`,t=>{if(t.source!==window.parent||t.origin!==e.hostOrigin)return;let n=t.data;u(n)&&n.type===`webmcp.connect`&&(p===`dormant`?V():!o&&p!==`discovering`&&P())}),e.autoConnect&&P().then(e=>{e||I()})}y()})();</script>
|
|
9
10
|
</body>
|
|
10
11
|
</html>
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
(function(){let e=new Set([`127.0.0.1`,`localhost`,`::1`,`[::1]`]),
|
|
1
|
+
(function(){function e(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function t(t){if(!e(t))return!1;let n=Object.getPrototypeOf(t);return n===Object.prototype||n===null}function n(e,r=new WeakSet){if(e===null||typeof e==`string`||typeof e==`boolean`)return!0;if(typeof e==`number`)return Number.isFinite(e);if(typeof e!=`object`||r.has(e))return!1;r.add(e);try{return(Array.isArray(e)?e:t(e)?Object.values(e):null)?.every(e=>n(e,r))??!1}catch{return!1}finally{r.delete(e)}}function r(t){if(!e(t))return!1;switch(t.type){case`text`:return typeof t.text==`string`;case`image`:case`audio`:return typeof t.data==`string`&&typeof t.mimeType==`string`;case`resource_link`:return typeof t.uri==`string`&&typeof t.name==`string`;case`resource`:{let n=t.resource;return e(n)&&typeof n.uri==`string`&&(typeof n.text==`string`||typeof n.blob==`string`)}default:return!1}}function i(t){return e(t)&&n(t)&&Array.isArray(t.content)&&t.content.every(r)&&(t.isError===void 0||typeof t.isError==`boolean`)&&(t._meta===void 0||e(t._meta))}function a(e){if(typeof e==`string`)return e;try{return JSON.stringify(e)??String(e)}catch{return String(e)}}function o(e){if(i(e))return e;let t={content:[{type:`text`,text:a(e)}],isError:!1};return n(e)&&(t.structuredContent=e),t}let s=new Set([`127.0.0.1`,`localhost`,`::1`,`[::1]`]),c=9333,l=new Set;function u(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function d(e){let t;try{t=JSON.parse(e)}catch{}return u(t)?t.resultType===`input_required`?{isError:!0,content:[{type:`text`,text:`The WebMCP local relay cannot forward MCP input_required results. Multi-round tool flows require direct McpServer registration.`}]}:o(t):o(typeof t==`string`?t:e)}function f(e,t){let n=new Map;for(let r of e){let e=n.get(r.name);e&&(l.has(r.name)||(l.add(r.name),console.warn(`[webmcp-relay-widget] More than one frame registered a tool named "${r.name}". Only one is relayed, preferring the host page's own tool.`)),e.window===t||r.window!==t)||n.set(r.name,r)}return[...n.values()]}function p(e){return s.has(e)}function m(e){return e.replace(/[\r\n]/g,``)}function h(e,t){try{e.readyState===WebSocket.OPEN&&e.send(t)}catch(e){console.warn(`[webmcp-relay] Failed to send message:`,e)}}function g(e){return`__webmcp_relay_endpoint:${[e.hostOrigin,e.relayId??``,e.workspace??``].map(e=>encodeURIComponent(e)).join(`:`)}`}let _=[1e4,2e4,3e4];function v(e=window.location.search){let t=new URLSearchParams(e),n=globalThis.__WEBMCP_RELAY_CONFIG;function r(e){return t.get(e)??n?.[e]??null}let i=r(`hostOrigin`);if(!i)return null;let a=r(`relayHost`)||`127.0.0.1`;if(!p(a))return console.error(`[webmcp-relay-widget] relayHost must be a loopback address, got:`,a),null;let o=r(`relayPort`),s=o?Number(o):c;if(!Number.isInteger(s)||s<1||s>65535)return console.error(`[webmcp-relay-widget] relayPort must be an integer between 1 and 65535, got:`,o),null;let l=r(`requestTimeout`),u=l?Number(l):6e4;return!Number.isInteger(u)||u<1?(console.error(`[webmcp-relay-widget] requestTimeout must be a positive integer (ms), got:`,l),null):{autoConnect:r(`autoConnect`)!==`false`,hostOrigin:i,hostTitle:r(`hostTitle`)||``,hostUrl:r(`hostUrl`)||i,relayHostHint:a,relayId:r(`relayId`)||void 0,relayPortHint:s,relayWorkspace:r(`relayWorkspace`)||void 0,requestTimeoutMs:u,tabId:r(`tabId`)||crypto.randomUUID()}}function y(){let e=v();if(!e){console.warn(`[webmcp-relay-widget] Missing required hostOrigin parameter. Widget will not start.`);return}j(e)}function b(e){if(!u(e)||e.type!==`server-hello`||e.service!==`webmcp-local-relay`||e.version!==1||typeof e.host!=`string`||typeof e.instanceId!=`string`||typeof e.port!=`number`)return null;let t={type:`server-hello`,service:`webmcp-local-relay`,version:1,host:e.host,instanceId:e.instanceId,port:e.port};return typeof e.label==`string`&&(t.label=e.label),typeof e.relayId==`string`&&(t.relayId=e.relayId),typeof e.workspace==`string`&&(t.workspace=e.workspace),t}function x(e){return!u(e)||e.type!==`hello/rejected`||typeof e.message!=`string`||typeof e.reason!=`string`?null:{type:`hello/rejected`,message:e.message,reason:e.reason}}function S(e){return g({hostOrigin:e.hostOrigin,relayId:e.relayId??null,workspace:e.relayWorkspace??null})}function C(e){try{let t=sessionStorage.getItem(S(e));if(!t)return null;let n=JSON.parse(t);return!u(n)||typeof n.host!=`string`||n.host.length===0||typeof n.port!=`number`||!Number.isInteger(n.port)||n.port<1||n.port>65535?null:{host:n.host,port:n.port}}catch{return null}}function w(e,t){try{sessionStorage.setItem(S(e),JSON.stringify({host:t.host,port:t.port}))}catch{}}function T(e){try{sessionStorage.removeItem(S(e))}catch{}}function E(e){let t=C(e),n=new Set,r=[],i=(e,t)=>{let i=`${e}:${String(t)}`;n.has(i)||(n.add(i),r.push({host:e,port:t}))};i(e.relayHostHint,e.relayPortHint),t&&i(t.host,t.port);for(let e of[`127.0.0.1`,`[::1]`])for(let t=c;t<=9348;t+=1)i(e,t);return r}async function D(e){let t=`ws://${e.host.includes(`:`)&&!e.host.startsWith(`[`)?`[${e.host}]`:e.host}:${String(e.port)}`;return new Promise(n=>{let r=!1,i=new WebSocket(t,[`webmcp-discovery.v1`,`webmcp.v1`]),a=e=>{if(!r){if(r=!0,clearTimeout(l),i.removeEventListener(`close`,s),i.removeEventListener(`error`,o),i.removeEventListener(`message`,c),e===null)try{i.close()}catch{}n(e)}},o=()=>{a(null)},s=()=>{a(null)},c=t=>{let n;try{n=JSON.parse(String(t.data))}catch{return}let r=b(n);r&&a({endpoint:{hello:r,host:e.host,port:e.port},socket:i})},l=setTimeout(()=>{a(null)},1e4);i.addEventListener(`open`,()=>{clearTimeout(l),l=setTimeout(()=>{a(null)},1200)},{once:!0}),i.addEventListener(`close`,s,{once:!0}),i.addEventListener(`error`,o,{once:!0}),i.addEventListener(`message`,c)})}function O(e){if(typeof e.inputSchema==`string`)return console.warn(`[webmcp-relay-widget] Tool "${e.name}" was not relayed because this browser's WebMCP predates Chrome 155. Update Chrome, or turn off the WebMCP testing flag to use the polyfill.`),null;if(e.inputSchema!==void 0&&!u(e.inputSchema))return console.warn(`[webmcp-relay-widget] Tool "${e.name}" was not relayed because its input schema is malformed.`),null;let t={name:e.name,title:e.title,description:e.description};return e.inputSchema!==void 0&&(t.inputSchema=e.inputSchema),e.annotations!==void 0&&(t.annotations=e.annotations),t}function k(){return window.parent.document.modelContext}async function A(){let e=k();return e?f(await e.getTools(),window.parent).flatMap(e=>{let t=O(e);return t?[{registered:e,descriptor:t}]:[]}):[]}function j(e){let t=[],n=[],r=0,i=``,a=null,o=null,s=!1,c=!1,l=null,f=null,p=`idle`,g=0,v=null,y=async()=>{for(;;){let e=r,a=await A();if(e!==r)continue;let l=a.map(({descriptor:e})=>e),u=l.map(e=>JSON.stringify(e)).sort().join(`
|
|
2
|
+
`);if(t=a,n=l,u===i)return;i=u,o&&s&&c&&h(o,JSON.stringify({type:`tools/changed`,tools:n}));return}},S=()=>{r++,y().catch(e=>{console.warn(`[webmcp-relay-widget] Failed to refresh WebMCP tools:`,e)})},O=k();typeof O?.addEventListener==`function`&&O.addEventListener(`toolchange`,S),y().catch(e=>{console.warn(`[webmcp-relay-widget] Failed to read WebMCP tools:`,e)});let j=(r,i)=>{let _=()=>{l&&(clearTimeout(l),l=null)},v=()=>{c=!0,h(r,JSON.stringify({type:`tools/list`,tools:n}))};f&&(clearTimeout(f),f=null);let S=new Map;a=i,o=r,s=!1;let C=!1;c=!1,p=`idle`,r.addEventListener(`message`,n=>{let o;try{o=JSON.parse(String(n.data))}catch(e){console.warn(`[webmcp-relay-widget] Failed to parse relay message:`,e);return}let c=b(o);if(c){a={hello:c,host:i.host,port:i.port};return}if(u(o)&&o.type===`hello/accepted`){_(),s=!0,g=0,w(e,i),y().then(v).catch(e=>{console.warn(`[webmcp-relay-widget] Failed to refresh WebMCP tools:`,e),v()});return}let l=x(o);if(l){_(),s=!1,C=!0,T(e),console.error(`[webmcp-relay-widget] Relay rejected browser hello:`,l.reason,l.message);try{r.close(1008,l.message)}catch{}return}if(!u(o)||typeof o.type!=`string`)return;let f=o;if(f.type===`ping`){h(r,JSON.stringify({type:`pong`}));return}if(f.type===`reload`){window.parent.postMessage({type:`webmcp.reload`},e.hostOrigin);return}if(f.type===`cancel`){if(typeof f.callId!=`string`)return;S.get(f.callId)?.abort(Error(`Tool execution cancelled`)),S.delete(f.callId);return}if(f.type!==`invoke`){console.debug(`[webmcp-relay-widget] Ignoring unrecognized message type:`,m(o.type));return}let{callId:p}=f;if(typeof p!=`string`||S.has(p))return;let E=String(f.toolName??``),D=new AbortController;S.set(p,D),(async()=>{await y(),D.signal.throwIfAborted();let n=t.find(({registered:e})=>e.name===E),i=k();if(!n||!i)throw Error(`Tool not found: ${E}`);let a;try{let t=new Promise((e,t)=>{D.signal.addEventListener(`abort`,()=>t(D.signal.reason),{once:!0})});a=setTimeout(()=>{D.abort(Error(`Tool execution timed out`))},e.requestTimeoutMs);let o=i.executeTool(n.registered,u(f.args)?f.args:{},{signal:D.signal}),s=await Promise.race([o,t]);h(r,JSON.stringify({type:`result`,callId:p,result:d(s)}))}finally{clearTimeout(a)}})().catch(e=>{S.get(p)===D&&h(r,JSON.stringify({type:`result`,callId:p,result:{isError:!0,content:[{type:`text`,text:String(e instanceof Error?e.message:e)}]}}))}).finally(()=>{S.get(p)===D&&S.delete(p)})}),r.addEventListener(`close`,()=>{for(let e of S.values())e.abort(Error(`Relay connection closed`));S.clear(),o===r&&(s=!1,o=null,_(),C?I():F())},{once:!0}),r.addEventListener(`error`,e=>{console.warn(`[webmcp-relay-widget] WebSocket error:`,e);try{r.close()}catch(e){console.warn(`[webmcp-relay-widget] Error closing socket after error:`,e)}}),y().catch(e=>{console.warn(`[webmcp-relay-widget] Failed to read WebMCP tools:`,e)}).then(()=>{h(r,JSON.stringify({type:`hello`,tabId:e.tabId,origin:e.hostOrigin,title:e.hostTitle||document.referrer||`Unknown page`,url:e.hostUrl})),l=setTimeout(()=>{l=null,!(o!==r||r.readyState!==WebSocket.OPEN||s)&&(console.warn(`[webmcp-relay-widget] Relay did not acknowledge browser hello`),C=!0,r.close(4e3,`Browser hello was not acknowledged`))},1e3)}).catch(e=>{console.warn(`[webmcp-relay-widget] Hello handshake failed:`,e);try{r.close()}catch{}})},M=t=>(!e.relayId||t.hello.relayId===e.relayId)&&(!e.relayWorkspace||t.hello.workspace===e.relayWorkspace),N=async e=>{let t=await D({host:e.host,port:e.port});return t?M(t.endpoint)?(j(t.socket,t.endpoint),!0):(t.socket.close(),!1):!1},P=async()=>{p=`discovering`;try{let[t,...n]=E(e);if(t&&await N(t))return!0;let r=await Promise.all(n.map(e=>D(e))),i=r.find(e=>e!==null&&M(e.endpoint));for(let e of r)e&&e!==i&&e.socket.close();return i?(j(i.socket,i.endpoint),!0):!1}finally{p===`discovering`&&(p=`idle`)}},F=()=>{if(!a||f)return;let e={host:a.host,port:a.port},t=Math.round(500*(.85+Math.random()*.3));f=setTimeout(()=>{f=null,N(e).then(e=>{e||I()})},t)},I=()=>{if(f)return;let e=_[g];if(e===void 0){z();return}f=setTimeout(()=>{f=null,g+=1,P().then(e=>{e||I()})},e)},L=()=>{document.visibilityState===`visible`&&p===`dormant`&&V()},R=()=>{document.removeEventListener(`visibilitychange`,L),v!==null&&(clearInterval(v),v=null)},z=()=>{p!==`dormant`&&(p=`dormant`,f&&(clearTimeout(f),f=null),document.addEventListener(`visibilitychange`,L),v=setInterval(()=>{B()},12e4))},B=async()=>{if(p!==`dormant`)return;let t=[{host:e.relayHostHint,port:e.relayPortHint}],n=C(e);n&&(n.host!==e.relayHostHint||n.port!==e.relayPortHint)&&t.push(n),p=`discovering`,R();for(let e of t)if(await N(e))return;z()},V=()=>{R(),g=0,P().then(e=>{e||z()})};window.addEventListener(`message`,t=>{if(t.source!==window.parent||t.origin!==e.hostOrigin)return;let n=t.data;u(n)&&n.type===`webmcp.connect`&&(p===`dormant`?V():!o&&p!==`discovering`&&P())}),e.autoConnect&&P().then(e=>{e||I()})}y()})();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
import VisualiserLayout from '@layouts/VisualiserLayout.astro';
|
|
3
|
+
import { ClientRouter } from 'astro:transitions';
|
|
4
|
+
// Server side, so the visualiser's styles ship in the page head (see NodeGraph.astro)
|
|
5
|
+
import '@eventcatalog/visualiser/styles-core.css';
|
|
6
|
+
import CatalogEntityMap from '@components/MDX/EntityMap/CatalogEntityMap';
|
|
7
|
+
import { getNodesAndEdges, SHARED_DOMAIN_LANE } from '@utils/node-graphs/catalog-entity-map';
|
|
8
|
+
import { compactVisualiserGraph } from '@utils/node-graphs/compact-visualiser-graph';
|
|
9
|
+
import { isEventCatalogChatVisible, isDevMode, isVisualiserEnabled } from '@utils/feature';
|
|
10
|
+
|
|
11
|
+
if (!isVisualiserEnabled()) return Astro.rewrite('/404');
|
|
12
|
+
|
|
13
|
+
const id = 'entity-map';
|
|
14
|
+
const graph = await getNodesAndEdges();
|
|
15
|
+
const { nodes, edges } = compactVisualiserGraph(graph.nodes as any[], graph.edges as any[]);
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<VisualiserLayout
|
|
19
|
+
title="Visualiser | Global Entity Map"
|
|
20
|
+
description="Every entity in the catalog and how they relate across domains"
|
|
21
|
+
>
|
|
22
|
+
<div id="entity-map-page" class="p-2 flex gap-2 h-[calc(100vh-60px)]">
|
|
23
|
+
<CatalogEntityMap
|
|
24
|
+
client:only="react"
|
|
25
|
+
id={id}
|
|
26
|
+
nodes={nodes}
|
|
27
|
+
edges={edges}
|
|
28
|
+
sharedLaneId={SHARED_DOMAIN_LANE.id}
|
|
29
|
+
title="Global Entity Map"
|
|
30
|
+
mode="full"
|
|
31
|
+
linkTo="docs"
|
|
32
|
+
swimlanes="domain"
|
|
33
|
+
preferenceScope="entity-map"
|
|
34
|
+
isChatEnabled={isEventCatalogChatVisible()}
|
|
35
|
+
isDevMode={isDevMode()}
|
|
36
|
+
syncRenderModeWithUrl
|
|
37
|
+
/>
|
|
38
|
+
<div class="flex-1 min-w-0 relative border border-[rgb(var(--ec-page-border))] rounded-md" id={`${id}-portal`}></div>
|
|
39
|
+
</div>
|
|
40
|
+
<ClientRouter />
|
|
41
|
+
</VisualiserLayout>
|
|
42
|
+
|
|
43
|
+
<style is:global>
|
|
44
|
+
.react-flow__attribution {
|
|
45
|
+
display: none;
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
@@ -16,10 +16,10 @@ const { nodes: wireNodes, links: wireLinks, linkLabels } = toCatalogForceGraphWi
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
<VisualiserLayout title="Visualiser | Architecture Graph" description="Force-directed graph of every resource in the catalog">
|
|
19
|
-
<div id="architecture-graph-page" class="
|
|
19
|
+
<div id="architecture-graph-page" class="p-4">
|
|
20
20
|
<div
|
|
21
21
|
id="architecture-graph-container"
|
|
22
|
-
class="h-[calc(100vh-
|
|
22
|
+
class="h-[calc(100vh-60px-2rem)] w-full relative border border-[rgb(var(--ec-page-border))] rounded-md overflow-hidden"
|
|
23
23
|
>
|
|
24
24
|
<CatalogForceGraph nodes={wireNodes} links={wireLinks} linkLabels={linkLabels} client:only="react" />
|
|
25
25
|
</div>
|
|
@@ -872,7 +872,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
|
|
|
872
872
|
// for any catalog — the group renders when either item has something to show
|
|
873
873
|
const architectureGraphEnabled = isArchitectureGraphEnabled();
|
|
874
874
|
const topLevelDiagramsNode =
|
|
875
|
-
visualiserEnabled && (systems.length > 0 || architectureGraphEnabled)
|
|
875
|
+
visualiserEnabled && (systems.length > 0 || architectureGraphEnabled || entities.length > 0)
|
|
876
876
|
? {
|
|
877
877
|
type: 'group' as const,
|
|
878
878
|
title: 'Top level diagrams',
|
|
@@ -896,6 +896,15 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
|
|
|
896
896
|
},
|
|
897
897
|
]
|
|
898
898
|
: []),
|
|
899
|
+
...(entities.length > 0
|
|
900
|
+
? [
|
|
901
|
+
{
|
|
902
|
+
type: 'item' as const,
|
|
903
|
+
title: 'Global Entity Map',
|
|
904
|
+
href: buildUrl('/visualiser/entity-map'),
|
|
905
|
+
},
|
|
906
|
+
]
|
|
907
|
+
: []),
|
|
899
908
|
],
|
|
900
909
|
}
|
|
901
910
|
: undefined;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Edge, Node } from '@xyflow/react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What the catalog's entity map shows, from the whole map and the viewer's filters. Pure, so it runs in
|
|
5
|
+
* the browser (the page's filters) and in tests.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type Lane = { id: string; name: string };
|
|
9
|
+
|
|
10
|
+
/** Where entities nothing lists are counted, in the list of domains */
|
|
11
|
+
export const NO_DOMAIN: Lane = { id: '__none', name: 'No domain' };
|
|
12
|
+
|
|
13
|
+
export const domainOf = (node: Node): Lane =>
|
|
14
|
+
(node.data as { lanes?: { domain?: Lane } } | undefined)?.lanes?.domain ?? NO_DOMAIN;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* References between entities in different domains. A shared entity (more than one domain has it) is
|
|
18
|
+
* across domains whatever it references. An entity with no domain isn't across anything: it has no
|
|
19
|
+
* domain to cross from.
|
|
20
|
+
*/
|
|
21
|
+
export const getCrossDomainEdges = (nodes: Node[], edges: Edge[], sharedLaneId: string): Edge[] => {
|
|
22
|
+
const domains = new Map(nodes.map((node) => [node.id, domainOf(node)]));
|
|
23
|
+
return edges.filter((edge) => {
|
|
24
|
+
const source = domains.get(edge.source);
|
|
25
|
+
const target = domains.get(edge.target);
|
|
26
|
+
if (!source || !target || source.id === NO_DOMAIN.id || target.id === NO_DOMAIN.id) return false;
|
|
27
|
+
return source.id !== target.id || source.id === sharedLaneId;
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type DomainSummary = { lane: Lane; count: number; crossDomain: number };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Each domain with how many entities it has and how many references across domains they're at either
|
|
35
|
+
* end of. Sorted by name, with the shared entities and those with no domain last.
|
|
36
|
+
*/
|
|
37
|
+
export const getDomainSummaries = (nodes: Node[], crossDomainEdges: Edge[], sharedLaneId: string): DomainSummary[] => {
|
|
38
|
+
const domains = new Map(nodes.map((node) => [node.id, domainOf(node)]));
|
|
39
|
+
const byId = new Map<string, DomainSummary>();
|
|
40
|
+
nodes.forEach((node) => {
|
|
41
|
+
const lane = domainOf(node);
|
|
42
|
+
const entry = byId.get(lane.id) ?? { lane, count: 0, crossDomain: 0 };
|
|
43
|
+
entry.count += 1;
|
|
44
|
+
byId.set(lane.id, entry);
|
|
45
|
+
});
|
|
46
|
+
crossDomainEdges.forEach((edge) => {
|
|
47
|
+
[edge.source, edge.target].forEach((id) => {
|
|
48
|
+
const entry = byId.get(domains.get(id)?.id ?? '');
|
|
49
|
+
if (entry) entry.crossDomain += 1;
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
const last = (id: string) => (id === sharedLaneId ? 1 : id === NO_DOMAIN.id ? 2 : 0);
|
|
53
|
+
return [...byId.values()].sort((a, b) => last(a.lane.id) - last(b.lane.id) || a.lane.name.localeCompare(b.lane.name));
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The entities of the domains not hidden (only those at an end of a reference across domains, when
|
|
58
|
+
* asked) and the references between them. `isEverything` when nothing is filtered out, so the map's
|
|
59
|
+
* own layout can be used.
|
|
60
|
+
*/
|
|
61
|
+
export const getShownGraph = ({
|
|
62
|
+
nodes,
|
|
63
|
+
edges,
|
|
64
|
+
crossDomainEdges,
|
|
65
|
+
hidden,
|
|
66
|
+
crossDomainOnly,
|
|
67
|
+
}: {
|
|
68
|
+
nodes: Node[];
|
|
69
|
+
edges: Edge[];
|
|
70
|
+
crossDomainEdges: Edge[];
|
|
71
|
+
hidden: Set<string>;
|
|
72
|
+
crossDomainOnly: boolean;
|
|
73
|
+
}) => {
|
|
74
|
+
const onCrossDomainEdge = new Set(crossDomainEdges.flatMap((edge) => [edge.source, edge.target]));
|
|
75
|
+
const visible = nodes.filter((node) => !hidden.has(domainOf(node).id) && (!crossDomainOnly || onCrossDomainEdge.has(node.id)));
|
|
76
|
+
const ids = new Set(visible.map((node) => node.id));
|
|
77
|
+
return {
|
|
78
|
+
nodes: visible,
|
|
79
|
+
edges: (crossDomainOnly ? crossDomainEdges : edges).filter((edge) => ids.has(edge.source) && ids.has(edge.target)),
|
|
80
|
+
isEverything: visible.length === nodes.length && !crossDomainOnly,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { getCollection } from 'astro:content';
|
|
2
|
+
import { getEntities, type Entity } from '@utils/collections/entities';
|
|
3
|
+
import { createVersionedMap, satisfies } from '@utils/collections/util';
|
|
4
|
+
import { generateIdForNode } from './utils/utils';
|
|
5
|
+
import { createLaneIndex, isLatest, toLane, type Lane, type StepLanes } from './flow-lanes';
|
|
6
|
+
import {
|
|
7
|
+
ENTITY_TARGET_HANDLE_ID,
|
|
8
|
+
createEntityReferenceEdges,
|
|
9
|
+
getReferencedEntityId,
|
|
10
|
+
getReferencePropertyNames,
|
|
11
|
+
} from './domain-entity-map';
|
|
12
|
+
import { layoutEntityNodes } from './entity-layout';
|
|
13
|
+
|
|
14
|
+
/** The domain lane of an entity more than one domain has: grouped by domain, it gets a box of its own */
|
|
15
|
+
export const SHARED_DOMAIN_LANE: Lane = { id: '__shared', name: 'Shared across domains' };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether a domain, system or service lists this version of the entity (the latest, which the map shows):
|
|
19
|
+
* a pointer without a version, or to `latest`, or one whose version or range it satisfies. A resource that
|
|
20
|
+
* pins an older version doesn't own the latest one (as `getEntities` matches them)
|
|
21
|
+
*/
|
|
22
|
+
const listsEntity = (resource: { data: { entities?: { id: string; version?: string }[] } }, entity: Entity) =>
|
|
23
|
+
(resource.data.entities || []).some((pointer) => {
|
|
24
|
+
if (pointer.id !== entity.data.id) return false;
|
|
25
|
+
if (!pointer.version || pointer.version === 'latest') return true;
|
|
26
|
+
return satisfies(entity.data.version, pointer.version);
|
|
27
|
+
});
|
|
28
|
+
const uniqueLanes = (lanes: (Lane | undefined)[]) => [
|
|
29
|
+
...new Map(lanes.filter((lane): lane is Lane => !!lane).map((lane) => [lane.id, lane])).values(),
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Every entity in the catalog (latest versions) and the references between them, across domains.
|
|
34
|
+
*
|
|
35
|
+
* Entities can be listed by domains, systems and services, so each node says which domain, system and
|
|
36
|
+
* team it belongs to (`data.lanes`), for the visualiser to group them by:
|
|
37
|
+
* - domain: the domain that lists it, or the domain of the system or service that does. An entity more
|
|
38
|
+
* than one domain has is in the "Shared across domains" lane.
|
|
39
|
+
* - system: the system that lists it, or the system of the service that does
|
|
40
|
+
* - team: the entity's owner, or else the owner of what lists it
|
|
41
|
+
*
|
|
42
|
+
* Entities nothing lists have no lanes (`{}`): they stay out of every group rather than taking a
|
|
43
|
+
* neighbour's.
|
|
44
|
+
*/
|
|
45
|
+
export const getNodesAndEdges = async () => {
|
|
46
|
+
const [entities, allEntities, domains, systems, services, teams, users] = await Promise.all([
|
|
47
|
+
getEntities({ getAllVersions: false }),
|
|
48
|
+
getCollection('entities'),
|
|
49
|
+
getCollection('domains'),
|
|
50
|
+
getCollection('systems'),
|
|
51
|
+
getCollection('services'),
|
|
52
|
+
getCollection('teams'),
|
|
53
|
+
getCollection('users'),
|
|
54
|
+
]);
|
|
55
|
+
const entityMap = createVersionedMap(allEntities as Entity[]);
|
|
56
|
+
const laneIndex = createLaneIndex({ domains, systems, teams, users });
|
|
57
|
+
const latestDomains = domains.filter(isLatest);
|
|
58
|
+
const latestSystems = systems.filter(isLatest);
|
|
59
|
+
const latestServices = services.filter(isLatest);
|
|
60
|
+
|
|
61
|
+
const lanesForEntity = (entity: Entity): StepLanes => {
|
|
62
|
+
const owningDomains = latestDomains.filter((domain) => listsEntity(domain, entity));
|
|
63
|
+
const owningSystems = latestSystems.filter((system) => listsEntity(system, entity));
|
|
64
|
+
const owningServices = latestServices.filter((service) => listsEntity(service, entity));
|
|
65
|
+
|
|
66
|
+
const ownerLanes = [
|
|
67
|
+
...owningSystems.map((system) => laneIndex.lanesForSystem(system as any)),
|
|
68
|
+
...owningServices.map((service) => laneIndex.lanesFor(service as any)),
|
|
69
|
+
];
|
|
70
|
+
// A domain listing the entity itself wins over the domains of its systems and services
|
|
71
|
+
const domainLanes = owningDomains.length
|
|
72
|
+
? uniqueLanes(owningDomains.map(toLane))
|
|
73
|
+
: uniqueLanes(ownerLanes.map((lanes) => lanes?.domain));
|
|
74
|
+
const domain = domainLanes.length > 1 ? SHARED_DOMAIN_LANE : domainLanes[0];
|
|
75
|
+
const system = uniqueLanes(ownerLanes.map((lanes) => lanes?.system))[0];
|
|
76
|
+
const team =
|
|
77
|
+
laneIndex.lanesFor(entity as any)?.team ??
|
|
78
|
+
[...owningDomains, ...owningSystems, ...owningServices]
|
|
79
|
+
.map((owner) => laneIndex.lanesFor(owner as any)?.team)
|
|
80
|
+
.find(Boolean);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
...(domain && { domain }),
|
|
84
|
+
...(system && { system }),
|
|
85
|
+
...(team && { team }),
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const nodes = entities.map((entity) => {
|
|
90
|
+
const lanes = lanesForEntity(entity);
|
|
91
|
+
const domain = lanes.domain && lanes.domain.id !== SHARED_DOMAIN_LANE.id ? lanes.domain : undefined;
|
|
92
|
+
return {
|
|
93
|
+
id: generateIdForNode(entity),
|
|
94
|
+
type: 'entities',
|
|
95
|
+
position: { x: 0, y: 0 },
|
|
96
|
+
data: {
|
|
97
|
+
label: entity.data.name,
|
|
98
|
+
entity,
|
|
99
|
+
...(domain && { domainName: domain.name, domainId: domain.id }),
|
|
100
|
+
entityTargetHandle: ENTITY_TARGET_HANDLE_ID,
|
|
101
|
+
referencePropertyNames: getReferencePropertyNames(entity, entityMap),
|
|
102
|
+
lanes,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const entitiesWithReferences = entities.filter((entity) =>
|
|
108
|
+
entity.data.properties?.some((property: any) => getReferencedEntityId(property, entityMap))
|
|
109
|
+
);
|
|
110
|
+
const edges = createEntityReferenceEdges(entitiesWithReferences, entities, entityMap);
|
|
111
|
+
|
|
112
|
+
return { nodes: await layoutEntityNodes(nodes, edges), edges };
|
|
113
|
+
};
|
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
import { getCollection, getEntry } from 'astro:content';
|
|
2
2
|
import { generateIdForNode } from './utils/utils';
|
|
3
|
-
import ELK from 'elkjs/lib/elk.bundled.js';
|
|
4
3
|
import { MarkerType } from '@xyflow/react';
|
|
5
4
|
import { createVersionedMap, findInMap } from '@utils/collections/util';
|
|
6
5
|
import { getVersionFromCollection } from '@utils/collections/versions';
|
|
7
6
|
import { getEntities, type Entity } from '@utils/collections/entities';
|
|
8
7
|
import { getDomains, type Domain } from '@utils/collections/domains';
|
|
9
8
|
import { getServices, type Service } from '@utils/collections/services';
|
|
10
|
-
|
|
11
|
-
const elk = new ELK();
|
|
9
|
+
import { layoutEntityNodes } from './entity-layout';
|
|
12
10
|
|
|
13
11
|
export const ENTITY_TARGET_HANDLE_ID = '__eventcatalog-entity-target';
|
|
14
12
|
|
|
15
|
-
const getReferencedEntityId = (property: any, entityMap: Map<string, Entity[]>) => {
|
|
13
|
+
export const getReferencedEntityId = (property: any, entityMap: Map<string, Entity[]>) => {
|
|
16
14
|
if (property.references) return property.references;
|
|
17
15
|
if (property.type === 'array' && property.items?.type && entityMap.has(property.items.type)) return property.items.type;
|
|
18
16
|
return undefined;
|
|
19
17
|
};
|
|
20
18
|
|
|
21
|
-
const getReferencePropertyNames = (entity: Entity, entityMap: Map<string, Entity[]>) =>
|
|
19
|
+
export const getReferencePropertyNames = (entity: Entity, entityMap: Map<string, Entity[]>) =>
|
|
22
20
|
entity.data.properties
|
|
23
21
|
?.filter((property: any) => getReferencedEntityId(property, entityMap))
|
|
24
22
|
.map((property: any) => property.name) ?? [];
|
|
@@ -151,6 +149,19 @@ export const getNodesAndEdges = async ({ id, version, entities, type = 'domains'
|
|
|
151
149
|
// Create complete list of entities for edge creation and layout
|
|
152
150
|
const allEntitiesInGraph = [...resourceEntities, ...addedExternalEntities];
|
|
153
151
|
|
|
152
|
+
edges = createEntityReferenceEdges(entitiesWithReferences, allEntitiesInGraph, entityMap);
|
|
153
|
+
|
|
154
|
+
return { nodes: await layoutEntityNodes(nodes, edges), edges };
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** The edges between entities: one for each property referencing another entity in the graph */
|
|
158
|
+
export const createEntityReferenceEdges = (
|
|
159
|
+
entitiesWithReferences: Entity[],
|
|
160
|
+
allEntitiesInGraph: Entity[],
|
|
161
|
+
entityMap: Map<string, Entity[]>
|
|
162
|
+
) => {
|
|
163
|
+
const edges: any[] = [];
|
|
164
|
+
|
|
154
165
|
// Go through any entities that are related to other entities
|
|
155
166
|
for (const entity of entitiesWithReferences) {
|
|
156
167
|
// Get a list of properties that reference other entities
|
|
@@ -220,60 +231,5 @@ export const getNodesAndEdges = async ({ id, version, entities, type = 'domains'
|
|
|
220
231
|
}
|
|
221
232
|
}
|
|
222
233
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
// Separate entities with and without relationships (including external entities)
|
|
226
|
-
const entitiesWithRelationships = allEntitiesInGraph.filter((entity) => {
|
|
227
|
-
// Has outgoing references
|
|
228
|
-
const hasOutgoingRefs = entity.data.properties?.some((property: any) => getReferencedEntityId(property, entityMap));
|
|
229
|
-
// Has incoming references (is referenced by others)
|
|
230
|
-
const hasIncomingRefs = entitiesWithReferences.some((e: any) =>
|
|
231
|
-
e.data.properties?.some((prop: any) => getReferencedEntityId(prop, entityMap) === entity.data.id)
|
|
232
|
-
);
|
|
233
|
-
return hasOutgoingRefs || hasIncomingRefs;
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// Prepare ELK graph structure
|
|
237
|
-
const elkNodes = nodes.map((node: any) => ({
|
|
238
|
-
id: node.id,
|
|
239
|
-
width: 220,
|
|
240
|
-
height: Math.max(120, 72 + (node.data.entity.data.properties?.length ?? 0) * 40),
|
|
241
|
-
}));
|
|
242
|
-
|
|
243
|
-
const elkEdges = edges.map((edge: any) => ({
|
|
244
|
-
id: edge.id,
|
|
245
|
-
sources: [edge.source],
|
|
246
|
-
targets: [edge.target],
|
|
247
|
-
}));
|
|
248
|
-
|
|
249
|
-
const elkGraph = {
|
|
250
|
-
id: 'root',
|
|
251
|
-
layoutOptions: {
|
|
252
|
-
'elk.algorithm': 'force',
|
|
253
|
-
'elk.force.iterations': '300',
|
|
254
|
-
'elk.spacing.nodeNode': '45',
|
|
255
|
-
'elk.spacing.componentComponent': '50',
|
|
256
|
-
'elk.padding': '[top=30,left=30,bottom=30,right=30]',
|
|
257
|
-
'elk.separateConnectedComponents': 'true',
|
|
258
|
-
},
|
|
259
|
-
children: elkNodes,
|
|
260
|
-
edges: elkEdges,
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
// Run ELK layout
|
|
264
|
-
const layoutedGraph = await elk.layout(elkGraph);
|
|
265
|
-
|
|
266
|
-
// Apply positions to nodes
|
|
267
|
-
const positionedNodes = nodes.map((node: any) => {
|
|
268
|
-
const elkNode = layoutedGraph.children?.find((n: any) => n.id === node.id);
|
|
269
|
-
return {
|
|
270
|
-
...node,
|
|
271
|
-
position: {
|
|
272
|
-
x: elkNode?.x || 0,
|
|
273
|
-
y: elkNode?.y || 0,
|
|
274
|
-
},
|
|
275
|
-
};
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
return { nodes: positionedNodes, edges };
|
|
234
|
+
return edges;
|
|
279
235
|
};
|