@eventcatalog/core 5.0.0-beta.0 → 5.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +227 -128
- package/dist/analytics/analytics.cjs +19 -3
- package/dist/analytics/analytics.d.cts +1 -1
- package/dist/analytics/analytics.d.ts +1 -1
- package/dist/analytics/analytics.js +3 -2
- package/dist/analytics/log-build.cjs +72 -35
- package/dist/analytics/log-build.js +5 -4
- package/dist/analytics/telemetry.cjs +42 -0
- package/dist/analytics/telemetry.d.cts +20 -0
- package/dist/analytics/telemetry.d.ts +20 -0
- package/dist/analytics/telemetry.js +8 -0
- package/dist/{chunk-ZHKEARLJ.js → chunk-2LBDRHE4.js} +1 -1
- package/dist/{chunk-TQLRG35T.js → chunk-AWOO63ME.js} +71 -45
- package/dist/chunk-CPW6G62V.js +17 -0
- package/dist/{chunk-LR7AGRHN.js → chunk-CRKJBDN7.js} +56 -33
- package/dist/{chunk-CR6AK5LR.js → chunk-EM36OL6A.js} +1 -1
- package/dist/{chunk-TWN43V4T.js → chunk-IONYKKOY.js} +1 -1
- package/dist/{chunk-WAIADKQE.js → chunk-LJMXOEEC.js} +10 -3
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +205 -124
- package/dist/eventcatalog.config.d.cts +59 -1
- package/dist/eventcatalog.config.d.ts +59 -1
- package/dist/eventcatalog.js +30 -11
- package/dist/federation/federate.js +2 -2
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
- package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
- package/dist/runtime-dependencies/manifest.json +8 -1
- package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
- package/dist/runtime-dependencies/yjs.mjs +3 -0
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/dist/utils/license-status.cjs +71 -45
- package/dist/utils/license-status.d.cts +14 -5
- package/dist/utils/license-status.d.ts +14 -5
- package/dist/utils/license-status.js +1 -1
- package/eventcatalog/astro.config.mjs +5 -0
- package/eventcatalog/integrations/storage.mjs +20 -0
- package/eventcatalog/integrations/studio-server.mjs +32 -0
- package/eventcatalog/src/components/Grids/utils.tsx +0 -23
- package/eventcatalog/src/components/Header.astro +3 -3
- package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +6 -1
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +12 -4
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +16 -7
- package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +10 -0
- package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +4 -62
- package/eventcatalog/src/components/Tables/Discover/columns.tsx +17 -150
- package/eventcatalog/src/components/Tables/Resources/ResourcesTable.tsx +425 -0
- package/eventcatalog/src/components/Tables/RowActionsMenu.tsx +157 -0
- package/eventcatalog/src/components/Tables/TablePagination.tsx +82 -0
- package/eventcatalog/src/features/ai/chat-api.ts +3 -3
- package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
- package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
- package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
- package/eventcatalog/src/features/mcp/apps/canvas/styles.css +22 -0
- package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +276 -0
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +210 -0
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +75 -74
- package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
- package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
- package/eventcatalog/src/features/mcp/mcp-server.ts +36 -5
- package/eventcatalog/src/features/server/start.ts +25 -0
- package/eventcatalog/src/features/storage/database.ts +88 -0
- package/eventcatalog/src/features/storage/migrations.ts +85 -0
- package/eventcatalog/src/features/storage/sql/database.sql +5 -0
- package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
- package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
- package/eventcatalog/src/features/storage/sql.ts +28 -0
- package/eventcatalog/src/features/studio/agent-choreography.ts +381 -0
- package/eventcatalog/src/features/studio/api/canvases-api.ts +170 -0
- package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
- package/eventcatalog/src/features/studio/api/client.ts +35 -0
- package/eventcatalog/src/features/studio/canvas-actions.ts +414 -0
- package/eventcatalog/src/features/studio/canvas-doc.ts +414 -0
- package/eventcatalog/src/features/studio/canvas-preview.ts +53 -0
- package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
- package/eventcatalog/src/features/studio/catalog.ts +171 -0
- package/eventcatalog/src/features/studio/clipboard.ts +97 -0
- package/eventcatalog/src/features/studio/components/CanvasControls.tsx +154 -0
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +370 -0
- package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +117 -0
- package/eventcatalog/src/features/studio/components/Comments.tsx +770 -0
- package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +281 -0
- package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +144 -0
- package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +145 -0
- package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +81 -0
- package/eventcatalog/src/features/studio/components/DragGhost.tsx +115 -0
- package/eventcatalog/src/features/studio/components/JoinForm.tsx +34 -0
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +314 -0
- package/eventcatalog/src/features/studio/components/Picture.tsx +21 -0
- package/eventcatalog/src/features/studio/components/Presence.tsx +286 -0
- package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +212 -0
- package/eventcatalog/src/features/studio/components/ShareDialog.tsx +260 -0
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +1216 -0
- package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
- package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +255 -0
- package/eventcatalog/src/features/studio/components/icons.ts +53 -0
- package/eventcatalog/src/features/studio/components/status.ts +53 -0
- package/eventcatalog/src/features/studio/edges.ts +56 -0
- package/eventcatalog/src/features/studio/flow-state.ts +92 -0
- package/eventcatalog/src/features/studio/grouping.ts +226 -0
- package/eventcatalog/src/features/studio/hooks/presence-sender.ts +46 -0
- package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +347 -0
- package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
- package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +104 -0
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +589 -0
- package/eventcatalog/src/features/studio/identity.ts +37 -0
- package/eventcatalog/src/features/studio/layout.ts +42 -0
- package/eventcatalog/src/features/studio/levels.ts +282 -0
- package/eventcatalog/src/features/studio/node-types.ts +205 -0
- package/eventcatalog/src/features/studio/pages/[id].astro +68 -0
- package/eventcatalog/src/features/studio/pages/index.astro +784 -0
- package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
- package/eventcatalog/src/features/studio/placement.ts +197 -0
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +510 -0
- package/eventcatalog/src/features/studio/server/runtime.ts +491 -0
- package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
- package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
- package/eventcatalog/src/features/studio/server/storage.ts +90 -0
- package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
- package/eventcatalog/src/features/studio/studio.css +273 -0
- package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +10 -5
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +52 -63
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +34 -46
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +11 -29
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_ResourcesPage.astro +79 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +32 -14
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +63 -58
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +2 -62
- package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +13 -0
- package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +24 -42
- package/eventcatalog/src/stores/sidebar-store/builders/flow.ts +2 -0
- package/eventcatalog/src/stores/sidebar-store/builders/message.ts +2 -0
- package/eventcatalog/src/stores/sidebar-store/builders/service.ts +23 -23
- package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +13 -0
- package/eventcatalog/src/stores/sidebar-store/builders/system.ts +18 -27
- package/eventcatalog/src/stores/sidebar-store/state.ts +5 -0
- package/eventcatalog/src/styles/tailwind.css +2 -2
- package/eventcatalog/src/utils/collections/channels.ts +56 -1
- package/eventcatalog/src/utils/collections/flows.ts +50 -1
- package/eventcatalog/src/utils/collections/message-connections.ts +60 -0
- package/eventcatalog/src/utils/collections/message-triggers.ts +2 -1
- package/eventcatalog/src/utils/collections/resource-owners.ts +193 -0
- package/eventcatalog/src/utils/collections/resources.ts +310 -0
- package/eventcatalog/src/utils/collections/util.ts +31 -0
- package/eventcatalog/src/utils/feature.ts +8 -0
- package/eventcatalog/src/utils/git.ts +11 -5
- package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
- package/eventcatalog/src/utils/trial.ts +1 -1
- package/package.json +17 -12
- package/dist/runtime-dependencies/shelljs.mjs +0 -3
- package/eventcatalog/public/studio-bg.png +0 -0
- package/eventcatalog/src/components/Grids/DomainGrid.tsx +0 -838
- package/eventcatalog/src/components/Grids/MessageGrid.tsx +0 -344
- package/eventcatalog/src/components/Grids/SystemGrid.tsx +0 -185
- package/eventcatalog/src/components/Grids/components.tsx +0 -170
- package/eventcatalog/src/components/Grids/message-link.ts +0 -38
- package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
- package/eventcatalog/src/components/Tables/SystemResources/SystemResourcesTable.tsx +0 -294
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.spec.ts +0 -65
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.ts +0 -46
- package/eventcatalog/src/pages/studio.astro +0 -316
- package/dist/{chunk-AYNZXUB5.js → chunk-KHACCTW2.js} +3 -3
|
@@ -127,10 +127,10 @@ The user will ask you some questions about the software architecture catalog, yo
|
|
|
127
127
|
|
|
128
128
|
At point the referer url (${referrer}) will be the URL of the page the user is on, you should use this to help you answer the question,
|
|
129
129
|
You may be able to get the resource from the URL
|
|
130
|
-
- Example if the url is like /docs|visualiser
|
|
130
|
+
- Example if the url is like /docs|visualiser/{collection}/{id}/{version}
|
|
131
131
|
- (e.g /docs/events/MyEvent/1.0.0) in this case the id is MyEvent and the version is 1.0.0 and collection is events.
|
|
132
132
|
- (e.g /visualiser/domains/MyDomain/1.0.0) in this case the id is MyDomain and the version is 1.0.0 and collection is domains.
|
|
133
|
-
- (e.g /
|
|
133
|
+
- (e.g /docs/services/MyService/1.0.0/resources) in this case the id is MyService and the version is 1.0.0 and collection is services; the user is looking at the resources the service uses.
|
|
134
134
|
|
|
135
135
|
The referer URL is: ${referrer}
|
|
136
136
|
|
|
@@ -159,7 +159,7 @@ When responding:
|
|
|
159
159
|
- The link options are:
|
|
160
160
|
- If you want to get the documentation for a resource use the /docs/ prefix (e.g /docs/{collection}/{id}/{version})
|
|
161
161
|
- If you want to let the user know they can visualize a resource use the /visualiser/ prefix (e.g /visualiser/{collection}/{id}/{version})
|
|
162
|
-
- If you want to
|
|
162
|
+
- If you want to show the user everything a domain, system, service, flow, channel or message (event, command, query) contains, uses or connects to, link its Resources page (e.g /docs/{collection}/{id}/{version}/resources). Only those collections have a Resources page.
|
|
163
163
|
- If you don't know the version, use the getResource tool to fetch the resource and get the actual version number before creating the link.
|
|
164
164
|
7. When you return a schema, use code blocks to render the schema to the user too, for example if the schema is in JSON format use \`\`\`json and if the schema is in YAML format use \`\`\`yaml
|
|
165
165
|
8. IMPORTANT: After answering each question, ALWAYS use the suggestFollowUpQuestions tool to suggest 2-3 relevant follow-up questions the user might want to ask next. These should be contextual to the conversation and help the user explore related topics.
|
|
@@ -96,6 +96,14 @@ export function isMcpRoute(pathname: string) {
|
|
|
96
96
|
return pathname === '/docs/mcp' || pathname.startsWith('/docs/mcp/');
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
// The Studio API answers in JSON, rather than sending callers to the sign-in page
|
|
100
|
+
const isStudioApiRoute = (pathname: string) => pathname === '/api/studio' || pathname.startsWith('/api/studio/');
|
|
101
|
+
const signInRequired = () =>
|
|
102
|
+
new Response(JSON.stringify({ error: 'Sign in to use this API' }), {
|
|
103
|
+
status: 401,
|
|
104
|
+
headers: { 'Content-Type': 'application/json' },
|
|
105
|
+
});
|
|
106
|
+
|
|
99
107
|
export const authMiddleware: MiddlewareHandler = async (context, next) => {
|
|
100
108
|
const { request, redirect, locals } = context;
|
|
101
109
|
const url = new URL(request.url);
|
|
@@ -131,6 +139,7 @@ export const authMiddleware: MiddlewareHandler = async (context, next) => {
|
|
|
131
139
|
const session = await getSession(request);
|
|
132
140
|
|
|
133
141
|
if (!session) {
|
|
142
|
+
if (isStudioApiRoute(pathname)) return signInRequired();
|
|
134
143
|
const callbackUrl = encodeURIComponent(pathname + url.search);
|
|
135
144
|
return redirect(`/auth/login?callbackUrl=${callbackUrl}`);
|
|
136
145
|
}
|
|
@@ -157,6 +166,7 @@ export const authMiddleware: MiddlewareHandler = async (context, next) => {
|
|
|
157
166
|
typedLocals.matchesPattern = matchesPattern;
|
|
158
167
|
} catch (error) {
|
|
159
168
|
console.error('Session error:', error);
|
|
169
|
+
if (isStudioApiRoute(pathname)) return signInRequired();
|
|
160
170
|
const callbackUrl = encodeURIComponent(pathname + url.search);
|
|
161
171
|
return redirect(`/auth/login?callbackUrl=${callbackUrl}`);
|
|
162
172
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
isFullCatalogAPIEnabled,
|
|
14
14
|
isDevMode,
|
|
15
15
|
isSSR,
|
|
16
|
+
isCanvasEnabled,
|
|
16
17
|
} from '../../utils/feature';
|
|
17
18
|
import { getCustomPageRoutes, isApiRoute, listCustomPageFiles, resolveCustomPagesPrefix } from '../custom-pages/routes';
|
|
18
19
|
|
|
@@ -111,7 +112,8 @@ export default function eventCatalogIntegration(): AstroIntegration {
|
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
// Handle routes for MCP Server (requires SSR)
|
|
114
|
-
|
|
115
|
+
// Also for canvases, so agents can join them (the canvas tools are on the MCP server)
|
|
116
|
+
if (isEventCatalogMCPEnabled() || isCanvasEnabled()) {
|
|
115
117
|
params.injectRoute({
|
|
116
118
|
pattern: '/docs/mcp/[...path]',
|
|
117
119
|
entrypoint: path.join(packageDirectory, 'src/features/mcp/mcp-server.ts'),
|
|
@@ -173,6 +175,37 @@ export default function eventCatalogIntegration(): AstroIntegration {
|
|
|
173
175
|
entrypoint: path.join(packageDirectory, 'src/features/visualizer-layout/reset.ts'),
|
|
174
176
|
});
|
|
175
177
|
}
|
|
178
|
+
|
|
179
|
+
// What a server starts before anyone asks for it (the storage configured, migrated; Studio), called by
|
|
180
|
+
// `eventcatalog start` once it listens
|
|
181
|
+
if (isSSR()) {
|
|
182
|
+
params.injectRoute({
|
|
183
|
+
pattern: '/_eventcatalog/start',
|
|
184
|
+
entrypoint: path.join(packageDirectory, 'src/features/server/start.ts'),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Collaborative canvases in EventCatalog Studio (/studio lists them, /studio?new=1 starts one). Their WebSocket
|
|
189
|
+
// server is in integrations/studio-server.mjs.
|
|
190
|
+
if (isCanvasEnabled()) {
|
|
191
|
+
params.injectRoute({
|
|
192
|
+
pattern: '/studio',
|
|
193
|
+
entrypoint: path.join(packageDirectory, 'src/features/studio/pages/index.astro'),
|
|
194
|
+
});
|
|
195
|
+
params.injectRoute({
|
|
196
|
+
pattern: '/studio/[id]',
|
|
197
|
+
entrypoint: path.join(packageDirectory, 'src/features/studio/pages/[id].astro'),
|
|
198
|
+
});
|
|
199
|
+
// The Studio API (canvases as a whole: listing, creating, deleting)
|
|
200
|
+
params.injectRoute({
|
|
201
|
+
pattern: '/api/studio/canvases/[...path]',
|
|
202
|
+
entrypoint: path.join(packageDirectory, 'src/features/studio/api/canvases.ts'),
|
|
203
|
+
});
|
|
204
|
+
params.injectRoute({
|
|
205
|
+
pattern: '/webmcp-relay/[file]',
|
|
206
|
+
entrypoint: path.join(packageDirectory, 'src/features/studio/pages/webmcp-relay.ts'),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
176
209
|
},
|
|
177
210
|
'astro:server:setup': ({ server }) => {
|
|
178
211
|
// When custom page files appear or disappear, refresh the manifest.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Shared by the canvas MCP App (client) and the MCP server: no server-only imports here
|
|
2
|
+
import type { CatalogRelation, CatalogResource } from '../../../studio/catalog-resources';
|
|
3
|
+
import type { CanvasStatus } from '../../../studio/canvas-doc';
|
|
4
|
+
|
|
5
|
+
export const CANVAS_RESOURCE_URI = 'ui://eventcatalog/canvas.html';
|
|
6
|
+
/** Key of the payload in openCanvas results' `_meta` */
|
|
7
|
+
export const CANVAS_META_KEY = 'eventcatalog/canvas';
|
|
8
|
+
/** App-only tool the view loads the canvas from when the host doesn't pass the result's `_meta` */
|
|
9
|
+
export const CANVAS_VIEW_TOOL = 'getCanvasView';
|
|
10
|
+
/** App-only tool the view loads the catalog (for dragging resources in) from */
|
|
11
|
+
export const CANVAS_CATALOG_TOOL = 'getCanvasCatalog';
|
|
12
|
+
/** App-only tool the view syncs through when the chat's sandbox blocks the collaboration WebSocket */
|
|
13
|
+
export const CANVAS_SYNC_TOOL = 'syncCanvas';
|
|
14
|
+
|
|
15
|
+
/** A canvas listed in the view's picker */
|
|
16
|
+
export type CanvasListItem = {
|
|
17
|
+
canvasId: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
status: CanvasStatus;
|
|
20
|
+
people: string[];
|
|
21
|
+
nodeCount: number;
|
|
22
|
+
openComments: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* What the view shows: a canvas (it connects to it itself, so people and agents edit it live), or, when opened
|
|
27
|
+
* without one (e.g. from ChatGPT's sidebar), a list of canvases to open or start.
|
|
28
|
+
*/
|
|
29
|
+
export type CanvasPayload = CanvasView | CanvasHome;
|
|
30
|
+
|
|
31
|
+
export type CanvasHome = { view: 'home'; canvases: CanvasListItem[] };
|
|
32
|
+
|
|
33
|
+
export type CanvasView = {
|
|
34
|
+
view: 'canvas';
|
|
35
|
+
canvasId: string;
|
|
36
|
+
title?: string;
|
|
37
|
+
/** The collaboration WebSocket, e.g. wss://catalog.example.com/_eventcatalog/studio */
|
|
38
|
+
socketUrl: string;
|
|
39
|
+
/** The canvas in EventCatalog */
|
|
40
|
+
canvasUrl: string;
|
|
41
|
+
/** The EventCatalog MCP server, for "Connect your agent" */
|
|
42
|
+
mcpUrl: string;
|
|
43
|
+
/** What to call the user on the canvas, when the agent knows their name */
|
|
44
|
+
userName?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type CanvasCatalog = { resources: CatalogResource[]; relations: CatalogRelation[] };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The canvas view's Tailwind stylesheet (with Tailwind's base reset) and EventCatalog's theme variables,
|
|
3
|
+
* built from the classes of the collaborative canvas components it renders.
|
|
4
|
+
*/
|
|
5
|
+
@import 'tailwindcss' source(none);
|
|
6
|
+
@import '../../../../styles/theme.css';
|
|
7
|
+
@import '../../../studio/studio.css';
|
|
8
|
+
|
|
9
|
+
@source './*.tsx';
|
|
10
|
+
@source '../../../studio/components/*.{ts,tsx}';
|
|
11
|
+
|
|
12
|
+
body {
|
|
13
|
+
margin: 0;
|
|
14
|
+
background: rgb(var(--ec-page-bg));
|
|
15
|
+
color: rgb(var(--ec-page-text));
|
|
16
|
+
font-family:
|
|
17
|
+
ui-sans-serif,
|
|
18
|
+
system-ui,
|
|
19
|
+
-apple-system,
|
|
20
|
+
'Segoe UI',
|
|
21
|
+
sans-serif;
|
|
22
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The collaborative canvas as an MCP App: openCanvas shows it in the chat (ChatGPT, Claude...), where it
|
|
3
|
+
* joins the canvas (over the collaboration WebSocket, or through tool calls when the chat's sandbox blocks it).
|
|
4
|
+
* The user edits it there with everyone else on it, people in EventCatalog and agents, live.
|
|
5
|
+
*
|
|
6
|
+
* Opened without a canvas (e.g. from ChatGPT's sidebar or a thread tab, its entrypoints), it lists the canvases
|
|
7
|
+
* to open one or start one. Follows the OpenAI MCP extensions for ChatGPT: entrypoints, display modes, deep
|
|
8
|
+
* links (/canvas/<id>) and model context (background context, and the selection as titled attachments).
|
|
9
|
+
*
|
|
10
|
+
* Built into a single HTML file by scripts/build-mcp-apps.mjs and served as a ui:// resource.
|
|
11
|
+
*/
|
|
12
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
13
|
+
import { ArrowLeft, ExternalLink, Maximize2, Minimize2, Plus, Users } from 'lucide-react';
|
|
14
|
+
import type { App } from '@modelcontextprotocol/ext-apps';
|
|
15
|
+
import styles from './styles.css?inline';
|
|
16
|
+
import visualiserStyles from '@eventcatalog/visualiser/styles.css?inline';
|
|
17
|
+
import StudioDesigner, { type SelectedNode } from '@features/studio/components/StudioDesigner';
|
|
18
|
+
import { CANVAS_STATUS_LOOK, STATUS } from '@features/studio/components/status';
|
|
19
|
+
import { getToolResultPayload, mountView, useMcpAppView } from '../shared/app-view';
|
|
20
|
+
import {
|
|
21
|
+
CANVAS_CATALOG_TOOL,
|
|
22
|
+
CANVAS_META_KEY,
|
|
23
|
+
CANVAS_SYNC_TOOL,
|
|
24
|
+
CANVAS_VIEW_TOOL,
|
|
25
|
+
type CanvasCatalog,
|
|
26
|
+
type CanvasHome,
|
|
27
|
+
type CanvasPayload,
|
|
28
|
+
type CanvasView,
|
|
29
|
+
} from './shared';
|
|
30
|
+
import type { SyncResponse, ToolSync } from '@features/studio/tool-sync';
|
|
31
|
+
|
|
32
|
+
const isCanvasPayload = (payload?: CanvasPayload): payload is CanvasPayload =>
|
|
33
|
+
payload?.view === 'home' ? Array.isArray(payload.canvases) : Boolean(payload?.canvasId && payload.socketUrl);
|
|
34
|
+
|
|
35
|
+
const VIEW_OPTIONS = {
|
|
36
|
+
name: 'EventCatalog canvas',
|
|
37
|
+
metaKey: CANVAS_META_KEY,
|
|
38
|
+
viewTool: CANVAS_VIEW_TOOL,
|
|
39
|
+
isPayload: isCanvasPayload,
|
|
40
|
+
getViewToolArguments: ({ canvasId }: Record<string, any>) => ({ canvasId }),
|
|
41
|
+
loadErrorMessage: 'The canvas could not be opened',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const INLINE_HEIGHT = 640;
|
|
45
|
+
/** Deep links into the app (ChatGPT's `openai/deepLink`), e.g. /canvas/<id> */
|
|
46
|
+
const DEEP_LINK = /^\/canvas(?:es)?\/([0-9a-f-]{36})/i;
|
|
47
|
+
|
|
48
|
+
/** Loads what to show through a tool (the canvas view's own, or openCanvas to start a canvas) */
|
|
49
|
+
const loadPayload = async (app: App, name: string, args: Record<string, unknown>) => {
|
|
50
|
+
const result = await app.callServerTool({ name, arguments: args });
|
|
51
|
+
const payload = getToolResultPayload<CanvasPayload>(result, CANVAS_META_KEY);
|
|
52
|
+
if (result.isError || !isCanvasPayload(payload)) throw new Error('The canvas could not be opened');
|
|
53
|
+
return payload;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function CanvasPicker({
|
|
57
|
+
home,
|
|
58
|
+
onOpen,
|
|
59
|
+
onCreate,
|
|
60
|
+
}: {
|
|
61
|
+
home: CanvasHome;
|
|
62
|
+
onOpen: (id: string) => void;
|
|
63
|
+
onCreate: (title: string) => void;
|
|
64
|
+
}) {
|
|
65
|
+
const [title, setTitle] = useState('');
|
|
66
|
+
return (
|
|
67
|
+
<div className="mx-auto max-w-2xl space-y-6 p-6">
|
|
68
|
+
<div className="space-y-1">
|
|
69
|
+
<h1 className="text-lg font-semibold">Canvases</h1>
|
|
70
|
+
<p className="text-sm text-[rgb(var(--ec-page-text-muted))]">
|
|
71
|
+
Work together, live: people in EventCatalog, people in chats and agents, on the same canvas.
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
<form
|
|
75
|
+
className="flex gap-2"
|
|
76
|
+
onSubmit={(event) => {
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
if (title.trim()) onCreate(title.trim());
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
<input
|
|
82
|
+
value={title}
|
|
83
|
+
onChange={(event) => setTitle(event.target.value)}
|
|
84
|
+
placeholder="What are you designing?"
|
|
85
|
+
className="min-w-0 flex-1 rounded-lg border px-3 py-2 text-sm bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))] text-[rgb(var(--ec-input-text))]"
|
|
86
|
+
/>
|
|
87
|
+
<button
|
|
88
|
+
type="submit"
|
|
89
|
+
disabled={!title.trim()}
|
|
90
|
+
className="flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium disabled:opacity-50 bg-[rgb(var(--ec-button-bg))] text-[rgb(var(--ec-button-text))] hover:bg-[rgb(var(--ec-button-bg-hover))]"
|
|
91
|
+
>
|
|
92
|
+
<Plus size={16} /> New canvas
|
|
93
|
+
</button>
|
|
94
|
+
</form>
|
|
95
|
+
<div className="space-y-2">
|
|
96
|
+
{home.canvases.length === 0 && <p className="text-sm text-[rgb(var(--ec-page-text-muted))]">No canvases yet.</p>}
|
|
97
|
+
{home.canvases.map((canvas) => (
|
|
98
|
+
<button
|
|
99
|
+
key={canvas.canvasId}
|
|
100
|
+
onClick={() => onOpen(canvas.canvasId)}
|
|
101
|
+
className="flex w-full items-center justify-between gap-3 rounded-xl border px-4 py-3 text-left bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] hover:border-[rgb(var(--ec-accent))]"
|
|
102
|
+
>
|
|
103
|
+
<span className="min-w-0">
|
|
104
|
+
<span className="flex items-center gap-2">
|
|
105
|
+
<span className="truncate text-sm font-semibold">{canvas.title ?? 'Untitled canvas'}</span>
|
|
106
|
+
<span
|
|
107
|
+
className={`shrink-0 rounded-full border px-1.5 py-px text-[10px] font-semibold ${CANVAS_STATUS_LOOK[canvas.status].pill}`}
|
|
108
|
+
>
|
|
109
|
+
{CANVAS_STATUS_LOOK[canvas.status].label}
|
|
110
|
+
</span>
|
|
111
|
+
</span>
|
|
112
|
+
<span className="text-xs text-[rgb(var(--ec-page-text-muted))]">
|
|
113
|
+
{canvas.nodeCount} {canvas.nodeCount === 1 ? 'node' : 'nodes'}
|
|
114
|
+
{canvas.openComments > 0 && ` · ${canvas.openComments} open comments`}
|
|
115
|
+
</span>
|
|
116
|
+
</span>
|
|
117
|
+
{canvas.people.length > 0 && (
|
|
118
|
+
<span
|
|
119
|
+
className={`flex shrink-0 items-center gap-1 text-xs ${STATUS.success.text}`}
|
|
120
|
+
title={canvas.people.join(', ')}
|
|
121
|
+
>
|
|
122
|
+
<Users size={14} /> {canvas.people.length} here now
|
|
123
|
+
</span>
|
|
124
|
+
)}
|
|
125
|
+
</button>
|
|
126
|
+
))}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function CanvasApp() {
|
|
133
|
+
const { app, payload, setPayload, error, hostContext } = useMcpAppView(VIEW_OPTIONS);
|
|
134
|
+
const [catalog, setCatalog] = useState<CanvasCatalog>({ resources: [], relations: [] });
|
|
135
|
+
const [selection, setSelection] = useState<SelectedNode[]>([]);
|
|
136
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
137
|
+
const canvas = payload?.view === 'canvas' ? payload : undefined;
|
|
138
|
+
|
|
139
|
+
const show = useCallback(
|
|
140
|
+
(load: Promise<CanvasPayload>) =>
|
|
141
|
+
load.then(setPayload).catch((cause) => setLoadError(String((cause as Error)?.message ?? cause))),
|
|
142
|
+
[setPayload]
|
|
143
|
+
);
|
|
144
|
+
const openCanvas = (canvasId?: string) => app && show(loadPayload(app, CANVAS_VIEW_TOOL, canvasId ? { canvasId } : {}));
|
|
145
|
+
|
|
146
|
+
// Deep links (ChatGPT's `openai/deepLink`): /canvas/<id> opens that canvas
|
|
147
|
+
const deepLink = (hostContext as Record<string, { url?: string } | undefined> | undefined)?.['openai/deepLink']?.url;
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
const canvasId = deepLink?.match(DEEP_LINK)?.[1];
|
|
150
|
+
if (app && canvasId && canvasId !== canvas?.canvasId) void show(loadPayload(app, CANVAS_VIEW_TOOL, { canvasId }));
|
|
151
|
+
}, [app, deepLink]);
|
|
152
|
+
|
|
153
|
+
// Chats may not let the view open the collaboration WebSocket (sandbox CSP, local network rules...):
|
|
154
|
+
// then it syncs through the MCP server, the same way the model's tools reach the canvas
|
|
155
|
+
const syncViaTools = useMemo<ToolSync | undefined>(
|
|
156
|
+
() =>
|
|
157
|
+
app
|
|
158
|
+
? async (request) => {
|
|
159
|
+
const result = await app.callServerTool({ name: CANVAS_SYNC_TOOL, arguments: request });
|
|
160
|
+
if (result.isError || !result.structuredContent) throw new Error('Sync failed');
|
|
161
|
+
return result.structuredContent as SyncResponse;
|
|
162
|
+
}
|
|
163
|
+
: undefined,
|
|
164
|
+
[app]
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// The catalog (to drag resources in) is loaded separately, to keep tool results small
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (!app || !canvas) return;
|
|
170
|
+
app
|
|
171
|
+
.callServerTool({ name: CANVAS_CATALOG_TOOL, arguments: {} })
|
|
172
|
+
.then((result) => {
|
|
173
|
+
const loaded = result.structuredContent as CanvasCatalog | undefined;
|
|
174
|
+
if (loaded?.resources) setCatalog(loaded);
|
|
175
|
+
})
|
|
176
|
+
.catch(() => {});
|
|
177
|
+
}, [app, canvas?.canvasId]);
|
|
178
|
+
|
|
179
|
+
// Model context (replaced on each update): which canvas is open as background context the user doesn't see,
|
|
180
|
+
// and what they've selected as an attachment they can see (and remove), titled with the names
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (!app || !payload || !app.getHostCapabilities()?.updateModelContext) return;
|
|
183
|
+
const background = {
|
|
184
|
+
type: 'text' as const,
|
|
185
|
+
text: canvas
|
|
186
|
+
? `The user has the canvas "${canvas.title ?? 'Untitled'}" open (canvasId ${canvas.canvasId}). Use getCanvas to see what is on it, and the canvas tools to work on it with them.`
|
|
187
|
+
: 'The user is looking at their canvases, to open one or start one.',
|
|
188
|
+
annotations: { audience: ['assistant' as const] },
|
|
189
|
+
};
|
|
190
|
+
const selected = selection.length
|
|
191
|
+
? [
|
|
192
|
+
{
|
|
193
|
+
type: 'text' as const,
|
|
194
|
+
text: `Selected on the canvas: ${selection
|
|
195
|
+
.map(
|
|
196
|
+
(node) =>
|
|
197
|
+
`${node.name} (${node.type}${node.catalogResource ? `, catalog ${node.catalogResource}` : ''}, nodeId ${node.id})`
|
|
198
|
+
)
|
|
199
|
+
.join('; ')}`,
|
|
200
|
+
_meta: { 'openai/title': selection.map((node) => node.name).join(', ') },
|
|
201
|
+
},
|
|
202
|
+
]
|
|
203
|
+
: [];
|
|
204
|
+
app.updateModelContext({ content: [background, ...selected] }).catch(() => {});
|
|
205
|
+
}, [app, payload, selection]);
|
|
206
|
+
|
|
207
|
+
if (error || loadError) return <p className="p-4 text-sm">{error ?? loadError}</p>;
|
|
208
|
+
if (!payload) return <p className="p-4 text-sm text-[rgb(var(--ec-page-text-muted))]">Opening the canvas…</p>;
|
|
209
|
+
|
|
210
|
+
const fullscreen = hostContext?.displayMode === 'fullscreen';
|
|
211
|
+
const canToggle = hostContext?.availableDisplayModes?.includes(fullscreen ? 'inline' : 'fullscreen');
|
|
212
|
+
const iconButton =
|
|
213
|
+
'flex items-center gap-1 rounded-md px-2 py-1 text-xs text-[rgb(var(--ec-page-text-muted))] hover:bg-[rgb(var(--ec-page-border)/0.5)]';
|
|
214
|
+
|
|
215
|
+
// No join form inside a chat: use the name the agent passed, or name them after the chat they're in
|
|
216
|
+
const hostName = app?.getHostVersion()?.name;
|
|
217
|
+
const userName = (view: CanvasView) =>
|
|
218
|
+
view.userName ?? `${hostName ? hostName.replace(/^\w/, (letter) => letter.toUpperCase()) : 'Chat'} user`;
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<div className="flex flex-col" style={{ height: fullscreen ? '100vh' : INLINE_HEIGHT }}>
|
|
222
|
+
<div className="flex items-center justify-between border-b px-3 py-1.5 border-[rgb(var(--ec-page-border))]">
|
|
223
|
+
<span className="flex min-w-0 items-center gap-1">
|
|
224
|
+
{canvas && (
|
|
225
|
+
<button className={iconButton} title="All canvases" onClick={() => void openCanvas()}>
|
|
226
|
+
<ArrowLeft size={14} />
|
|
227
|
+
</button>
|
|
228
|
+
)}
|
|
229
|
+
<span className="truncate text-sm font-semibold">{canvas ? (canvas.title ?? 'Canvas') : 'Canvases'}</span>
|
|
230
|
+
</span>
|
|
231
|
+
<span className="flex items-center gap-1">
|
|
232
|
+
{canvas && (
|
|
233
|
+
<button className={iconButton} onClick={() => app?.openLink({ url: canvas.canvasUrl })}>
|
|
234
|
+
<ExternalLink size={14} />
|
|
235
|
+
Open in EventCatalog
|
|
236
|
+
</button>
|
|
237
|
+
)}
|
|
238
|
+
{canToggle && (
|
|
239
|
+
<button
|
|
240
|
+
className={iconButton}
|
|
241
|
+
title={fullscreen ? 'Exit full screen' : 'Full screen'}
|
|
242
|
+
onClick={() => app?.requestDisplayMode({ mode: fullscreen ? 'inline' : 'fullscreen' })}
|
|
243
|
+
>
|
|
244
|
+
{fullscreen ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
|
|
245
|
+
</button>
|
|
246
|
+
)}
|
|
247
|
+
</span>
|
|
248
|
+
</div>
|
|
249
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
250
|
+
{canvas ? (
|
|
251
|
+
<StudioDesigner
|
|
252
|
+
key={canvas.canvasId}
|
|
253
|
+
canvasId={canvas.canvasId}
|
|
254
|
+
socketUrl={canvas.socketUrl}
|
|
255
|
+
shareUrl={canvas.canvasUrl}
|
|
256
|
+
mcpUrl={canvas.mcpUrl}
|
|
257
|
+
syncViaTools={syncViaTools}
|
|
258
|
+
defaultName={userName(canvas)}
|
|
259
|
+
resources={catalog.resources}
|
|
260
|
+
relations={catalog.relations}
|
|
261
|
+
onSelectionChange={setSelection}
|
|
262
|
+
followChanges
|
|
263
|
+
/>
|
|
264
|
+
) : (
|
|
265
|
+
<CanvasPicker
|
|
266
|
+
home={payload as CanvasHome}
|
|
267
|
+
onOpen={(canvasId) => void openCanvas(canvasId)}
|
|
268
|
+
onCreate={(title) => app && void show(loadPayload(app, 'openCanvas', { title }))}
|
|
269
|
+
/>
|
|
270
|
+
)}
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
mountView(CanvasApp, [visualiserStyles, styles].join('\n'));
|