@eventcatalog/core 5.0.0-beta.3 → 5.0.0-beta.31

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.
Files changed (277) hide show
  1. package/LICENSE +234 -134
  2. package/README.md +5 -5
  3. package/dist/analytics/analytics.cjs +18 -3
  4. package/dist/analytics/analytics.d.cts +1 -1
  5. package/dist/analytics/analytics.d.ts +1 -1
  6. package/dist/analytics/analytics.js +3 -2
  7. package/dist/analytics/log-build.cjs +160 -77
  8. package/dist/analytics/log-build.d.cts +2 -2
  9. package/dist/analytics/log-build.d.ts +2 -2
  10. package/dist/analytics/log-build.js +7 -5
  11. package/dist/analytics/studio-usage.cjs +90 -0
  12. package/dist/analytics/studio-usage.d.cts +5 -0
  13. package/dist/analytics/studio-usage.d.ts +5 -0
  14. package/dist/analytics/studio-usage.js +10 -0
  15. package/dist/analytics/telemetry.cjs +42 -0
  16. package/dist/analytics/telemetry.d.cts +20 -0
  17. package/dist/analytics/telemetry.d.ts +20 -0
  18. package/dist/analytics/telemetry.js +8 -0
  19. package/dist/catalog-runtime.cjs +1 -1
  20. package/dist/{chunk-BH2F5WPN.js → chunk-2UCCTEIH.js} +1 -1
  21. package/dist/{chunk-RFIXKATC.js → chunk-4G7FSRPN.js} +35 -42
  22. package/dist/{chunk-TQLRG35T.js → chunk-AD7CL772.js} +78 -45
  23. package/dist/{chunk-NDJOAITI.js → chunk-BNSLGNQI.js} +9 -3
  24. package/dist/chunk-CA6WCQ2T.js +54 -0
  25. package/dist/chunk-CPW6G62V.js +17 -0
  26. package/dist/{chunk-MDWB5HLO.js → chunk-NKNOXQ62.js} +1 -1
  27. package/dist/{chunk-423S2KHN.js → chunk-OUEGYMBA.js} +4 -2
  28. package/dist/{chunk-3PKS2IIP.js → chunk-OX6TSVHY.js} +1 -1
  29. package/dist/{chunk-36RYTYT5.js → chunk-PD7KTSYN.js} +1 -1
  30. package/dist/chunk-S5GWAUG5.js +29 -0
  31. package/dist/{chunk-GYIVJXOJ.js → chunk-WADZVNCA.js} +1 -1
  32. package/dist/{chunk-6OC46WQJ.js → chunk-XRFL642Z.js} +66 -35
  33. package/dist/constants.cjs +1 -1
  34. package/dist/constants.js +1 -1
  35. package/dist/eventcatalog-config-file-utils.cjs +34 -41
  36. package/dist/eventcatalog-config-file-utils.d.cts +56 -58
  37. package/dist/eventcatalog-config-file-utils.d.ts +56 -58
  38. package/dist/eventcatalog-config-file-utils.js +1 -1
  39. package/dist/eventcatalog.cjs +531 -356
  40. package/dist/eventcatalog.config.d.cts +59 -1
  41. package/dist/eventcatalog.config.d.ts +59 -1
  42. package/dist/eventcatalog.js +63 -25
  43. package/dist/features.cjs +34 -41
  44. package/dist/features.js +2 -2
  45. package/dist/federation/federate.cjs +37 -44
  46. package/dist/federation/federate.js +2 -2
  47. package/dist/generate.cjs +37 -42
  48. package/dist/generate.js +4 -4
  49. package/dist/resolve-catalog-dependencies.cjs +34 -41
  50. package/dist/resolve-catalog-dependencies.js +2 -2
  51. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio-fonts.css.mjs +3 -0
  52. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.css.mjs +3 -0
  53. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.mjs +3 -0
  54. package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
  55. package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
  56. package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
  57. package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
  58. package/dist/runtime-dependencies/htmlparser2.mjs +3 -0
  59. package/dist/runtime-dependencies/manifest.json +13 -1
  60. package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
  61. package/dist/runtime-dependencies/react-syntax-highlighter__dist__cjs__create-element.js.mjs +3 -0
  62. package/dist/runtime-dependencies/y-protocols.mjs +3 -0
  63. package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
  64. package/dist/runtime-dependencies/yjs.mjs +3 -0
  65. package/dist/utils/child-process-signals.cjs +53 -0
  66. package/dist/utils/child-process-signals.d.cts +19 -0
  67. package/dist/utils/child-process-signals.d.ts +19 -0
  68. package/dist/utils/child-process-signals.js +6 -0
  69. package/dist/utils/cli-logger.cjs +1 -1
  70. package/dist/utils/cli-logger.js +2 -2
  71. package/dist/utils/license-status.cjs +78 -45
  72. package/dist/utils/license-status.d.cts +15 -5
  73. package/dist/utils/license-status.d.ts +15 -5
  74. package/dist/utils/license-status.js +1 -1
  75. package/eventcatalog/astro.config.mjs +23 -1
  76. package/eventcatalog/integrations/config-loader.mjs +56 -58
  77. package/eventcatalog/integrations/skip-startup-reload.mjs +31 -0
  78. package/eventcatalog/integrations/stable-dependency-order.mjs +29 -0
  79. package/eventcatalog/integrations/storage.mjs +20 -0
  80. package/eventcatalog/integrations/studio-server.mjs +32 -0
  81. package/eventcatalog/src/components/CatalogGraph/CatalogForceGraph.tsx +39 -0
  82. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +2 -0
  83. package/eventcatalog/src/components/Header.astro +3 -3
  84. package/eventcatalog/src/components/Homepage/CatalogDashboard.astro +342 -0
  85. package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +60 -4
  86. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -1
  87. package/eventcatalog/src/components/NavigationProgress.astro +7 -0
  88. package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +2 -0
  89. package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +2 -0
  90. package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +2 -0
  91. package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +2 -0
  92. package/eventcatalog/src/components/Search/SearchDataLoader.astro +16 -28
  93. package/eventcatalog/src/components/Search/SearchModal.tsx +2 -10
  94. package/eventcatalog/src/components/Search/search-utils.ts +26 -0
  95. package/eventcatalog/src/components/Seo.astro +5 -1
  96. package/eventcatalog/src/components/Settings/AssistantSettingsForm.tsx +53 -48
  97. package/eventcatalog/src/components/Settings/GeneralSettingsForm.tsx +183 -178
  98. package/eventcatalog/src/components/Settings/LicenseSettings.tsx +140 -31
  99. package/eventcatalog/src/components/Settings/LlmAccessSettingsForm.tsx +40 -35
  100. package/eventcatalog/src/components/Settings/LogoUpload.tsx +1 -1
  101. package/eventcatalog/src/components/Settings/McpSettingsForm.tsx +9 -8
  102. package/eventcatalog/src/components/Settings/Row.tsx +7 -4
  103. package/eventcatalog/src/components/Settings/SettingsShared.tsx +3 -3
  104. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +53 -83
  105. package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +2 -0
  106. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +131 -0
  107. package/eventcatalog/src/components/SideNav/SideNav.astro +9 -1
  108. package/eventcatalog/src/components/TrialBadge.astro +20 -9
  109. package/eventcatalog/src/content.config.ts +19 -2
  110. package/eventcatalog/src/features/auth/error.astro +4 -2
  111. package/eventcatalog/src/features/auth/login.astro +2 -1
  112. package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
  113. package/eventcatalog/src/features/auth/unauthorized.astro +1 -1
  114. package/eventcatalog/src/features/fields/pages/fields.astro +1 -1
  115. package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
  116. package/eventcatalog/src/features/mcp/apps/architecture-diagram/node-chat.tsx +6 -45
  117. package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
  118. package/eventcatalog/src/features/mcp/apps/canvas/styles.css +24 -0
  119. package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +278 -0
  120. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +272 -0
  121. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +65 -65
  122. package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
  123. package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
  124. package/eventcatalog/src/features/mcp/mcp-server.ts +36 -5
  125. package/eventcatalog/src/features/server/start.ts +25 -0
  126. package/eventcatalog/src/features/storage/database.ts +88 -0
  127. package/eventcatalog/src/features/storage/migrations.ts +85 -0
  128. package/eventcatalog/src/features/storage/sql/database.sql +5 -0
  129. package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
  130. package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
  131. package/eventcatalog/src/features/storage/sql.ts +28 -0
  132. package/eventcatalog/src/features/studio/agent-choreography.ts +384 -0
  133. package/eventcatalog/src/features/studio/agents.ts +28 -0
  134. package/eventcatalog/src/features/studio/api/canvases-api.ts +219 -0
  135. package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
  136. package/eventcatalog/src/features/studio/api/client.ts +40 -0
  137. package/eventcatalog/src/features/studio/canvas-actions.ts +540 -0
  138. package/eventcatalog/src/features/studio/canvas-preview.ts +52 -0
  139. package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
  140. package/eventcatalog/src/features/studio/catalog.ts +173 -0
  141. package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +518 -0
  142. package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +118 -0
  143. package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +434 -0
  144. package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +143 -0
  145. package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +65 -0
  146. package/eventcatalog/src/features/studio/components/Dialog.tsx +60 -0
  147. package/eventcatalog/src/features/studio/components/JoinForm.tsx +109 -0
  148. package/eventcatalog/src/features/studio/components/LeftPanel.tsx +206 -0
  149. package/eventcatalog/src/features/studio/components/NodeDetails.tsx +183 -0
  150. package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +79 -0
  151. package/eventcatalog/src/features/studio/components/Presence.tsx +293 -0
  152. package/eventcatalog/src/features/studio/components/ShareDialog.tsx +396 -0
  153. package/eventcatalog/src/features/studio/components/ShortcutsDialog.tsx +41 -0
  154. package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +2091 -0
  155. package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
  156. package/eventcatalog/src/features/studio/document-name.ts +7 -0
  157. package/eventcatalog/src/features/studio/from-visualiser.ts +138 -0
  158. package/eventcatalog/src/features/studio/hooks/presence-sender.ts +63 -0
  159. package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
  160. package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +353 -0
  161. package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
  162. package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +109 -0
  163. package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +782 -0
  164. package/eventcatalog/src/features/studio/identity.ts +59 -0
  165. package/eventcatalog/src/features/studio/limits.ts +2 -0
  166. package/eventcatalog/src/features/studio/pages/[id].astro +69 -0
  167. package/eventcatalog/src/features/studio/pages/index.astro +784 -0
  168. package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
  169. package/eventcatalog/src/features/studio/server/canvas-mcp.ts +495 -0
  170. package/eventcatalog/src/features/studio/server/runtime.ts +498 -0
  171. package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
  172. package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
  173. package/eventcatalog/src/features/studio/server/storage.ts +90 -0
  174. package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
  175. package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
  176. package/eventcatalog/src/layouts/BaseLayout.astro +4 -4
  177. package/eventcatalog/src/layouts/SettingsLayout.astro +2 -2
  178. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +13 -5
  179. package/eventcatalog/src/pages/_index.astro +5 -342
  180. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +2 -3
  181. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +0 -7
  182. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +3 -1
  183. package/eventcatalog/src/pages/diagrams/[id]/[version]/_index.data.ts +0 -7
  184. package/eventcatalog/src/pages/diagrams/[id]/[version]/embed.astro +1 -0
  185. package/eventcatalog/src/pages/diagrams/[id]/[version]/index.astro +1 -0
  186. package/eventcatalog/src/pages/directory/[type]/_index.data.ts +5 -7
  187. package/eventcatalog/src/pages/directory/[type]/index.astro +3 -1
  188. package/eventcatalog/src/pages/discover/[type]/_index.data.ts +5 -7
  189. package/eventcatalog/src/pages/discover/[type]/index.astro +3 -1
  190. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/_index.data.ts +0 -7
  191. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +1 -0
  192. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/_index.data.ts +0 -7
  193. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +1 -0
  194. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +0 -7
  195. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +3 -1
  196. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/_[filename].data.ts +0 -7
  197. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts +0 -7
  198. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +1 -0
  199. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +1 -0
  200. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/_[...filename].data.ts +0 -4
  201. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/_index.data.ts +0 -7
  202. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/index.astro +1 -0
  203. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +3 -1
  204. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/_[filename].data.ts +0 -7
  205. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +1 -0
  206. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +2 -3
  207. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +0 -7
  208. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +1 -0
  209. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +1 -0
  210. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/_[filename].data.ts +0 -7
  211. package/eventcatalog/src/pages/docs/[type]/[id]/_index.data.ts +0 -7
  212. package/eventcatalog/src/pages/docs/[type]/[id]/index.astro +2 -1
  213. package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/_index.data.ts +0 -7
  214. package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +1 -0
  215. package/eventcatalog/src/pages/docs/[type]/[id]/language/_index.data.ts +0 -7
  216. package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +1 -0
  217. package/eventcatalog/src/pages/docs/custom/[...path]/_index.data.ts +0 -7
  218. package/eventcatalog/src/pages/docs/custom/[...path]/index.astro +1 -0
  219. package/eventcatalog/src/pages/docs/services/[id]/[docType]/[docId]/index.astro +1 -1
  220. package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +1 -0
  221. package/eventcatalog/src/pages/docs/services/[id]/asyncapi/[filename].astro +1 -1
  222. package/eventcatalog/src/pages/docs/services/[id]/changelog/index.astro +1 -1
  223. package/eventcatalog/src/pages/docs/services/[id]/graphql/[filename].astro +1 -1
  224. package/eventcatalog/src/pages/docs/services/[id]/spec/[filename].astro +1 -1
  225. package/eventcatalog/src/pages/docs/teams/[id]/_index.data.ts +0 -7
  226. package/eventcatalog/src/pages/docs/teams/[id]/index.astro +1 -0
  227. package/eventcatalog/src/pages/docs/users/[id]/_index.data.ts +0 -7
  228. package/eventcatalog/src/pages/docs/users/[id]/index.astro +1 -0
  229. package/eventcatalog/src/pages/index.astro +4 -2
  230. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/_index.data.ts +0 -7
  231. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +1 -0
  232. package/eventcatalog/src/pages/schemas/explorer/_index.data.ts +0 -7
  233. package/eventcatalog/src/pages/schemas/explorer/index.astro +4 -2
  234. package/eventcatalog/src/pages/settings/license.astro +9 -1
  235. package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/_index.data.ts +0 -7
  236. package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/index.astro +1 -0
  237. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts +0 -7
  238. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/_index.data.ts +0 -7
  239. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/index.astro +1 -0
  240. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/_index.data.ts +0 -7
  241. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/index.astro +1 -0
  242. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +1 -0
  243. package/eventcatalog/src/pages/visualiser/[type]/[id]/_index.data.ts +38 -0
  244. package/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +18 -41
  245. package/eventcatalog/src/pages/visualiser/designs/[id]/_index.data.ts +0 -7
  246. package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +2 -1
  247. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/_index.data.ts +0 -7
  248. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +1 -0
  249. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/index.astro +1 -0
  250. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/_index.data.ts +0 -7
  251. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/index.astro +1 -0
  252. package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/_index.data.ts +0 -7
  253. package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/index.astro +1 -0
  254. package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/index.astro +1 -0
  255. package/eventcatalog/src/types/react-syntax-highlighter.d.ts +10 -0
  256. package/eventcatalog/src/utils/collections/util.ts +8 -4
  257. package/eventcatalog/src/utils/feature.ts +8 -0
  258. package/eventcatalog/src/utils/git.ts +22 -5
  259. package/eventcatalog/src/utils/homepage-dashboard.ts +143 -0
  260. package/eventcatalog/src/utils/license.ts +1 -1
  261. package/eventcatalog/src/utils/link-validation.ts +25 -19
  262. package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +3 -1
  263. package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +3 -3
  264. package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
  265. package/eventcatalog/src/utils/node-graphs/field-node-graph.ts +4 -2
  266. package/eventcatalog/src/utils/node-graphs/layout-node-graph.ts +20 -1
  267. package/eventcatalog/src/utils/node-graphs/node-resource.ts +67 -0
  268. package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +1 -1
  269. package/eventcatalog/src/utils/page-loaders/hybrid-page.ts +4 -15
  270. package/eventcatalog/src/utils/page-title.ts +15 -0
  271. package/eventcatalog/src/utils/syntax-highlighter-renderer.ts +64 -0
  272. package/eventcatalog/src/utils/trial.ts +14 -1
  273. package/package.json +19 -13
  274. package/dist/runtime-dependencies/shelljs.mjs +0 -3
  275. package/eventcatalog/public/studio-bg.png +0 -0
  276. package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
  277. package/eventcatalog/src/pages/studio.astro +0 -316
@@ -0,0 +1,434 @@
1
+ import { useEffect, useState, type ReactNode } from 'react';
2
+ import { Check, ChevronDown, Code2, Copy, Globe, Sparkles, X } from 'lucide-react';
3
+ import type { WebMcpStatus } from '../hooks/use-canvas-webmcp';
4
+ import { STATUS, ClaudeCodeLogo, ClaudeLogo, CursorLogo, OpenAILogo } from '@eventcatalog/visualiser/studio';
5
+
6
+ type CanvasLink = { canvasId: string; title?: string; canvasUrl: string; mcpUrl: string };
7
+
8
+ export const RELAY_COMMAND = 'npx -y @mcp-b/webmcp-local-relay@5.1.0';
9
+ const RELAY_KEY = 'eventcatalog-webmcp-relay';
10
+ const WEBMCP_KEY = 'eventcatalog-webmcp';
11
+
12
+ const isOn = (key: string) => {
13
+ try {
14
+ return localStorage.getItem(key) === 'true';
15
+ } catch {
16
+ return false;
17
+ }
18
+ };
19
+ const turnOn = (key: string) => {
20
+ try {
21
+ localStorage.setItem(key, 'true');
22
+ } catch {}
23
+ };
24
+
25
+ /**
26
+ * WebMCP through the polyfill (for browsers without WebMCP of their own) is opt in, remembered in this browser:
27
+ * the polyfill watches every change to the page, which on a canvas is every drag and pan. The relay needs it.
28
+ */
29
+ export const isWebMcpEnabled = () => isOn(WEBMCP_KEY) || isOn(RELAY_KEY);
30
+ export const enableWebMcp = () => turnOn(WEBMCP_KEY);
31
+
32
+ /**
33
+ * The WebMCP local relay hands this tab's tools to desktop agents. Opt in: its script keeps looking for a
34
+ * relay on localhost, so it only loads once someone turns it on (remembered in this browser).
35
+ */
36
+ export const isRelayEnabled = () => isOn(RELAY_KEY);
37
+
38
+ export const loadRelay = (scriptPath: string) => {
39
+ if (document.querySelector('script[data-webmcp-relay-embed]')) return;
40
+ const script = document.createElement('script');
41
+ script.src = scriptPath;
42
+ script.dataset.webmcpRelayEmbed = '';
43
+ script.dataset.relayPort = '9333';
44
+ document.body.appendChild(script);
45
+ };
46
+
47
+ const enableRelay = (scriptPath: string) => {
48
+ turnOn(RELAY_KEY);
49
+ loadRelay(scriptPath);
50
+ };
51
+
52
+ /**
53
+ * The prompt people give their agent to join this canvas. It works out how to connect from what the agent
54
+ * can do: the EventCatalog MCP server (and openCanvas in chats that show MCP Apps), WebMCP in the open tab,
55
+ * or the WebMCP local relay for desktop agents.
56
+ */
57
+ export const buildAgentPrompt = ({ canvasId, title, canvasUrl, mcpUrl }: CanvasLink) =>
58
+ `Join me on an EventCatalog canvas so we can design this together, live.
59
+
60
+ Canvas: ${title ? `"${title}"` : 'Untitled'}
61
+ Canvas id: ${canvasId}
62
+ Link: ${canvasUrl}
63
+
64
+ Connect with the first of these you can use:
65
+
66
+ 1. EventCatalog MCP server (Streamable HTTP): ${mcpUrl}
67
+ - If you already have it (tools like getCanvas, addToCanvas), use them with canvasId "${canvasId}".
68
+ - If not and you can add MCP servers, add it. For example, in Claude Code: claude mcp add --transport http eventcatalog ${mcpUrl}
69
+ - If you can show MCP Apps in this chat (e.g. ChatGPT, Claude), call openCanvas with canvasId "${canvasId}" so I can work on the canvas with you right here.
70
+ 2. WebMCP: if you can use tools from my browser tab, I have the canvas open there and it offers tools like get_canvas, add_to_canvas and add_comment.
71
+ 3. WebMCP local relay: if you run on my computer (e.g. Claude Code, Cursor) and can't reach the MCP server, add the MCP server \`${RELAY_COMMAND}\`. It gives you the tools of my open canvas tab.
72
+
73
+ Once connected:
74
+ - Read the canvas first (getCanvas / get_canvas) and tell me here who else is on it.
75
+ - Ask me here, in our conversation, what we are designing, then work on the canvas as we talk: add catalog resources (find them with getResources / search_catalog), add new components for things that don't exist yet, and connect them. Leave out positions so new nodes go next to what they connect to. When you finish a round of changes, tidy the whole canvas once (layoutCanvas / layout_canvas) before you hand back to me, unless I asked you to keep the layout.
76
+ - Talk to me here, not in canvas comments. Only comment on the canvas when I ask you to (e.g. to review it), and reply to comments people leave for you.
77
+ - Pass agentName with your name on MCP tools, so people on the canvas can see who you are.`;
78
+
79
+ const button =
80
+ 'inline-flex h-9 flex-none items-center justify-center gap-2 rounded-lg px-4 text-sm font-medium transition-colors [&_svg]:h-4 [&_svg]:w-4';
81
+ const primary = `${button} bg-[rgb(var(--ec-page-text))] text-[rgb(var(--ec-page-bg))] hover:bg-[rgb(var(--ec-page-text)/0.85)]`;
82
+ const secondary = `${button} border border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-content-hover))]`;
83
+
84
+ const copyText = async (text: string) => {
85
+ try {
86
+ await navigator.clipboard.writeText(text);
87
+ return true;
88
+ } catch {
89
+ return false;
90
+ }
91
+ };
92
+
93
+ /** Something to copy, with a button that copies it (as Studio on the web's) */
94
+ function Snippet({ text, label }: { text: string; label: string }) {
95
+ const [copied, setCopied] = useState(false);
96
+ useEffect(() => {
97
+ if (!copied) return;
98
+ const timer = setTimeout(() => setCopied(false), 1500);
99
+ return () => clearTimeout(timer);
100
+ }, [copied]);
101
+ return (
102
+ <div className="relative mt-2 rounded-lg border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-content-hover))]">
103
+ <pre className="overflow-x-auto whitespace-pre p-3 pr-12 font-mono text-xs leading-relaxed text-[rgb(var(--ec-page-text))]">
104
+ {text}
105
+ </pre>
106
+ <button
107
+ type="button"
108
+ aria-label={`Copy ${label}`}
109
+ title="Copy"
110
+ onClick={async () => setCopied(await copyText(text))}
111
+ className="absolute right-2 top-2 rounded-md p-1.5 text-[rgb(var(--ec-page-text-muted))] hover:bg-[rgb(var(--ec-card-bg))] hover:text-[rgb(var(--ec-page-text))] [&_svg]:h-4 [&_svg]:w-4"
112
+ >
113
+ {copied ? <Check aria-hidden /> : <Copy aria-hidden />}
114
+ </button>
115
+ </div>
116
+ );
117
+ }
118
+
119
+ const Steps = ({ children }: { children: ReactNode }) => (
120
+ <ol className="list-decimal space-y-2 pl-5 text-sm leading-relaxed text-[rgb(var(--ec-page-text))] marker:text-[rgb(var(--ec-page-text-muted))]">
121
+ {children}
122
+ </ol>
123
+ );
124
+
125
+ /** Turns something on for this tab, or says it's on */
126
+ function TabSwitch({ on, onTurnOn, label }: { on: boolean; onTurnOn: () => void; label: string }) {
127
+ return on ? (
128
+ <p className={`flex shrink-0 items-center gap-1.5 whitespace-nowrap text-xs font-medium ${STATUS.success.text}`}>
129
+ <span className={`h-2 w-2 rounded-full ${STATUS.success.dot}`} />
130
+ On for this tab
131
+ </p>
132
+ ) : (
133
+ <button type="button" onClick={onTurnOn} className={`${secondary} h-8 px-3 text-xs`}>
134
+ {label}
135
+ </button>
136
+ );
137
+ }
138
+
139
+ /** Things to ask, for the two ways of pairing with an agent on a canvas: designing something new, and understanding what's there */
140
+ const PAIRING = [
141
+ {
142
+ title: 'Design ideas together',
143
+ prompts: [
144
+ 'Add a payments service that listens to OrderPlaced',
145
+ 'How could we split the orders service?',
146
+ 'Add retries and a dead letter queue to this',
147
+ ],
148
+ },
149
+ {
150
+ title: 'Understand how it works',
151
+ prompts: [
152
+ 'Put everything the Orders domain owns on the canvas',
153
+ 'Walk me through what happens when a payment fails',
154
+ 'Which services consume this event?',
155
+ ],
156
+ },
157
+ ];
158
+
159
+ /**
160
+ * Bringing an AI agent onto the canvas, as Studio on the web does it: how to add EventCatalog's MCP server to each
161
+ * common client, a step or two each with what to copy, and what to ask it. The quickest way first: a prompt to give
162
+ * the agent, which works out how to connect (and which canvas to join) itself. Agents in this browser use WebMCP.
163
+ */
164
+ export default function ConnectAgentDialog({
165
+ link,
166
+ webMcp,
167
+ relayScriptPath,
168
+ onEnableWebMcp,
169
+ onClose,
170
+ }: {
171
+ link: CanvasLink;
172
+ webMcp: WebMcpStatus;
173
+ /** Turn WebMCP on for this tab (through the polyfill, when the browser has none) */
174
+ onEnableWebMcp: () => void;
175
+ /** Where the relay's script is served (only on EventCatalog's own canvas page) */
176
+ relayScriptPath?: string;
177
+ onClose: () => void;
178
+ }) {
179
+ const prompt = buildAgentPrompt(link);
180
+ const { mcpUrl, canvasId } = link;
181
+ const [relayOn, setRelayOn] = useState(isRelayEnabled);
182
+ const [showPrompt, setShowPrompt] = useState(false);
183
+ const [promptCopied, setPromptCopied] = useState(false);
184
+ const [client, setClient] = useState(0);
185
+
186
+ useEffect(() => {
187
+ const onKeyDown = (event: KeyboardEvent) => event.key === 'Escape' && onClose();
188
+ window.addEventListener('keydown', onKeyDown);
189
+ return () => window.removeEventListener('keydown', onKeyDown);
190
+ }, [onClose]);
191
+ useEffect(() => {
192
+ if (!promptCopied) return;
193
+ const timer = setTimeout(() => setPromptCopied(false), 1500);
194
+ return () => clearTimeout(timer);
195
+ }, [promptCopied]);
196
+
197
+ const turnOnRelay = () => {
198
+ if (!relayScriptPath) return;
199
+ enableRelay(relayScriptPath);
200
+ setRelayOn(true);
201
+ // The relay hands this tab's WebMCP tools to desktop agents
202
+ onEnableWebMcp();
203
+ };
204
+
205
+ const joinThisCanvas = (
206
+ <>
207
+ Ask it to join this canvas (<code className="text-xs">{canvasId}</code>), or paste the prompt above, and start pairing (try
208
+ one of the prompts below).
209
+ </>
210
+ );
211
+ const clients: { name: string; logo: ReactNode; body: ReactNode }[] = [
212
+ {
213
+ name: 'Claude',
214
+ logo: <ClaudeLogo />,
215
+ body: (
216
+ <Steps>
217
+ <li>
218
+ In Claude, open <strong>Settings → Connectors</strong> and choose <strong>Add custom connector</strong>.
219
+ </li>
220
+ <li>
221
+ Name it <strong>EventCatalog</strong> and paste this URL:
222
+ <Snippet text={mcpUrl} label="the URL" />
223
+ </li>
224
+ <li>In a chat, turn the connector on. {joinThisCanvas}</li>
225
+ </Steps>
226
+ ),
227
+ },
228
+ {
229
+ name: 'Claude Code',
230
+ logo: <ClaudeCodeLogo />,
231
+ body: (
232
+ <>
233
+ <Steps>
234
+ <li>
235
+ Run this in your project:
236
+ <Snippet text={`claude mcp add --transport http eventcatalog ${mcpUrl}`} label="the command" />
237
+ </li>
238
+ <li>{joinThisCanvas}</li>
239
+ </Steps>
240
+ {relayScriptPath && (
241
+ <div className="mt-4 flex items-center justify-between gap-3 rounded-lg border border-[rgb(var(--ec-page-border))] p-3">
242
+ <p className="text-xs text-[rgb(var(--ec-page-text-muted))]">
243
+ Can't reach this EventCatalog from your computer? Use this tab through the local relay: add{' '}
244
+ <code>{RELAY_COMMAND}</code> as an MCP server.
245
+ </p>
246
+ <TabSwitch on={relayOn} onTurnOn={turnOnRelay} label="Turn on relay" />
247
+ </div>
248
+ )}
249
+ </>
250
+ ),
251
+ },
252
+ {
253
+ name: 'ChatGPT',
254
+ logo: <OpenAILogo />,
255
+ body: (
256
+ <Steps>
257
+ <li>
258
+ In ChatGPT, open <strong>Settings → Apps &amp; Connectors</strong>, turn on developer mode and choose{' '}
259
+ <strong>Create</strong>.
260
+ </li>
261
+ <li>
262
+ Name it <strong>EventCatalog</strong> and paste this URL:
263
+ <Snippet text={mcpUrl} label="the URL" />
264
+ </li>
265
+ <li>In a chat, add the connector. Chats that show MCP Apps open the canvas right there. {joinThisCanvas}</li>
266
+ </Steps>
267
+ ),
268
+ },
269
+ {
270
+ name: 'Cursor',
271
+ logo: <CursorLogo />,
272
+ body: (
273
+ <Steps>
274
+ <li>
275
+ Add this server to <code className="text-xs">.cursor/mcp.json</code> in your project:
276
+ <Snippet text={JSON.stringify({ mcpServers: { eventcatalog: { url: mcpUrl } } }, null, 2)} label="the settings" />
277
+ </li>
278
+ <li>{joinThisCanvas}</li>
279
+ </Steps>
280
+ ),
281
+ },
282
+ {
283
+ name: 'VS Code',
284
+ logo: <Code2 />,
285
+ body: (
286
+ <Steps>
287
+ <li>
288
+ Add this server to <code className="text-xs">.vscode/mcp.json</code> in your project:
289
+ <Snippet
290
+ text={JSON.stringify({ servers: { eventcatalog: { type: 'http', url: mcpUrl } } }, null, 2)}
291
+ label="the settings"
292
+ />
293
+ </li>
294
+ <li>In Copilot's agent mode: {joinThisCanvas}</li>
295
+ </Steps>
296
+ ),
297
+ },
298
+ {
299
+ name: 'This browser',
300
+ logo: <Globe />,
301
+ body: (
302
+ <div className="flex items-center justify-between gap-3">
303
+ <p className="text-sm leading-relaxed text-[rgb(var(--ec-page-text))]">
304
+ {webMcp.state === 'on'
305
+ ? `This tab offers ${webMcp.tools} canvas tools to agents in your browser (like the MCP-B extension) through WebMCP.`
306
+ : 'Let agents in your browser (like the MCP-B extension) use this canvas through WebMCP.'}
307
+ </p>
308
+ <TabSwitch on={webMcp.state === 'on'} onTurnOn={onEnableWebMcp} label="Turn on" />
309
+ </div>
310
+ ),
311
+ },
312
+ ];
313
+
314
+ return (
315
+ <div className="fixed inset-0 z-[80] flex items-center justify-center bg-black/40 p-4" onClick={onClose}>
316
+ <div
317
+ role="dialog"
318
+ aria-modal="true"
319
+ aria-labelledby="connect-agent-title"
320
+ onClick={(event) => event.stopPropagation()}
321
+ className="flex max-h-full w-full max-w-2xl flex-col rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] text-[rgb(var(--ec-page-text))] shadow-2xl"
322
+ >
323
+ <div className="flex items-start gap-3 px-5 pt-5">
324
+ <div className="min-w-0 flex-1">
325
+ <h2 id="connect-agent-title" className="text-base font-semibold">
326
+ Connect your AI agent
327
+ </h2>
328
+ <p className="mt-1.5 text-sm font-light leading-relaxed text-[rgb(var(--ec-page-text-muted))]">
329
+ Pair with your AI agent on this canvas: sketch ideas together, or have it bring in what your catalog has and walk
330
+ you through it. It joins with its own pointer, and you both change the canvas. Add this EventCatalog as an MCP
331
+ server.
332
+ </p>
333
+ </div>
334
+ <button
335
+ type="button"
336
+ aria-label="Close"
337
+ onClick={onClose}
338
+ className="-mr-1 -mt-1 rounded-md p-1.5 text-[rgb(var(--ec-page-text-muted))] hover:bg-[rgb(var(--ec-content-hover))] hover:text-[rgb(var(--ec-page-text))]"
339
+ >
340
+ <X aria-hidden className="h-4 w-4" />
341
+ </button>
342
+ </div>
343
+
344
+ <div className="min-h-0 overflow-y-auto">
345
+ {/* The quickest way: one prompt, which the agent works out how to connect from (and which canvas to join) */}
346
+ <section className="mx-5 mt-4 overflow-hidden rounded-xl border border-[color-mix(in_srgb,rgb(var(--ec-accent))_35%,rgb(var(--ec-page-border)))] bg-[color-mix(in_srgb,rgb(var(--ec-accent))_6%,transparent)]">
347
+ <div className="flex items-center gap-3 p-4">
348
+ <span className="flex h-9 w-9 flex-none items-center justify-center rounded-lg bg-[color-mix(in_srgb,rgb(var(--ec-accent))_14%,transparent)] text-[rgb(var(--ec-accent))]">
349
+ <Sparkles aria-hidden className="h-[18px] w-[18px]" />
350
+ </span>
351
+ <div className="min-w-0 flex-1">
352
+ <h3 className="text-sm font-semibold">Quickest: paste one prompt</h3>
353
+ <p className="mt-0.5 text-xs leading-relaxed text-[rgb(var(--ec-page-text-muted))]">
354
+ Give it to your agent: it connects to this EventCatalog and joins this canvas on its own.
355
+ </p>
356
+ </div>
357
+ <button type="button" onClick={async () => setPromptCopied(await copyText(prompt))} className={primary}>
358
+ {promptCopied ? <Check aria-hidden /> : <Copy aria-hidden />}
359
+ {promptCopied ? 'Copied' : 'Copy prompt'}
360
+ </button>
361
+ </div>
362
+ {/* What's in it, at a glance (the whole of it on a click) */}
363
+ <div className="relative border-t border-[color-mix(in_srgb,rgb(var(--ec-accent))_20%,rgb(var(--ec-page-border)))] bg-[rgb(var(--ec-card-bg)/0.6)]">
364
+ <pre
365
+ className={`whitespace-pre-wrap px-4 py-3 font-mono text-[11px] leading-relaxed text-[rgb(var(--ec-page-text-muted))] ${
366
+ showPrompt ? 'max-h-56 overflow-y-auto' : 'max-h-[112px] overflow-hidden'
367
+ }`}
368
+ >
369
+ {prompt}
370
+ </pre>
371
+ {!showPrompt && (
372
+ <div className="pointer-events-none absolute inset-x-0 bottom-0 h-20 bg-gradient-to-t from-[rgb(var(--ec-card-bg))] via-[rgb(var(--ec-card-bg)/0.85)] to-transparent" />
373
+ )}
374
+ <button
375
+ type="button"
376
+ onClick={() => setShowPrompt((shown) => !shown)}
377
+ aria-expanded={showPrompt}
378
+ className={`${
379
+ showPrompt ? 'mx-4 mb-3' : 'absolute bottom-2 left-1/2 -translate-x-1/2'
380
+ } inline-flex items-center gap-1 rounded-full border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] px-2.5 py-1 text-[11px] font-medium text-[rgb(var(--ec-page-text-muted))] shadow-sm hover:text-[rgb(var(--ec-page-text))]`}
381
+ >
382
+ {showPrompt ? 'Show less' : 'Show the whole prompt'}
383
+ <ChevronDown aria-hidden size={12} className={`transition-transform ${showPrompt ? 'rotate-180' : ''}`} />
384
+ </button>
385
+ </div>
386
+ </section>
387
+
388
+ <p className="mx-5 mt-5 text-[11px] font-semibold uppercase tracking-wide text-[rgb(var(--ec-page-text-muted))]">
389
+ Or set it up yourself
390
+ </p>
391
+ {/* Or add the server to the client they use, a step or two each */}
392
+ <div className="px-5 pb-5 pt-2">
393
+ <div role="tablist" className="flex gap-1 rounded-lg bg-[rgb(var(--ec-content-hover))] p-1">
394
+ {clients.map(({ name, logo }, index) => (
395
+ <button
396
+ key={name}
397
+ type="button"
398
+ role="tab"
399
+ aria-selected={client === index}
400
+ onClick={() => setClient(index)}
401
+ className={`flex flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-md px-2 py-1.5 text-xs font-medium outline-none [&_svg]:h-3.5 [&_svg]:w-3.5 [&_svg]:flex-none ${
402
+ client === index
403
+ ? 'bg-[rgb(var(--ec-card-bg))] text-[rgb(var(--ec-page-text))] shadow-sm'
404
+ : 'text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))]'
405
+ }`}
406
+ >
407
+ {logo}
408
+ {name}
409
+ </button>
410
+ ))}
411
+ </div>
412
+ <div role="tabpanel" className="mt-4">
413
+ {clients[client].body}
414
+ </div>
415
+ </div>
416
+
417
+ {/* What pairing looks like: the two ways people work with their agent on a canvas */}
418
+ <div className="grid gap-3 border-t border-[rgb(var(--ec-page-border))] px-5 py-4 sm:grid-cols-2">
419
+ {PAIRING.map(({ title, prompts }) => (
420
+ <div key={title}>
421
+ <h3 className="text-xs font-semibold text-[rgb(var(--ec-page-text))]">{title}</h3>
422
+ <ul className="mt-1.5 space-y-1 text-xs leading-relaxed text-[rgb(var(--ec-page-text-muted))]">
423
+ {prompts.map((text) => (
424
+ <li key={text}>“{text}”</li>
425
+ ))}
426
+ </ul>
427
+ </div>
428
+ ))}
429
+ </div>
430
+ </div>
431
+ </div>
432
+ </div>
433
+ );
434
+ }
@@ -0,0 +1,143 @@
1
+ import { useEffect, useState, type ReactElement } from 'react';
2
+ import { X } from 'lucide-react';
3
+ import type { ConnectionGroup, ConnectionGroupId } from '../canvas-actions';
4
+ import type { CatalogResource } from '../catalog-resources';
5
+ import { getNodeLabel, CardPreview } from '@eventcatalog/visualiser/studio';
6
+
7
+ /**
8
+ * Asked when a catalog service or message is dropped on the canvas (and connects to things that aren't on it yet):
9
+ * just it, or it with its connections (a service's messages and data stores, the services that send and receive a
10
+ * message), picked by group. Styled like the domain and system chooser.
11
+ */
12
+
13
+ export type ConnectionsChoice = { as: 'card' } | { as: 'connected'; include: CatalogResource[] };
14
+
15
+ // It in the middle, what comes in before it and what goes out after it
16
+ const ConnectedPreview = () => (
17
+ <svg viewBox="0 0 64 36" className="h-9 w-16" aria-hidden>
18
+ <rect x="1" y="7" width="10" height="8" rx="1.5" fill="currentColor" fillOpacity="0.4" />
19
+ <rect x="1" y="21" width="10" height="8" rx="1.5" fill="currentColor" fillOpacity="0.4" />
20
+ <path d="M11 11h8M11 25h8" stroke="currentColor" strokeOpacity="0.5" strokeWidth="1.5" />
21
+ <rect
22
+ x="19"
23
+ y="8"
24
+ width="26"
25
+ height="20"
26
+ rx="4"
27
+ fill="currentColor"
28
+ fillOpacity="0.1"
29
+ stroke="currentColor"
30
+ strokeOpacity="0.5"
31
+ strokeWidth="1.5"
32
+ />
33
+ <rect x="23" y="13" width="13" height="3" rx="1.5" fill="currentColor" fillOpacity="0.6" />
34
+ <rect x="23" y="19" width="17" height="2" rx="1" fill="currentColor" fillOpacity="0.35" />
35
+ <path d="M45 18h8" stroke="currentColor" strokeOpacity="0.5" strokeWidth="1.5" />
36
+ <rect x="53" y="14" width="10" height="8" rx="1.5" fill="currentColor" fillOpacity="0.4" />
37
+ </svg>
38
+ );
39
+
40
+ /** What "with its connections" brings, by what's dropped */
41
+ const CONNECTED_HINTS: Record<string, string> = {
42
+ services: 'What it sends, receives and stores, around it',
43
+ };
44
+ const MESSAGE_HINT = 'The services that send and receive it, around it';
45
+
46
+ export default function ConnectionsChooser({
47
+ resource,
48
+ groups,
49
+ position,
50
+ onChoose,
51
+ onCancel,
52
+ }: {
53
+ resource: CatalogResource;
54
+ /** What it connects to that isn't on the canvas yet */
55
+ groups: ConnectionGroup[];
56
+ /** Where to show it (screen coordinates) */
57
+ position: { x: number; y: number };
58
+ onChoose: (choice: ConnectionsChoice) => void;
59
+ onCancel: () => void;
60
+ }) {
61
+ const noun = getNodeLabel(resource.node.type).toLowerCase();
62
+ const [included, setIncluded] = useState<Set<ConnectionGroupId>>(() => new Set(groups.map((group) => group.id)));
63
+ const toggle = (id: ConnectionGroupId) =>
64
+ setIncluded((current) => {
65
+ const next = new Set(current);
66
+ if (next.has(id)) next.delete(id);
67
+ else next.add(id);
68
+ return next;
69
+ });
70
+ const include = groups.flatMap((group) => (included.has(group.id) ? group.resources : []));
71
+ const options: { as: ConnectionsChoice['as']; label: string; hint: string; Preview: () => ReactElement }[] = [
72
+ { as: 'card', label: `Just the ${noun}`, hint: 'Connected to what is already on the canvas', Preview: CardPreview },
73
+ {
74
+ as: 'connected',
75
+ label: 'With its connections',
76
+ hint: CONNECTED_HINTS[resource.collection] ?? MESSAGE_HINT,
77
+ Preview: ConnectedPreview,
78
+ },
79
+ ];
80
+
81
+ useEffect(() => {
82
+ const onKeyDown = (event: KeyboardEvent) => event.key === 'Escape' && onCancel();
83
+ window.addEventListener('keydown', onKeyDown);
84
+ return () => window.removeEventListener('keydown', onKeyDown);
85
+ }, [onCancel]);
86
+
87
+ // Kept on screen, next to where it was dropped
88
+ const left = Math.min(Math.max(position.x - 160, 12), window.innerWidth - 332);
89
+ const top = Math.min(Math.max(position.y - 20, 12), window.innerHeight - 300);
90
+
91
+ return (
92
+ <div className="fixed inset-0 z-50" onPointerDown={onCancel}>
93
+ <div
94
+ role="dialog"
95
+ aria-label={`Add ${resource.name}`}
96
+ onPointerDown={(event) => event.stopPropagation()}
97
+ className="absolute w-80 rounded-lg border p-1.5 shadow-xl bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))]"
98
+ style={{ left, top }}
99
+ >
100
+ <div className="flex items-start justify-between gap-2 px-2 pb-1.5 pt-1">
101
+ <div>
102
+ <p className="text-[10px] font-semibold uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]">
103
+ Add this {noun}
104
+ </p>
105
+ <p className="truncate text-xs font-semibold">{resource.name}</p>
106
+ </div>
107
+ <button
108
+ onClick={onCancel}
109
+ aria-label="Cancel"
110
+ className="rounded p-0.5 text-[rgb(var(--ec-icon-color))] hover:bg-[rgb(var(--ec-page-border)/0.5)]"
111
+ >
112
+ <X size={14} />
113
+ </button>
114
+ </div>
115
+
116
+ <div className="grid grid-cols-2 gap-1.5 px-0.5 pb-0.5">
117
+ {options.map(({ as, label, hint, Preview }) => (
118
+ <button
119
+ key={as}
120
+ autoFocus={as === 'connected'}
121
+ disabled={as === 'connected' && include.length === 0}
122
+ onClick={() => onChoose(as === 'card' ? { as } : { as, include })}
123
+ className="flex flex-col items-center gap-1 rounded-md border px-2 pb-1.5 pt-2 text-xs outline-none transition-colors border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text-muted))] hover:border-[rgb(var(--ec-accent))] hover:bg-[rgb(var(--ec-accent)/0.08)] hover:text-[rgb(var(--ec-accent))] focus-visible:border-[rgb(var(--ec-accent))] focus-visible:text-[rgb(var(--ec-accent))] disabled:cursor-not-allowed disabled:opacity-50"
124
+ >
125
+ <Preview />
126
+ <span className="font-semibold">{label}</span>
127
+ <span className="text-center text-[10px] leading-tight text-[rgb(var(--ec-page-text-muted))]">{hint}</span>
128
+ </button>
129
+ ))}
130
+ </div>
131
+
132
+ <div className="mt-1 space-y-0.5 px-2 pb-1">
133
+ {groups.map((group) => (
134
+ <label key={group.id} className="flex items-center gap-2 py-0.5 text-[11px] text-[rgb(var(--ec-page-text-muted))]">
135
+ <input type="checkbox" checked={included.has(group.id)} onChange={() => toggle(group.id)} />
136
+ {group.label} ({group.resources.length})
137
+ </label>
138
+ ))}
139
+ </div>
140
+ </div>
141
+ </div>
142
+ );
143
+ }
@@ -0,0 +1,65 @@
1
+ import { useState } from 'react';
2
+ import { usePeople, type PresenceStore } from '../hooks/presence-store';
3
+ import Dialog, { SECONDARY_BUTTON } from './Dialog';
4
+ import { STATUS } from '@eventcatalog/visualiser/studio';
5
+
6
+ /** Asks before deleting a canvas: it's deleted for everyone, for good */
7
+ export default function DeleteCanvasDialog({
8
+ title,
9
+ presence,
10
+ clientId,
11
+ onDelete,
12
+ onClose,
13
+ }: {
14
+ title?: string;
15
+ /** Who has it open (to say it closes for them) */
16
+ presence: PresenceStore | null;
17
+ clientId: number | null;
18
+ /** Deletes it (rejects with what went wrong) */
19
+ onDelete: () => Promise<void>;
20
+ onClose: () => void;
21
+ }) {
22
+ const othersHere = usePeople(presence).filter((peer) => peer.clientId !== clientId).length;
23
+ const [deleting, setDeleting] = useState(false);
24
+ const [error, setError] = useState<string>();
25
+
26
+ const confirm = async () => {
27
+ setDeleting(true);
28
+ setError(undefined);
29
+ try {
30
+ await onDelete();
31
+ } catch (failure) {
32
+ setError((failure as Error).message);
33
+ setDeleting(false);
34
+ }
35
+ };
36
+
37
+ return (
38
+ <Dialog role="alertdialog" labelledBy="delete-canvas-title" busy={deleting} onClose={onClose}>
39
+ <div className="space-y-2 px-5 py-4">
40
+ <h2 id="delete-canvas-title" className="text-base font-semibold">
41
+ Delete {title ? `"${title}"` : 'this canvas'}?
42
+ </h2>
43
+ <p className="text-sm text-[rgb(var(--ec-page-text-muted))]">
44
+ It's deleted for everyone, with its comments, and can't be brought back.
45
+ {othersHere > 0 &&
46
+ ` ${othersHere === 1 ? 'Someone else has' : `${othersHere} others have`} it open: it closes for them too.`}
47
+ </p>
48
+ {error && <p className={`text-sm ${STATUS.danger.text}`}>{error}</p>}
49
+ </div>
50
+ <div className="flex justify-end gap-2 border-t px-5 py-4 border-[rgb(var(--ec-page-border))]">
51
+ <button onClick={onClose} disabled={deleting} className={SECONDARY_BUTTON}>
52
+ Cancel
53
+ </button>
54
+ <button
55
+ autoFocus
56
+ onClick={confirm}
57
+ disabled={deleting}
58
+ className={`rounded-lg px-4 py-2 text-sm font-medium disabled:opacity-60 ${STATUS.danger.button}`}
59
+ >
60
+ {deleting ? 'Deleting…' : 'Delete canvas'}
61
+ </button>
62
+ </div>
63
+ </Dialog>
64
+ );
65
+ }