@eventcatalog/core 5.0.0-beta.3 → 5.0.0-beta.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +234 -134
- package/README.md +5 -5
- package/dist/analytics/analytics.cjs +18 -3
- package/dist/analytics/analytics.d.cts +1 -1
- package/dist/analytics/analytics.d.ts +1 -1
- package/dist/analytics/analytics.js +3 -2
- package/dist/analytics/log-build.cjs +160 -77
- package/dist/analytics/log-build.d.cts +2 -2
- package/dist/analytics/log-build.d.ts +2 -2
- package/dist/analytics/log-build.js +7 -5
- package/dist/analytics/studio-usage.cjs +90 -0
- package/dist/analytics/studio-usage.d.cts +5 -0
- package/dist/analytics/studio-usage.d.ts +5 -0
- package/dist/analytics/studio-usage.js +10 -0
- package/dist/analytics/telemetry.cjs +42 -0
- package/dist/analytics/telemetry.d.cts +20 -0
- package/dist/analytics/telemetry.d.ts +20 -0
- package/dist/analytics/telemetry.js +8 -0
- package/dist/catalog-runtime.cjs +1 -1
- package/dist/{chunk-RFIXKATC.js → chunk-4G7FSRPN.js} +35 -42
- package/dist/{chunk-6OC46WQJ.js → chunk-55QETQZ3.js} +66 -35
- package/dist/{chunk-TQLRG35T.js → chunk-AD7CL772.js} +78 -45
- package/dist/chunk-CA6WCQ2T.js +54 -0
- package/dist/chunk-CPW6G62V.js +17 -0
- package/dist/{chunk-423S2KHN.js → chunk-FLAZ2JVN.js} +4 -2
- package/dist/{chunk-BH2F5WPN.js → chunk-GQI5UYXZ.js} +1 -1
- package/dist/{chunk-GYIVJXOJ.js → chunk-NA7OTBWW.js} +1 -1
- package/dist/{chunk-MDWB5HLO.js → chunk-NKNOXQ62.js} +1 -1
- package/dist/{chunk-3PKS2IIP.js → chunk-OX6TSVHY.js} +1 -1
- package/dist/{chunk-36RYTYT5.js → chunk-PD7KTSYN.js} +1 -1
- package/dist/chunk-S5GWAUG5.js +29 -0
- package/dist/{chunk-NDJOAITI.js → chunk-T3OROMAU.js} +9 -3
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog-config-file-utils.cjs +34 -41
- package/dist/eventcatalog-config-file-utils.d.cts +56 -58
- package/dist/eventcatalog-config-file-utils.d.ts +56 -58
- package/dist/eventcatalog-config-file-utils.js +1 -1
- package/dist/eventcatalog.cjs +531 -356
- package/dist/eventcatalog.config.d.cts +59 -1
- package/dist/eventcatalog.config.d.ts +59 -1
- package/dist/eventcatalog.js +63 -25
- package/dist/features.cjs +34 -41
- package/dist/features.js +2 -2
- package/dist/federation/federate.cjs +37 -44
- package/dist/federation/federate.js +2 -2
- package/dist/generate.cjs +37 -42
- package/dist/generate.js +4 -4
- package/dist/resolve-catalog-dependencies.cjs +34 -41
- package/dist/resolve-catalog-dependencies.js +2 -2
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio-fonts.css.mjs +3 -0
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.css.mjs +3 -0
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.mjs +3 -0
- package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
- package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
- package/dist/runtime-dependencies/htmlparser2.mjs +3 -0
- package/dist/runtime-dependencies/manifest.json +13 -1
- package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
- package/dist/runtime-dependencies/react-syntax-highlighter__dist__cjs__create-element.js.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
- package/dist/runtime-dependencies/yjs.mjs +3 -0
- package/dist/utils/child-process-signals.cjs +53 -0
- package/dist/utils/child-process-signals.d.cts +19 -0
- package/dist/utils/child-process-signals.d.ts +19 -0
- package/dist/utils/child-process-signals.js +6 -0
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/dist/utils/license-status.cjs +78 -45
- package/dist/utils/license-status.d.cts +15 -5
- package/dist/utils/license-status.d.ts +15 -5
- package/dist/utils/license-status.js +1 -1
- package/eventcatalog/astro.config.mjs +23 -1
- package/eventcatalog/integrations/config-loader.mjs +56 -58
- package/eventcatalog/integrations/skip-startup-reload.mjs +31 -0
- package/eventcatalog/integrations/stable-dependency-order.mjs +29 -0
- package/eventcatalog/integrations/storage.mjs +20 -0
- package/eventcatalog/integrations/studio-server.mjs +32 -0
- package/eventcatalog/src/components/CatalogGraph/CatalogForceGraph.tsx +39 -0
- package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +2 -0
- package/eventcatalog/src/components/Header.astro +3 -3
- package/eventcatalog/src/components/Homepage/CatalogDashboard.astro +342 -0
- package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +60 -4
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -1
- package/eventcatalog/src/components/NavigationProgress.astro +7 -0
- package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +2 -0
- package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +2 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +2 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +2 -0
- package/eventcatalog/src/components/Search/SearchDataLoader.astro +16 -28
- package/eventcatalog/src/components/Search/SearchModal.tsx +2 -10
- package/eventcatalog/src/components/Search/search-utils.ts +26 -0
- package/eventcatalog/src/components/Seo.astro +5 -1
- package/eventcatalog/src/components/Settings/AssistantSettingsForm.tsx +53 -48
- package/eventcatalog/src/components/Settings/GeneralSettingsForm.tsx +183 -178
- package/eventcatalog/src/components/Settings/LicenseSettings.tsx +140 -31
- package/eventcatalog/src/components/Settings/LlmAccessSettingsForm.tsx +40 -35
- package/eventcatalog/src/components/Settings/LogoUpload.tsx +1 -1
- package/eventcatalog/src/components/Settings/McpSettingsForm.tsx +9 -8
- package/eventcatalog/src/components/Settings/Row.tsx +7 -4
- package/eventcatalog/src/components/Settings/SettingsShared.tsx +3 -3
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +53 -83
- package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +2 -0
- package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +131 -0
- package/eventcatalog/src/components/SideNav/SideNav.astro +9 -1
- package/eventcatalog/src/components/TrialBadge.astro +20 -9
- package/eventcatalog/src/content.config.ts +19 -2
- package/eventcatalog/src/features/auth/error.astro +4 -2
- package/eventcatalog/src/features/auth/login.astro +2 -1
- package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
- package/eventcatalog/src/features/auth/unauthorized.astro +1 -1
- package/eventcatalog/src/features/fields/pages/fields.astro +1 -1
- package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
- package/eventcatalog/src/features/mcp/apps/architecture-diagram/node-chat.tsx +6 -45
- package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
- package/eventcatalog/src/features/mcp/apps/canvas/styles.css +24 -0
- package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +278 -0
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +272 -0
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +65 -65
- package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
- package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
- package/eventcatalog/src/features/mcp/mcp-server.ts +36 -5
- package/eventcatalog/src/features/server/start.ts +25 -0
- package/eventcatalog/src/features/storage/database.ts +88 -0
- package/eventcatalog/src/features/storage/migrations.ts +85 -0
- package/eventcatalog/src/features/storage/sql/database.sql +5 -0
- package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
- package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
- package/eventcatalog/src/features/storage/sql.ts +28 -0
- package/eventcatalog/src/features/studio/agent-choreography.ts +384 -0
- package/eventcatalog/src/features/studio/agents.ts +28 -0
- package/eventcatalog/src/features/studio/api/canvases-api.ts +219 -0
- package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
- package/eventcatalog/src/features/studio/api/client.ts +40 -0
- package/eventcatalog/src/features/studio/canvas-actions.ts +540 -0
- package/eventcatalog/src/features/studio/canvas-preview.ts +52 -0
- package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
- package/eventcatalog/src/features/studio/catalog.ts +173 -0
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +518 -0
- package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +118 -0
- package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +434 -0
- package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +143 -0
- package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +65 -0
- package/eventcatalog/src/features/studio/components/Dialog.tsx +60 -0
- package/eventcatalog/src/features/studio/components/JoinForm.tsx +109 -0
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +206 -0
- package/eventcatalog/src/features/studio/components/NodeDetails.tsx +183 -0
- package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +79 -0
- package/eventcatalog/src/features/studio/components/Presence.tsx +293 -0
- package/eventcatalog/src/features/studio/components/ShareDialog.tsx +396 -0
- package/eventcatalog/src/features/studio/components/ShortcutsDialog.tsx +41 -0
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +2091 -0
- package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
- package/eventcatalog/src/features/studio/document-name.ts +7 -0
- package/eventcatalog/src/features/studio/from-visualiser.ts +138 -0
- package/eventcatalog/src/features/studio/hooks/presence-sender.ts +63 -0
- package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +353 -0
- package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
- package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +109 -0
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +782 -0
- package/eventcatalog/src/features/studio/identity.ts +59 -0
- package/eventcatalog/src/features/studio/limits.ts +2 -0
- package/eventcatalog/src/features/studio/pages/[id].astro +69 -0
- package/eventcatalog/src/features/studio/pages/index.astro +784 -0
- package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +495 -0
- package/eventcatalog/src/features/studio/server/runtime.ts +498 -0
- package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
- package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
- package/eventcatalog/src/features/studio/server/storage.ts +90 -0
- package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
- package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
- package/eventcatalog/src/layouts/BaseLayout.astro +4 -4
- package/eventcatalog/src/layouts/SettingsLayout.astro +2 -2
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +13 -5
- package/eventcatalog/src/pages/_index.astro +5 -342
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +2 -3
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +3 -1
- package/eventcatalog/src/pages/diagrams/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/diagrams/[id]/[version]/embed.astro +1 -0
- package/eventcatalog/src/pages/diagrams/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/directory/[type]/_index.data.ts +5 -7
- package/eventcatalog/src/pages/directory/[type]/index.astro +3 -1
- package/eventcatalog/src/pages/discover/[type]/_index.data.ts +5 -7
- package/eventcatalog/src/pages/discover/[type]/index.astro +3 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +3 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/_[filename].data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/_[...filename].data.ts +0 -4
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +3 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/_[filename].data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +2 -3
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/_[filename].data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/index.astro +2 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/language/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +1 -0
- package/eventcatalog/src/pages/docs/custom/[...path]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/custom/[...path]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/services/[id]/[docType]/[docId]/index.astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +1 -0
- package/eventcatalog/src/pages/docs/services/[id]/asyncapi/[filename].astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/changelog/index.astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/graphql/[filename].astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/spec/[filename].astro +1 -1
- package/eventcatalog/src/pages/docs/teams/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/teams/[id]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/users/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/users/[id]/index.astro +1 -0
- package/eventcatalog/src/pages/index.astro +4 -2
- package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/schemas/explorer/_index.data.ts +0 -7
- package/eventcatalog/src/pages/schemas/explorer/index.astro +4 -2
- package/eventcatalog/src/pages/settings/license.astro +9 -1
- package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/_index.data.ts +38 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +18 -41
- package/eventcatalog/src/pages/visualiser/designs/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +2 -1
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/types/react-syntax-highlighter.d.ts +10 -0
- package/eventcatalog/src/utils/collections/util.ts +8 -4
- package/eventcatalog/src/utils/feature.ts +8 -0
- package/eventcatalog/src/utils/git.ts +22 -5
- package/eventcatalog/src/utils/homepage-dashboard.ts +143 -0
- package/eventcatalog/src/utils/license.ts +1 -1
- package/eventcatalog/src/utils/link-validation.ts +25 -19
- package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +3 -1
- package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +3 -3
- package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
- package/eventcatalog/src/utils/node-graphs/field-node-graph.ts +4 -2
- package/eventcatalog/src/utils/node-graphs/layout-node-graph.ts +20 -1
- package/eventcatalog/src/utils/node-graphs/node-resource.ts +67 -0
- package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +1 -1
- package/eventcatalog/src/utils/page-loaders/hybrid-page.ts +4 -15
- package/eventcatalog/src/utils/page-title.ts +15 -0
- package/eventcatalog/src/utils/syntax-highlighter-renderer.ts +64 -0
- package/eventcatalog/src/utils/trial.ts +14 -1
- package/package.json +19 -13
- package/dist/runtime-dependencies/shelljs.mjs +0 -3
- package/eventcatalog/public/studio-bg.png +0 -0
- package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
- package/eventcatalog/src/pages/studio.astro +0 -316
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { memo, useEffect, useRef, useState, type ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
CircleHelp,
|
|
4
|
+
Download,
|
|
5
|
+
FolderOpen,
|
|
6
|
+
ImageDown,
|
|
7
|
+
Link2,
|
|
8
|
+
MoreVertical,
|
|
9
|
+
Pencil,
|
|
10
|
+
Plug,
|
|
11
|
+
Plus,
|
|
12
|
+
Share2,
|
|
13
|
+
Trash2,
|
|
14
|
+
Users,
|
|
15
|
+
} from 'lucide-react';
|
|
16
|
+
import {
|
|
17
|
+
DIAGRAM_MENU,
|
|
18
|
+
DIAGRAM_MENU_BUTTON_ICON,
|
|
19
|
+
DIAGRAM_MENU_ITEM,
|
|
20
|
+
DIAGRAM_MENU_ITEM_ICON,
|
|
21
|
+
DIAGRAM_MENU_SEPARATOR,
|
|
22
|
+
DIAGRAM_MENU_TITLE,
|
|
23
|
+
} from '@eventcatalog/visualiser';
|
|
24
|
+
import { usePeople, type Peer, type PresenceStore } from '../hooks/presence-store';
|
|
25
|
+
import { MAX_NAME_LENGTH, PERSON_COLORS } from '../identity';
|
|
26
|
+
import type { WebMcpStatus } from '../hooks/use-canvas-webmcp';
|
|
27
|
+
import type { Status, Transport } from '../hooks/use-studio-flow';
|
|
28
|
+
import { type CanvasStatus, type StatusChange, STATUS, Picture, AgentMark } from '@eventcatalog/visualiser/studio';
|
|
29
|
+
import CanvasStatusMenu from './CanvasStatusMenu';
|
|
30
|
+
|
|
31
|
+
/** The dot showing whether the canvas is connected */
|
|
32
|
+
const CONNECTION_DOT: Record<Status, string> = {
|
|
33
|
+
connected: STATUS.success.dot,
|
|
34
|
+
connecting: STATUS.warning.dot,
|
|
35
|
+
disconnected: STATUS.danger.dot,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The canvas's header, as Studio on the web's top bar: on the left the canvas (its title, its status and a menu), on
|
|
40
|
+
* the right who's here, connecting an agent and sharing, each a small card over the canvas. Its menus look like the
|
|
41
|
+
* visualiser's (`DIAGRAM_MENU_*`).
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const barClass =
|
|
45
|
+
'pointer-events-auto flex h-[38px] items-center rounded-[9px] border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] shadow-sm';
|
|
46
|
+
// The header lets the pointer through to the canvas around its bars, so its menus take it back
|
|
47
|
+
const popoverClass = 'pointer-events-auto absolute top-full mt-2';
|
|
48
|
+
const menuClass = `${popoverClass} ${DIAGRAM_MENU}`;
|
|
49
|
+
const menuItemClass = `${DIAGRAM_MENU_ITEM} w-full text-left`;
|
|
50
|
+
const MAX_AVATARS = 4;
|
|
51
|
+
const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform);
|
|
52
|
+
const MOD = isMac ? 'Cmd' : 'Ctrl';
|
|
53
|
+
const itemClass =
|
|
54
|
+
'flex h-[34px] w-full cursor-pointer items-center gap-3 rounded-[7px] px-2 text-left text-sm outline-none hover:bg-[rgb(var(--ec-content-hover))]';
|
|
55
|
+
const itemIconClass = 'flex flex-none [&_svg]:h-[17px] [&_svg]:w-[17px] [&_svg]:stroke-[1.75]';
|
|
56
|
+
|
|
57
|
+
/** Closes a popover on outside clicks and Escape */
|
|
58
|
+
function usePopover() {
|
|
59
|
+
const [open, setOpen] = useState(false);
|
|
60
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!open) return;
|
|
63
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
64
|
+
if (!ref.current?.contains(event.target as globalThis.Node)) setOpen(false);
|
|
65
|
+
};
|
|
66
|
+
const onKeyDown = (event: KeyboardEvent) => event.key === 'Escape' && setOpen(false);
|
|
67
|
+
window.addEventListener('pointerdown', onPointerDown);
|
|
68
|
+
window.addEventListener('keydown', onKeyDown);
|
|
69
|
+
return () => {
|
|
70
|
+
window.removeEventListener('pointerdown', onPointerDown);
|
|
71
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
72
|
+
};
|
|
73
|
+
}, [open]);
|
|
74
|
+
return { open, setOpen, ref };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function Avatar({ peer, size = 28, children }: { peer: Peer; size?: number; children?: ReactNode }) {
|
|
78
|
+
return (
|
|
79
|
+
<span
|
|
80
|
+
className="relative flex items-center justify-center rounded-full text-[11px] font-semibold text-white ring-2 ring-[rgb(var(--ec-card-bg))]"
|
|
81
|
+
style={{ backgroundColor: peer.color, width: size, height: size }}
|
|
82
|
+
>
|
|
83
|
+
{children ?? (peer.agent ? <AgentMark name={peer.name} size={14} /> : peer.name.slice(0, 2).toUpperCase())}
|
|
84
|
+
{!children && <Picture src={peer.picture} />}
|
|
85
|
+
</span>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function CanvasMenu({
|
|
90
|
+
title,
|
|
91
|
+
status,
|
|
92
|
+
transport,
|
|
93
|
+
onRetitle,
|
|
94
|
+
shareUrl,
|
|
95
|
+
newCanvasUrl,
|
|
96
|
+
onConnectAgent,
|
|
97
|
+
webMcp,
|
|
98
|
+
agentsHere,
|
|
99
|
+
onDelete,
|
|
100
|
+
onOpenFile,
|
|
101
|
+
onSaveToFile,
|
|
102
|
+
onExportImage,
|
|
103
|
+
onLiveCollaboration,
|
|
104
|
+
onHelp,
|
|
105
|
+
children,
|
|
106
|
+
}: {
|
|
107
|
+
/** Opens a .eventcatalog file as a new canvas (not offered when not given, e.g. inside a chat) */
|
|
108
|
+
onOpenFile?: () => void;
|
|
109
|
+
onSaveToFile: () => void;
|
|
110
|
+
onExportImage: () => void;
|
|
111
|
+
onLiveCollaboration: () => void;
|
|
112
|
+
onHelp: () => void;
|
|
113
|
+
title?: string;
|
|
114
|
+
status: Status;
|
|
115
|
+
transport: Transport;
|
|
116
|
+
onRetitle: (title: string) => void;
|
|
117
|
+
shareUrl?: string;
|
|
118
|
+
newCanvasUrl?: string;
|
|
119
|
+
onConnectAgent: () => void;
|
|
120
|
+
webMcp: WebMcpStatus;
|
|
121
|
+
/** An AI agent is on the canvas */
|
|
122
|
+
agentsHere: boolean;
|
|
123
|
+
/** Asks to delete the canvas (not offered when not given, e.g. inside a chat) */
|
|
124
|
+
onDelete?: () => void;
|
|
125
|
+
/** Shown after the title (the canvas's status) */
|
|
126
|
+
children?: ReactNode;
|
|
127
|
+
}) {
|
|
128
|
+
const menu = usePopover();
|
|
129
|
+
const titleInput = useRef<HTMLInputElement>(null);
|
|
130
|
+
const [draft, setDraft] = useState<string | null>(null);
|
|
131
|
+
const statusLabel = {
|
|
132
|
+
connected: transport === 'tools' ? 'Connected through the chat' : 'Connected',
|
|
133
|
+
connecting: 'Connecting…',
|
|
134
|
+
disconnected: 'Offline, reconnecting…',
|
|
135
|
+
}[status];
|
|
136
|
+
const statusColor = CONNECTION_DOT[status];
|
|
137
|
+
/** A menu item, as Studio on the web's: its icon, what it does, and its shortcut (or something else on the right) */
|
|
138
|
+
const item = (icon: ReactNode, label: string, onSelect: () => void, shortcut?: ReactNode, className = '') => (
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
className={`${itemClass} ${className}`}
|
|
142
|
+
onClick={() => {
|
|
143
|
+
menu.setOpen(false);
|
|
144
|
+
onSelect();
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
<span className={itemIconClass}>{icon}</span>
|
|
148
|
+
<span className="min-w-0 flex-1 truncate">{label}</span>
|
|
149
|
+
{typeof shortcut === 'string' ? (
|
|
150
|
+
<span className="flex-none text-xs font-normal text-[rgb(var(--ec-page-text-muted))]">{shortcut}</span>
|
|
151
|
+
) : (
|
|
152
|
+
shortcut
|
|
153
|
+
)}
|
|
154
|
+
</button>
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div ref={menu.ref} className="relative">
|
|
159
|
+
<div className={`${barClass} gap-1.5 pl-3 pr-1`}>
|
|
160
|
+
<span title={statusLabel} className={`h-2 w-2 shrink-0 rounded-full ${statusColor}`} />
|
|
161
|
+
<input
|
|
162
|
+
ref={titleInput}
|
|
163
|
+
aria-label="Canvas title"
|
|
164
|
+
value={draft ?? title ?? ''}
|
|
165
|
+
placeholder="Untitled canvas"
|
|
166
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
167
|
+
onBlur={() => {
|
|
168
|
+
if (draft !== null && draft.trim() !== (title ?? '')) onRetitle(draft.trim());
|
|
169
|
+
setDraft(null);
|
|
170
|
+
}}
|
|
171
|
+
onKeyDown={(e) => e.key === 'Enter' && e.currentTarget.blur()}
|
|
172
|
+
size={Math.max(12, (draft ?? title ?? 'Untitled canvas').length)}
|
|
173
|
+
className={`${DIAGRAM_MENU_TITLE} max-w-64 truncate rounded-md bg-transparent px-1.5 py-1 placeholder:text-[rgb(var(--ec-page-text-muted))] hover:bg-[rgb(var(--ec-page-border)/0.4)] focus:bg-[rgb(var(--ec-input-bg))] focus:outline-none`}
|
|
174
|
+
/>
|
|
175
|
+
{children}
|
|
176
|
+
<button
|
|
177
|
+
aria-label="Canvas menu"
|
|
178
|
+
onClick={() => menu.setOpen((open) => !open)}
|
|
179
|
+
className="group rounded-md p-1 hover:bg-[rgb(var(--ec-accent-subtle))]"
|
|
180
|
+
>
|
|
181
|
+
<MoreVertical className={DIAGRAM_MENU_BUTTON_ICON} />
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
{menu.open && (
|
|
185
|
+
<div
|
|
186
|
+
className={`${popoverClass} left-0 z-50 w-72 rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] p-1.5 text-[rgb(var(--ec-page-text))] shadow-xl`}
|
|
187
|
+
>
|
|
188
|
+
{/* As Studio on the web's menu: files and images, people and agents, help */}
|
|
189
|
+
{onOpenFile && item(<FolderOpen />, 'Open…', onOpenFile, `${MOD}+O`)}
|
|
190
|
+
{item(<Download />, 'Save to file…', onSaveToFile, `${MOD}+S`)}
|
|
191
|
+
{item(<ImageDown />, 'Export image…', onExportImage, `${MOD}+Shift+E`)}
|
|
192
|
+
{item(<Users />, 'Live collaboration…', onLiveCollaboration)}
|
|
193
|
+
{item(
|
|
194
|
+
<Plug />,
|
|
195
|
+
agentsHere ? 'AI agent connected' : 'Connect your AI agent…',
|
|
196
|
+
onConnectAgent,
|
|
197
|
+
webMcp.state === 'on' || agentsHere ? (
|
|
198
|
+
<span className={`h-1.5 w-1.5 rounded-full ${STATUS.success.dot}`} />
|
|
199
|
+
) : undefined
|
|
200
|
+
)}
|
|
201
|
+
{item(<CircleHelp />, 'Help', onHelp, '?')}
|
|
202
|
+
<div className="my-1.5 h-px bg-[rgb(var(--ec-page-border))]" />
|
|
203
|
+
{item(<Pencil />, 'Rename canvas', () => {
|
|
204
|
+
titleInput.current?.focus();
|
|
205
|
+
titleInput.current?.select();
|
|
206
|
+
})}
|
|
207
|
+
{item(<Link2 />, 'Copy link', () => void navigator.clipboard.writeText(shareUrl ?? window.location.href))}
|
|
208
|
+
{newCanvasUrl && (
|
|
209
|
+
<a className={itemClass} href={newCanvasUrl}>
|
|
210
|
+
<span className={itemIconClass}>
|
|
211
|
+
<Plus />
|
|
212
|
+
</span>
|
|
213
|
+
<span className="min-w-0 flex-1 truncate">New canvas</span>
|
|
214
|
+
</a>
|
|
215
|
+
)}
|
|
216
|
+
{onDelete && (
|
|
217
|
+
<>
|
|
218
|
+
<div className="my-1.5 h-px bg-[rgb(var(--ec-page-border))]" />
|
|
219
|
+
{item(<Trash2 />, 'Delete canvas', onDelete, undefined, `${STATUS.danger.text} ${STATUS.danger.hover}`)}
|
|
220
|
+
</>
|
|
221
|
+
)}
|
|
222
|
+
<div className="my-1.5 h-px bg-[rgb(var(--ec-page-border))]" />
|
|
223
|
+
<p className="flex items-center gap-2 px-2 py-1.5 text-[11px] text-[rgb(var(--ec-page-text-muted))]">
|
|
224
|
+
<span className={`h-1.5 w-1.5 rounded-full ${statusColor}`} />
|
|
225
|
+
{statusLabel}
|
|
226
|
+
</p>
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const initialsOf = (name: string) => {
|
|
234
|
+
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
235
|
+
return (words.length > 1 ? words.map((word) => word[0]).join('') : name.trim()).slice(0, 2).toUpperCase();
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Who you are here, top right (as Studio on the web's): your avatar, opening your name and colour to change. They're
|
|
240
|
+
* on your pointer and comments. Signed in, your name and picture are the ones you signed in with.
|
|
241
|
+
*/
|
|
242
|
+
function ProfileMenu({
|
|
243
|
+
me,
|
|
244
|
+
onRename,
|
|
245
|
+
onRecolor,
|
|
246
|
+
}: {
|
|
247
|
+
me: { name: string; color: string; picture?: string; agent?: boolean };
|
|
248
|
+
/** Not given when you're signed in */
|
|
249
|
+
onRename?: (name: string) => void;
|
|
250
|
+
onRecolor: (color: string) => void;
|
|
251
|
+
}) {
|
|
252
|
+
const popover = usePopover();
|
|
253
|
+
const [draft, setDraft] = useState(me.name);
|
|
254
|
+
useEffect(() => setDraft(me.name), [me.name]);
|
|
255
|
+
const save = () => {
|
|
256
|
+
const next = draft.trim();
|
|
257
|
+
if (!next) return setDraft(me.name);
|
|
258
|
+
if (next !== me.name) onRename?.(next);
|
|
259
|
+
};
|
|
260
|
+
const avatar = (size: string, text: string) => (
|
|
261
|
+
<span
|
|
262
|
+
className={`relative flex ${size} flex-none items-center justify-center rounded-full text-[10px] font-semibold text-white`}
|
|
263
|
+
style={{ background: me.color }}
|
|
264
|
+
>
|
|
265
|
+
{initialsOf(text)}
|
|
266
|
+
<Picture src={me.picture} />
|
|
267
|
+
</span>
|
|
268
|
+
);
|
|
269
|
+
// Here as an AI agent (e.g. a chat's view, as the chat's agent): who you are is its logo, not yours to change
|
|
270
|
+
if (me.agent)
|
|
271
|
+
return (
|
|
272
|
+
<span title={`You're here as ${me.name} (AI agent)`} className={`${barClass} pointer-events-auto w-[38px] justify-center`}>
|
|
273
|
+
<span className="flex h-6 w-6 items-center justify-center rounded-full text-white" style={{ background: me.color }}>
|
|
274
|
+
<AgentMark name={me.name} size={14} />
|
|
275
|
+
</span>
|
|
276
|
+
</span>
|
|
277
|
+
);
|
|
278
|
+
return (
|
|
279
|
+
<div ref={popover.ref} className="pointer-events-auto relative">
|
|
280
|
+
<button
|
|
281
|
+
type="button"
|
|
282
|
+
title={onRename ? `${me.name}: change your name and colour` : `${me.name}: change your colour`}
|
|
283
|
+
onClick={() => popover.setOpen((open) => !open)}
|
|
284
|
+
className={`${barClass} w-[38px] justify-center hover:bg-[rgb(var(--ec-content-hover))]`}
|
|
285
|
+
>
|
|
286
|
+
{avatar('h-6 w-6', me.name)}
|
|
287
|
+
</button>
|
|
288
|
+
{popover.open && (
|
|
289
|
+
<div
|
|
290
|
+
className={`${popoverClass} right-0 z-[70] w-72 rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] p-4 text-[rgb(var(--ec-page-text))] shadow-xl`}
|
|
291
|
+
>
|
|
292
|
+
<div className="flex items-center gap-3">
|
|
293
|
+
{avatar('h-9 w-9 text-xs', draft || me.name)}
|
|
294
|
+
<div className="min-w-0">
|
|
295
|
+
<p className="truncate text-sm font-semibold">{draft || me.name}</p>
|
|
296
|
+
<p className="text-xs text-[rgb(var(--ec-page-text-muted))]">On your pointer and comments</p>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
{onRename && (
|
|
300
|
+
<label className="mt-4 block text-xs font-medium text-[rgb(var(--ec-page-text-muted))]">
|
|
301
|
+
Your name
|
|
302
|
+
<input
|
|
303
|
+
value={draft}
|
|
304
|
+
maxLength={MAX_NAME_LENGTH}
|
|
305
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
306
|
+
onBlur={save}
|
|
307
|
+
onKeyDown={(event) => event.key === 'Enter' && event.currentTarget.blur()}
|
|
308
|
+
className="mt-1.5 block h-9 w-full rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg))] px-3 text-sm text-[rgb(var(--ec-page-text))] outline-none focus:border-[rgb(var(--ec-accent))]"
|
|
309
|
+
/>
|
|
310
|
+
</label>
|
|
311
|
+
)}
|
|
312
|
+
<div className="mt-3 text-xs font-medium text-[rgb(var(--ec-page-text-muted))]">
|
|
313
|
+
Your colour
|
|
314
|
+
<div role="radiogroup" aria-label="Your colour" className="mt-2 flex gap-2">
|
|
315
|
+
{PERSON_COLORS.map((color) => (
|
|
316
|
+
<button
|
|
317
|
+
key={color}
|
|
318
|
+
type="button"
|
|
319
|
+
role="radio"
|
|
320
|
+
aria-checked={color === me.color}
|
|
321
|
+
aria-label={color}
|
|
322
|
+
onClick={() => onRecolor(color)}
|
|
323
|
+
className={`h-6 w-6 rounded-full transition-transform hover:scale-110 ${
|
|
324
|
+
color === me.color
|
|
325
|
+
? 'ring-2 ring-[rgb(var(--ec-page-text))] ring-offset-2 ring-offset-[rgb(var(--ec-card-bg))]'
|
|
326
|
+
: ''
|
|
327
|
+
}`}
|
|
328
|
+
style={{ background: color }}
|
|
329
|
+
/>
|
|
330
|
+
))}
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Who's here: click someone to jump to them; everyone past the first few is in a list */
|
|
340
|
+
/**
|
|
341
|
+
* Who else is here. An agent is shown once, however many places it's in (e.g. its MCP tools and its chat's canvas
|
|
342
|
+
* view), and not at all when it's you.
|
|
343
|
+
*/
|
|
344
|
+
const othersHere = (peers: Peer[], clientId: number | null, me: { name: string; agent?: boolean }) => {
|
|
345
|
+
const agentsSeen = new Set(me.agent ? [me.name] : []);
|
|
346
|
+
return peers.filter((peer) => {
|
|
347
|
+
if (peer.clientId === clientId) return false;
|
|
348
|
+
if (!peer.agent) return true;
|
|
349
|
+
if (agentsSeen.has(peer.name)) return false;
|
|
350
|
+
agentsSeen.add(peer.name);
|
|
351
|
+
return true;
|
|
352
|
+
});
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
function People({
|
|
356
|
+
peers,
|
|
357
|
+
clientId,
|
|
358
|
+
me,
|
|
359
|
+
onJumpTo,
|
|
360
|
+
}: {
|
|
361
|
+
peers: Peer[];
|
|
362
|
+
clientId: number | null;
|
|
363
|
+
me: { name: string; agent?: boolean };
|
|
364
|
+
onJumpTo: (peer: Peer) => void;
|
|
365
|
+
}) {
|
|
366
|
+
const overflow = usePopover();
|
|
367
|
+
const others = othersHere(peers, clientId, me);
|
|
368
|
+
const shown = others.slice(0, MAX_AVATARS);
|
|
369
|
+
const hidden = others.slice(MAX_AVATARS);
|
|
370
|
+
const describe = (peer: Peer) => `${peer.name}${peer.agent ? ` (AI agent)${peer.activity ? `: ${peer.activity}` : ''}` : ''}`;
|
|
371
|
+
|
|
372
|
+
return (
|
|
373
|
+
<div className="flex items-center gap-1.5">
|
|
374
|
+
<div className="flex -space-x-1.5">
|
|
375
|
+
{shown.map((peer) => (
|
|
376
|
+
<button
|
|
377
|
+
key={peer.clientId}
|
|
378
|
+
title={`${describe(peer)}. Click to jump to them`}
|
|
379
|
+
onClick={() => onJumpTo(peer)}
|
|
380
|
+
className="block rounded-full transition-transform hover:z-10 hover:scale-110"
|
|
381
|
+
>
|
|
382
|
+
<Avatar peer={peer} />
|
|
383
|
+
</button>
|
|
384
|
+
))}
|
|
385
|
+
{hidden.length > 0 && (
|
|
386
|
+
<div ref={overflow.ref} className="relative">
|
|
387
|
+
<button
|
|
388
|
+
title={`${hidden.length} more`}
|
|
389
|
+
onClick={() => overflow.setOpen((open) => !open)}
|
|
390
|
+
className="flex h-7 w-7 items-center justify-center rounded-full text-[11px] font-semibold ring-2 ring-[rgb(var(--ec-card-bg))] bg-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))]"
|
|
391
|
+
>
|
|
392
|
+
+{hidden.length}
|
|
393
|
+
</button>
|
|
394
|
+
{overflow.open && (
|
|
395
|
+
<div className={`${menuClass} right-0`}>
|
|
396
|
+
{hidden.map((peer) => (
|
|
397
|
+
<button
|
|
398
|
+
key={peer.clientId}
|
|
399
|
+
className={menuItemClass}
|
|
400
|
+
onClick={() => {
|
|
401
|
+
onJumpTo(peer);
|
|
402
|
+
overflow.setOpen(false);
|
|
403
|
+
}}
|
|
404
|
+
>
|
|
405
|
+
<Avatar peer={peer} size={20} />
|
|
406
|
+
{describe(peer)}
|
|
407
|
+
</button>
|
|
408
|
+
))}
|
|
409
|
+
</div>
|
|
410
|
+
)}
|
|
411
|
+
</div>
|
|
412
|
+
)}
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export default memo(function CanvasHeader({
|
|
419
|
+
title,
|
|
420
|
+
onRetitle,
|
|
421
|
+
status,
|
|
422
|
+
transport,
|
|
423
|
+
presence,
|
|
424
|
+
clientId,
|
|
425
|
+
onJumpTo,
|
|
426
|
+
me,
|
|
427
|
+
onRename,
|
|
428
|
+
onRecolor,
|
|
429
|
+
onConnectAgent,
|
|
430
|
+
webMcp,
|
|
431
|
+
shareUrl,
|
|
432
|
+
newCanvasUrl,
|
|
433
|
+
onShare,
|
|
434
|
+
onOpenFile,
|
|
435
|
+
onSaveToFile,
|
|
436
|
+
onExportImage,
|
|
437
|
+
onLiveCollaboration,
|
|
438
|
+
onHelp,
|
|
439
|
+
canvasStatus,
|
|
440
|
+
statusHistory,
|
|
441
|
+
onStatusChange,
|
|
442
|
+
onDelete,
|
|
443
|
+
}: {
|
|
444
|
+
title?: string;
|
|
445
|
+
onRetitle: (title: string) => void;
|
|
446
|
+
canvasStatus: CanvasStatus;
|
|
447
|
+
statusHistory: StatusChange[];
|
|
448
|
+
onStatusChange: (status: CanvasStatus, note?: string) => void;
|
|
449
|
+
status: Status;
|
|
450
|
+
transport: Transport;
|
|
451
|
+
presence: PresenceStore | null;
|
|
452
|
+
clientId: number | null;
|
|
453
|
+
onJumpTo: (peer: Peer) => void;
|
|
454
|
+
/** You: your name, colour and (signed in) picture */
|
|
455
|
+
me: { name: string; color: string; picture?: string; agent?: boolean };
|
|
456
|
+
/** Not given when you're signed in */
|
|
457
|
+
onRename?: (name: string) => void;
|
|
458
|
+
onRecolor: (color: string) => void;
|
|
459
|
+
onConnectAgent: () => void;
|
|
460
|
+
webMcp: WebMcpStatus;
|
|
461
|
+
shareUrl?: string;
|
|
462
|
+
newCanvasUrl?: string;
|
|
463
|
+
/** Open the share dialog */
|
|
464
|
+
onShare: () => void;
|
|
465
|
+
/** Opens a .eventcatalog file as a new canvas (not given where it can't, e.g. inside a chat) */
|
|
466
|
+
onOpenFile?: () => void;
|
|
467
|
+
onSaveToFile: () => void;
|
|
468
|
+
onExportImage: () => void;
|
|
469
|
+
onLiveCollaboration: () => void;
|
|
470
|
+
onHelp: () => void;
|
|
471
|
+
/** Ask to delete the canvas (not given where it can't be, e.g. inside a chat) */
|
|
472
|
+
onDelete?: () => void;
|
|
473
|
+
}) {
|
|
474
|
+
const peers = usePeople(presence);
|
|
475
|
+
const agentsHere = peers.some((peer) => peer.agent);
|
|
476
|
+
// Who else is here (you're your own avatar, next to it)
|
|
477
|
+
const showPeople = othersHere(peers, clientId, me).length > 0;
|
|
478
|
+
|
|
479
|
+
return (
|
|
480
|
+
<div className="pointer-events-none absolute left-3 right-3 top-3 z-10 flex items-start justify-between gap-3">
|
|
481
|
+
<CanvasMenu
|
|
482
|
+
title={title}
|
|
483
|
+
status={status}
|
|
484
|
+
transport={transport}
|
|
485
|
+
onRetitle={onRetitle}
|
|
486
|
+
shareUrl={shareUrl}
|
|
487
|
+
newCanvasUrl={newCanvasUrl}
|
|
488
|
+
onConnectAgent={onConnectAgent}
|
|
489
|
+
webMcp={webMcp}
|
|
490
|
+
agentsHere={agentsHere}
|
|
491
|
+
onDelete={onDelete}
|
|
492
|
+
onOpenFile={onOpenFile}
|
|
493
|
+
onSaveToFile={onSaveToFile}
|
|
494
|
+
onExportImage={onExportImage}
|
|
495
|
+
onLiveCollaboration={onLiveCollaboration}
|
|
496
|
+
onHelp={onHelp}
|
|
497
|
+
>
|
|
498
|
+
<CanvasStatusMenu status={canvasStatus} history={statusHistory} onChange={onStatusChange} />
|
|
499
|
+
</CanvasMenu>
|
|
500
|
+
|
|
501
|
+
<div className="flex items-center gap-2">
|
|
502
|
+
{showPeople && (
|
|
503
|
+
<div className={`${barClass} px-1.5`}>
|
|
504
|
+
<People peers={peers} clientId={clientId} me={me} onJumpTo={onJumpTo} />
|
|
505
|
+
</div>
|
|
506
|
+
)}
|
|
507
|
+
<ProfileMenu me={me} onRename={onRename} onRecolor={onRecolor} />
|
|
508
|
+
<button
|
|
509
|
+
onClick={onShare}
|
|
510
|
+
className="pointer-events-auto inline-flex h-[38px] items-center gap-2 rounded-[9px] bg-[rgb(var(--ec-page-text))] px-4 text-sm font-semibold text-[rgb(var(--ec-page-bg))] shadow-sm transition-colors hover:bg-[rgb(var(--ec-page-text)/0.85)]"
|
|
511
|
+
>
|
|
512
|
+
<Share2 size={16} aria-hidden />
|
|
513
|
+
Share
|
|
514
|
+
</button>
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
);
|
|
518
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { memo, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Check, ChevronDown } from 'lucide-react';
|
|
3
|
+
import { DIAGRAM_MENU, DIAGRAM_MENU_ITEM, DIAGRAM_MENU_SEPARATOR } from '@eventcatalog/visualiser';
|
|
4
|
+
import {
|
|
5
|
+
CANVAS_STATUSES,
|
|
6
|
+
type CanvasStatus,
|
|
7
|
+
type StatusChange,
|
|
8
|
+
timeAgo,
|
|
9
|
+
CANVAS_STATUS_LOOK,
|
|
10
|
+
} from '@eventcatalog/visualiser/studio';
|
|
11
|
+
|
|
12
|
+
/** Status changes shown in the menu, newest first */
|
|
13
|
+
const HISTORY_SHOWN = 8;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The canvas's status (draft, proposed, accepted, rejected) as a pill beside its title. It opens a menu to change it,
|
|
17
|
+
* with a note on why, and who changed it before.
|
|
18
|
+
*/
|
|
19
|
+
export default memo(function CanvasStatusMenu({
|
|
20
|
+
status,
|
|
21
|
+
history,
|
|
22
|
+
onChange,
|
|
23
|
+
}: {
|
|
24
|
+
status: CanvasStatus;
|
|
25
|
+
history: StatusChange[];
|
|
26
|
+
onChange: (status: CanvasStatus, note?: string) => void;
|
|
27
|
+
}) {
|
|
28
|
+
const [open, setOpen] = useState(false);
|
|
29
|
+
const [note, setNote] = useState('');
|
|
30
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
31
|
+
const look = CANVAS_STATUS_LOOK[status];
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!open) return;
|
|
35
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
36
|
+
if (!ref.current?.contains(event.target as globalThis.Node)) setOpen(false);
|
|
37
|
+
};
|
|
38
|
+
const onKeyDown = (event: KeyboardEvent) => event.key === 'Escape' && setOpen(false);
|
|
39
|
+
window.addEventListener('pointerdown', onPointerDown);
|
|
40
|
+
window.addEventListener('keydown', onKeyDown);
|
|
41
|
+
return () => {
|
|
42
|
+
window.removeEventListener('pointerdown', onPointerDown);
|
|
43
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
44
|
+
};
|
|
45
|
+
}, [open]);
|
|
46
|
+
|
|
47
|
+
const choose = (next: CanvasStatus) => {
|
|
48
|
+
if (next !== status) onChange(next, note);
|
|
49
|
+
setNote('');
|
|
50
|
+
setOpen(false);
|
|
51
|
+
};
|
|
52
|
+
const recent = history.slice(-HISTORY_SHOWN).reverse();
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div ref={ref} className="relative">
|
|
56
|
+
<button
|
|
57
|
+
onClick={() => setOpen((current) => !current)}
|
|
58
|
+
aria-label={`Status: ${look.label}`}
|
|
59
|
+
aria-expanded={open}
|
|
60
|
+
className={`flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-semibold ${look.pill}`}
|
|
61
|
+
>
|
|
62
|
+
{look.label}
|
|
63
|
+
<ChevronDown size={12} />
|
|
64
|
+
</button>
|
|
65
|
+
{open && (
|
|
66
|
+
<div className={`absolute left-0 top-full mt-2 w-72 ${DIAGRAM_MENU}`}>
|
|
67
|
+
<p className="px-3 pb-1 pt-1.5 text-[11px] font-semibold uppercase tracking-wide text-[rgb(var(--ec-page-text-muted))]">
|
|
68
|
+
Status
|
|
69
|
+
</p>
|
|
70
|
+
{CANVAS_STATUSES.map((value) => {
|
|
71
|
+
const option = CANVAS_STATUS_LOOK[value];
|
|
72
|
+
return (
|
|
73
|
+
<button key={value} onClick={() => choose(value)} className={`${DIAGRAM_MENU_ITEM} w-full text-left`}>
|
|
74
|
+
<span className={`h-2 w-2 shrink-0 rounded-full ${option.dot}`} />
|
|
75
|
+
<span className="min-w-0 flex-1">
|
|
76
|
+
<span className="block text-xs font-medium">{option.label}</span>
|
|
77
|
+
<span className="block text-[11px] text-[rgb(var(--ec-page-text-muted))]">{option.description}</span>
|
|
78
|
+
</span>
|
|
79
|
+
{value === status && <Check size={14} className="shrink-0 text-[rgb(var(--ec-accent))]" />}
|
|
80
|
+
</button>
|
|
81
|
+
);
|
|
82
|
+
})}
|
|
83
|
+
<div className="px-3 py-2">
|
|
84
|
+
<input
|
|
85
|
+
value={note}
|
|
86
|
+
onChange={(event) => setNote(event.target.value)}
|
|
87
|
+
placeholder="Add a note (optional), then pick a status"
|
|
88
|
+
aria-label="Note for the status change"
|
|
89
|
+
className="w-full rounded-md border px-2 py-1.5 text-xs bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))] text-[rgb(var(--ec-input-text))] placeholder:text-[rgb(var(--ec-page-text-muted))] focus:outline-none focus:border-[rgb(var(--ec-accent))]"
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
{recent.length > 0 && (
|
|
93
|
+
<>
|
|
94
|
+
<div className={DIAGRAM_MENU_SEPARATOR} />
|
|
95
|
+
<p className="px-3 pb-1 pt-1.5 text-[11px] font-semibold uppercase tracking-wide text-[rgb(var(--ec-page-text-muted))]">
|
|
96
|
+
History
|
|
97
|
+
</p>
|
|
98
|
+
<ul className="max-h-48 space-y-1.5 overflow-y-auto px-3 pb-2">
|
|
99
|
+
{recent.map((change) => (
|
|
100
|
+
<li key={`${change.at}-${change.status}`} className="text-[11px]">
|
|
101
|
+
<span className="flex items-center gap-1.5">
|
|
102
|
+
<span className={`h-1.5 w-1.5 shrink-0 rounded-full ${CANVAS_STATUS_LOOK[change.status].dot}`} />
|
|
103
|
+
<span className="font-medium">{CANVAS_STATUS_LOOK[change.status].label}</span>
|
|
104
|
+
<span className="text-[rgb(var(--ec-page-text-muted))]">
|
|
105
|
+
by {change.by.name} · {timeAgo(change.at)}
|
|
106
|
+
</span>
|
|
107
|
+
</span>
|
|
108
|
+
{change.note && <span className="block pl-3 text-[rgb(var(--ec-page-text-muted))]">{change.note}</span>}
|
|
109
|
+
</li>
|
|
110
|
+
))}
|
|
111
|
+
</ul>
|
|
112
|
+
</>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
});
|