@eventcatalog/core 5.0.0-beta.1 → 5.0.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +227 -128
- package/README.md +4 -4
- 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 +71 -35
- package/dist/analytics/log-build.js +5 -4
- package/dist/analytics/telemetry.cjs +42 -0
- package/dist/analytics/telemetry.d.cts +20 -0
- package/dist/analytics/telemetry.d.ts +20 -0
- package/dist/analytics/telemetry.js +8 -0
- package/dist/{chunk-KYJZJLBD.js → chunk-4YFM63DS.js} +9 -3
- package/dist/{chunk-UDS3SYLZ.js → chunk-76DAAJLV.js} +56 -33
- package/dist/{chunk-TQLRG35T.js → chunk-AWOO63ME.js} +71 -45
- package/dist/chunk-CPW6G62V.js +17 -0
- package/dist/{chunk-K2ACDO7Y.js → chunk-LQZ7IG7B.js} +1 -1
- package/dist/{chunk-S2QGT7ZF.js → chunk-MFEMKQZ6.js} +1 -1
- package/dist/{chunk-Q6SD5SNQ.js → chunk-WM4QYMIA.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +204 -124
- package/dist/eventcatalog.config.d.cts +59 -1
- package/dist/eventcatalog.config.d.ts +59 -1
- package/dist/eventcatalog.js +30 -11
- package/dist/federation/federate.js +2 -2
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
- package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
- package/dist/runtime-dependencies/manifest.json +8 -1
- package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
- package/dist/runtime-dependencies/yjs.mjs +3 -0
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/dist/utils/license-status.cjs +71 -45
- package/dist/utils/license-status.d.cts +14 -5
- package/dist/utils/license-status.d.ts +14 -5
- package/dist/utils/license-status.js +1 -1
- package/eventcatalog/astro.config.mjs +5 -0
- package/eventcatalog/integrations/storage.mjs +20 -0
- package/eventcatalog/integrations/studio-server.mjs +32 -0
- package/eventcatalog/src/components/Grids/utils.tsx +0 -23
- package/eventcatalog/src/components/Header.astro +3 -3
- package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +6 -1
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +12 -4
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +16 -7
- package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +10 -0
- package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +4 -62
- package/eventcatalog/src/components/Tables/Discover/columns.tsx +17 -150
- package/eventcatalog/src/components/Tables/Resources/ResourcesTable.tsx +425 -0
- package/eventcatalog/src/components/Tables/RowActionsMenu.tsx +157 -0
- package/eventcatalog/src/components/Tables/TablePagination.tsx +82 -0
- package/eventcatalog/src/features/ai/chat-api.ts +3 -3
- package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
- package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
- package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
- package/eventcatalog/src/features/mcp/apps/canvas/styles.css +22 -0
- package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +276 -0
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +210 -0
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +75 -74
- package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
- package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
- package/eventcatalog/src/features/mcp/mcp-server.ts +36 -5
- package/eventcatalog/src/features/server/start.ts +25 -0
- package/eventcatalog/src/features/storage/database.ts +88 -0
- package/eventcatalog/src/features/storage/migrations.ts +85 -0
- package/eventcatalog/src/features/storage/sql/database.sql +5 -0
- package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
- package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
- package/eventcatalog/src/features/storage/sql.ts +28 -0
- package/eventcatalog/src/features/studio/agent-choreography.ts +381 -0
- package/eventcatalog/src/features/studio/api/canvases-api.ts +170 -0
- package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
- package/eventcatalog/src/features/studio/api/client.ts +35 -0
- package/eventcatalog/src/features/studio/canvas-actions.ts +414 -0
- package/eventcatalog/src/features/studio/canvas-doc.ts +414 -0
- package/eventcatalog/src/features/studio/canvas-preview.ts +53 -0
- package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
- package/eventcatalog/src/features/studio/catalog.ts +171 -0
- package/eventcatalog/src/features/studio/clipboard.ts +97 -0
- package/eventcatalog/src/features/studio/components/CanvasControls.tsx +154 -0
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +370 -0
- package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +117 -0
- package/eventcatalog/src/features/studio/components/Comments.tsx +770 -0
- package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +281 -0
- package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +144 -0
- package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +145 -0
- package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +81 -0
- package/eventcatalog/src/features/studio/components/DragGhost.tsx +115 -0
- package/eventcatalog/src/features/studio/components/JoinForm.tsx +34 -0
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +314 -0
- package/eventcatalog/src/features/studio/components/Picture.tsx +21 -0
- package/eventcatalog/src/features/studio/components/Presence.tsx +286 -0
- package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +212 -0
- package/eventcatalog/src/features/studio/components/ShareDialog.tsx +260 -0
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +1216 -0
- package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
- package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +255 -0
- package/eventcatalog/src/features/studio/components/icons.ts +53 -0
- package/eventcatalog/src/features/studio/components/status.ts +53 -0
- package/eventcatalog/src/features/studio/edges.ts +56 -0
- package/eventcatalog/src/features/studio/flow-state.ts +92 -0
- package/eventcatalog/src/features/studio/grouping.ts +226 -0
- package/eventcatalog/src/features/studio/hooks/presence-sender.ts +46 -0
- package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +347 -0
- package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
- package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +104 -0
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +589 -0
- package/eventcatalog/src/features/studio/identity.ts +37 -0
- package/eventcatalog/src/features/studio/layout.ts +42 -0
- package/eventcatalog/src/features/studio/levels.ts +282 -0
- package/eventcatalog/src/features/studio/node-types.ts +205 -0
- package/eventcatalog/src/features/studio/pages/[id].astro +68 -0
- package/eventcatalog/src/features/studio/pages/index.astro +784 -0
- package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
- package/eventcatalog/src/features/studio/placement.ts +197 -0
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +510 -0
- package/eventcatalog/src/features/studio/server/runtime.ts +491 -0
- package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
- package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
- package/eventcatalog/src/features/studio/server/storage.ts +90 -0
- package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
- package/eventcatalog/src/features/studio/studio.css +273 -0
- package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +10 -5
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +52 -63
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +34 -46
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +11 -29
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_ResourcesPage.astro +79 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +32 -14
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +63 -58
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +2 -62
- package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +13 -0
- package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +24 -42
- package/eventcatalog/src/stores/sidebar-store/builders/flow.ts +2 -0
- package/eventcatalog/src/stores/sidebar-store/builders/message.ts +2 -0
- package/eventcatalog/src/stores/sidebar-store/builders/service.ts +23 -23
- package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +13 -0
- package/eventcatalog/src/stores/sidebar-store/builders/system.ts +18 -27
- package/eventcatalog/src/stores/sidebar-store/state.ts +5 -0
- package/eventcatalog/src/styles/tailwind.css +2 -2
- package/eventcatalog/src/utils/collections/channels.ts +56 -1
- package/eventcatalog/src/utils/collections/flows.ts +50 -1
- package/eventcatalog/src/utils/collections/message-connections.ts +60 -0
- package/eventcatalog/src/utils/collections/message-triggers.ts +2 -1
- package/eventcatalog/src/utils/collections/resource-owners.ts +193 -0
- package/eventcatalog/src/utils/collections/resources.ts +310 -0
- package/eventcatalog/src/utils/collections/util.ts +31 -0
- package/eventcatalog/src/utils/feature.ts +8 -0
- package/eventcatalog/src/utils/git.ts +11 -5
- package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
- package/eventcatalog/src/utils/trial.ts +1 -1
- package/package.json +17 -12
- package/dist/runtime-dependencies/shelljs.mjs +0 -3
- package/eventcatalog/public/studio-bg.png +0 -0
- package/eventcatalog/src/components/Grids/DomainGrid.tsx +0 -838
- package/eventcatalog/src/components/Grids/MessageGrid.tsx +0 -344
- package/eventcatalog/src/components/Grids/SystemGrid.tsx +0 -185
- package/eventcatalog/src/components/Grids/components.tsx +0 -170
- package/eventcatalog/src/components/Grids/message-link.ts +0 -38
- package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
- package/eventcatalog/src/components/Tables/SystemResources/SystemResourcesTable.tsx +0 -294
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.spec.ts +0 -65
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.ts +0 -46
- package/eventcatalog/src/pages/studio.astro +0 -316
- package/dist/{chunk-AYNZXUB5.js → chunk-KHACCTW2.js} +3 -3
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type DragEvent } from 'react';
|
|
2
|
+
import { ReactFlow, ReactFlowProvider, useStoreApi, type Node, type XYPosition } from '@xyflow/react';
|
|
3
|
+
import { getNodeSize, isGroupType } from '../node-types';
|
|
4
|
+
import { nodeTypes } from './canvas-nodes';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Dragging something from the left panel onto the canvas shows the node it becomes, under the pointer, at the
|
|
8
|
+
* canvas's zoom and where it will land (centred on the pointer, as it's dropped). Drawn here rather than as the
|
|
9
|
+
* browser's drag image, which browsers shrink, fade and put a backing behind.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** What's dragged: the node it becomes */
|
|
13
|
+
export type DragPreview = { type: string; data: Record<string, unknown> };
|
|
14
|
+
/** Starts dragging `value` as `dragType`, showing `preview` as what's dragged */
|
|
15
|
+
export type StartDrag = (dragType: string, value: string, preview: DragPreview) => (event: DragEvent) => void;
|
|
16
|
+
|
|
17
|
+
// Hides the browser's own drag image (a transparent pixel)
|
|
18
|
+
const NO_DRAG_IMAGE =
|
|
19
|
+
typeof Image === 'undefined'
|
|
20
|
+
? undefined
|
|
21
|
+
: Object.assign(new Image(), { src: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' });
|
|
22
|
+
|
|
23
|
+
// Room around the node for the badge on its top edge
|
|
24
|
+
const MARGIN = 16;
|
|
25
|
+
const PRO_OPTIONS = { hideAttribution: true };
|
|
26
|
+
|
|
27
|
+
type Ghost = { preview: DragPreview; zoom: number; at: XYPosition };
|
|
28
|
+
|
|
29
|
+
/** The ghost of what's being dragged, and how to start dragging something with one */
|
|
30
|
+
export function useDragGhost() {
|
|
31
|
+
const store = useStoreApi();
|
|
32
|
+
const [ghost, setGhost] = useState<Ghost | null>(null);
|
|
33
|
+
|
|
34
|
+
const startDrag = useCallback<StartDrag>(
|
|
35
|
+
(dragType, value, preview) => (event) => {
|
|
36
|
+
event.dataTransfer.setData(dragType, value);
|
|
37
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
38
|
+
if (NO_DRAG_IMAGE) event.dataTransfer.setDragImage(NO_DRAG_IMAGE, 0, 0);
|
|
39
|
+
setGhost({ preview, zoom: store.getState().transform[2], at: { x: event.clientX, y: event.clientY } });
|
|
40
|
+
},
|
|
41
|
+
[store]
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const element = ghost && <DragGhost ghost={ghost} onEnd={() => setGhost(null)} />;
|
|
45
|
+
return { startDrag, ghost: element };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function DragGhost({ ghost, onEnd }: { ghost: Ghost; onEnd: () => void }) {
|
|
49
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
50
|
+
const { preview, zoom } = ghost;
|
|
51
|
+
const container = isGroupType(preview.type);
|
|
52
|
+
// The size it's added at (containers are sized; cards are drawn at their own size inside this)
|
|
53
|
+
const size = getNodeSize(preview.type);
|
|
54
|
+
// The node is placed with its middle where it's dropped, at the size it's added at
|
|
55
|
+
const offset = { x: (size.width / 2) * zoom + MARGIN, y: (size.height / 2) * zoom + MARGIN };
|
|
56
|
+
|
|
57
|
+
// Follows the pointer (as the browser reports it while dragging) without re-rendering, and goes when it's dropped
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const move = (x: number, y: number) => {
|
|
60
|
+
if (ref.current) ref.current.style.transform = `translate(${x - offset.x}px, ${y - offset.y}px)`;
|
|
61
|
+
};
|
|
62
|
+
move(ghost.at.x, ghost.at.y);
|
|
63
|
+
const onDragOver = (event: globalThis.DragEvent) => {
|
|
64
|
+
if (event.clientX || event.clientY) move(event.clientX, event.clientY);
|
|
65
|
+
};
|
|
66
|
+
document.addEventListener('dragover', onDragOver);
|
|
67
|
+
document.addEventListener('drop', onEnd);
|
|
68
|
+
document.addEventListener('dragend', onEnd);
|
|
69
|
+
return () => {
|
|
70
|
+
document.removeEventListener('dragover', onDragOver);
|
|
71
|
+
document.removeEventListener('drop', onEnd);
|
|
72
|
+
document.removeEventListener('dragend', onEnd);
|
|
73
|
+
};
|
|
74
|
+
}, [ghost]);
|
|
75
|
+
|
|
76
|
+
const nodes = useMemo<Node[]>(
|
|
77
|
+
() => [
|
|
78
|
+
{
|
|
79
|
+
id: 'drag-ghost',
|
|
80
|
+
type: preview.type,
|
|
81
|
+
position: { x: 0, y: 0 },
|
|
82
|
+
data: preview.data,
|
|
83
|
+
...(container && size),
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
[preview, container, size]
|
|
87
|
+
);
|
|
88
|
+
const viewport = useMemo(() => ({ x: MARGIN, y: MARGIN, zoom }), [zoom]);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
ref={ref}
|
|
93
|
+
aria-hidden
|
|
94
|
+
// Sized here: the visualiser's styles make its class fill the window
|
|
95
|
+
className="studio-drag-preview eventcatalog-visualizer pointer-events-none fixed left-0 top-0 z-[100]"
|
|
96
|
+
style={{ width: size.width * zoom + MARGIN * 2, height: size.height * zoom + MARGIN * 2 }}
|
|
97
|
+
>
|
|
98
|
+
<ReactFlowProvider>
|
|
99
|
+
<ReactFlow
|
|
100
|
+
nodes={nodes}
|
|
101
|
+
nodeTypes={nodeTypes}
|
|
102
|
+
defaultViewport={viewport}
|
|
103
|
+
proOptions={PRO_OPTIONS}
|
|
104
|
+
nodesDraggable={false}
|
|
105
|
+
nodesConnectable={false}
|
|
106
|
+
elementsSelectable={false}
|
|
107
|
+
panOnDrag={false}
|
|
108
|
+
zoomOnScroll={false}
|
|
109
|
+
zoomOnPinch={false}
|
|
110
|
+
preventScrolling={false}
|
|
111
|
+
/>
|
|
112
|
+
</ReactFlowProvider>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export default function JoinForm({ onJoin }: { onJoin: (name: string) => void }) {
|
|
4
|
+
const [draft, setDraft] = useState('');
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
<form
|
|
8
|
+
onSubmit={(e) => {
|
|
9
|
+
e.preventDefault();
|
|
10
|
+
if (draft.trim()) onJoin(draft.trim());
|
|
11
|
+
}}
|
|
12
|
+
className="mx-auto mt-20 max-w-sm space-y-4 rounded-lg border p-6 bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))]"
|
|
13
|
+
>
|
|
14
|
+
<div>
|
|
15
|
+
<h1 className="text-lg font-semibold">Join this canvas</h1>
|
|
16
|
+
<p className="text-sm text-[rgb(var(--ec-page-text-muted))]">What should others call you?</p>
|
|
17
|
+
</div>
|
|
18
|
+
<input
|
|
19
|
+
autoFocus
|
|
20
|
+
value={draft}
|
|
21
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
22
|
+
placeholder="Your name"
|
|
23
|
+
className="w-full rounded-md px-3 py-2 border bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))] text-[rgb(var(--ec-input-text))]"
|
|
24
|
+
/>
|
|
25
|
+
<button
|
|
26
|
+
type="submit"
|
|
27
|
+
disabled={!draft.trim()}
|
|
28
|
+
className="w-full rounded-md px-3 py-2 text-sm disabled:opacity-50 bg-[rgb(var(--ec-button-bg))] text-[rgb(var(--ec-button-text))]"
|
|
29
|
+
>
|
|
30
|
+
Join
|
|
31
|
+
</button>
|
|
32
|
+
</form>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { memo, useCallback, useMemo, useState, type ReactNode } from 'react';
|
|
2
|
+
import { Check, ChevronRight, Plus, Search } from 'lucide-react';
|
|
3
|
+
import type { CatalogResource } from '../catalog-resources';
|
|
4
|
+
import { catalogNodeData } from '../catalog';
|
|
5
|
+
import { NODE_CATEGORIES, nodeDefinitions } from '../node-types';
|
|
6
|
+
import type { Thread } from '../hooks/use-comments';
|
|
7
|
+
import { CommentList } from './Comments';
|
|
8
|
+
import { useDragGhost, type StartDrag } from './DragGhost';
|
|
9
|
+
import { CATALOG_GROUPS, NODE_ICONS } from './icons';
|
|
10
|
+
import { STATUS } from './status';
|
|
11
|
+
|
|
12
|
+
/** What's dragged from the left panel onto the canvas: a component's type, or a catalog resource's key */
|
|
13
|
+
export const COMPONENT_DRAG_TYPE = 'application/eventcatalog-studio';
|
|
14
|
+
export const CATALOG_DRAG_TYPE = 'application/eventcatalog-studio-resource';
|
|
15
|
+
|
|
16
|
+
/** A catalog group shows this many resources until it's expanded, so big catalogs stay quick */
|
|
17
|
+
const GROUP_PREVIEW_SIZE = 50;
|
|
18
|
+
|
|
19
|
+
const itemClass =
|
|
20
|
+
'group flex cursor-grab items-center justify-between gap-2 rounded-md border px-2.5 py-2 bg-[rgb(var(--ec-page-bg))] border-[rgb(var(--ec-page-border))] hover:border-[rgb(var(--ec-accent))]';
|
|
21
|
+
const addButtonClass =
|
|
22
|
+
'shrink-0 rounded p-0.5 opacity-0 group-hover:opacity-100 text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-accent))]';
|
|
23
|
+
// The sections that are collapsed, by id (e.g. "components:messages"), remembered in this browser
|
|
24
|
+
const COLLAPSED_KEY = 'eventcatalog-studio-collapsed';
|
|
25
|
+
type Collapsed = { collapsed: ReadonlySet<string>; toggle: (id: string) => void };
|
|
26
|
+
|
|
27
|
+
const readCollapsed = (): Set<string> => {
|
|
28
|
+
try {
|
|
29
|
+
return new Set(JSON.parse(localStorage.getItem(COLLAPSED_KEY) ?? '[]') as string[]);
|
|
30
|
+
} catch {
|
|
31
|
+
return new Set();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function useCollapsed(): Collapsed {
|
|
36
|
+
const [collapsed, setCollapsed] = useState(readCollapsed);
|
|
37
|
+
const toggle = useCallback((id: string) => {
|
|
38
|
+
setCollapsed((current) => {
|
|
39
|
+
const next = new Set(current);
|
|
40
|
+
if (next.has(id)) next.delete(id);
|
|
41
|
+
else next.add(id);
|
|
42
|
+
try {
|
|
43
|
+
localStorage.setItem(COLLAPSED_KEY, JSON.stringify([...next]));
|
|
44
|
+
} catch {
|
|
45
|
+
// Not remembered (e.g. storage is blocked): it still works for now
|
|
46
|
+
}
|
|
47
|
+
return next;
|
|
48
|
+
});
|
|
49
|
+
}, []);
|
|
50
|
+
return useMemo(() => ({ collapsed, toggle }), [collapsed, toggle]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A section's heading, which opens and closes it */
|
|
54
|
+
function SectionHeading({ open, onToggle, children }: { open: boolean; onToggle: () => void; children: ReactNode }) {
|
|
55
|
+
return (
|
|
56
|
+
<h3>
|
|
57
|
+
<button
|
|
58
|
+
onClick={onToggle}
|
|
59
|
+
aria-expanded={open}
|
|
60
|
+
className="flex w-full cursor-pointer items-center gap-1 rounded-md px-1.5 py-1 text-left text-[11px] font-semibold uppercase tracking-wide text-[rgb(var(--ec-page-text-muted))] transition-colors hover:bg-[rgb(var(--ec-page-border)/0.6)] hover:text-[rgb(var(--ec-page-text))]"
|
|
61
|
+
>
|
|
62
|
+
<ChevronRight size={12} className={`shrink-0 transition-transform ${open ? 'rotate-90' : ''}`} />
|
|
63
|
+
{children}
|
|
64
|
+
</button>
|
|
65
|
+
</h3>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type Tab = 'components' | 'catalog' | 'comments';
|
|
70
|
+
|
|
71
|
+
/** Components and catalog resources to drag onto the canvas, and the comments on it */
|
|
72
|
+
export default memo(function LeftPanel({
|
|
73
|
+
resources,
|
|
74
|
+
keysOnCanvas,
|
|
75
|
+
onAddComponent,
|
|
76
|
+
onAddResource,
|
|
77
|
+
threads,
|
|
78
|
+
showResolved,
|
|
79
|
+
onToggleResolved,
|
|
80
|
+
onOpenThread,
|
|
81
|
+
}: {
|
|
82
|
+
resources: CatalogResource[];
|
|
83
|
+
/** The catalog resources on the canvas */
|
|
84
|
+
keysOnCanvas: Set<string>;
|
|
85
|
+
onAddComponent: (type: string) => void;
|
|
86
|
+
onAddResource: (key: string) => void;
|
|
87
|
+
threads: Thread[];
|
|
88
|
+
showResolved: boolean;
|
|
89
|
+
onToggleResolved: () => void;
|
|
90
|
+
onOpenThread: (thread: Thread) => void;
|
|
91
|
+
}) {
|
|
92
|
+
const [tab, setTab] = useState<Tab>('components');
|
|
93
|
+
// What's being dragged onto the canvas, drawn as the node it becomes (see DragGhost)
|
|
94
|
+
const { startDrag, ghost } = useDragGhost();
|
|
95
|
+
const sections = useCollapsed();
|
|
96
|
+
const openThreads = threads.filter((thread) => !thread.resolved).length;
|
|
97
|
+
const tabClass = (active: boolean) =>
|
|
98
|
+
`flex flex-1 items-center justify-center gap-1 whitespace-nowrap rounded-md px-1.5 py-1 text-xs font-medium ${
|
|
99
|
+
active ? 'bg-[rgb(var(--ec-page-bg))] shadow-sm' : 'text-[rgb(var(--ec-page-text-muted))]'
|
|
100
|
+
}`;
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<aside className="flex w-72 shrink-0 flex-col border-r bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))]">
|
|
104
|
+
<div className="m-3 flex gap-1 rounded-lg p-1 bg-[rgb(var(--ec-page-border)/0.5)]">
|
|
105
|
+
<button className={tabClass(tab === 'components')} onClick={() => setTab('components')}>
|
|
106
|
+
Components
|
|
107
|
+
</button>
|
|
108
|
+
<button className={tabClass(tab === 'catalog')} onClick={() => setTab('catalog')}>
|
|
109
|
+
Catalog
|
|
110
|
+
</button>
|
|
111
|
+
<button className={tabClass(tab === 'comments')} onClick={() => setTab('comments')}>
|
|
112
|
+
Comments
|
|
113
|
+
{openThreads > 0 && (
|
|
114
|
+
<span className="rounded-full px-1.5 text-[10px] leading-4 bg-[rgb(var(--ec-button-bg))] text-[rgb(var(--ec-button-text))]">
|
|
115
|
+
{openThreads}
|
|
116
|
+
</span>
|
|
117
|
+
)}
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
{tab === 'components' && <ComponentList onAdd={onAddComponent} startDrag={startDrag} sections={sections} />}
|
|
121
|
+
{tab === 'catalog' && (
|
|
122
|
+
<CatalogList
|
|
123
|
+
resources={resources}
|
|
124
|
+
keysOnCanvas={keysOnCanvas}
|
|
125
|
+
onAdd={onAddResource}
|
|
126
|
+
startDrag={startDrag}
|
|
127
|
+
sections={sections}
|
|
128
|
+
/>
|
|
129
|
+
)}
|
|
130
|
+
{tab === 'comments' && (
|
|
131
|
+
<CommentList threads={threads} showResolved={showResolved} onToggleResolved={onToggleResolved} onOpen={onOpenThread} />
|
|
132
|
+
)}
|
|
133
|
+
{ghost}
|
|
134
|
+
</aside>
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const listedDefinitions = NODE_CATEGORIES.map((category) => ({
|
|
139
|
+
...category,
|
|
140
|
+
definitions: nodeDefinitions.filter((definition) => definition.category === category.id && !definition.unlisted),
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
// Each component's data, made once, for the node shown while it's dragged
|
|
144
|
+
const componentPreviews = new Map(
|
|
145
|
+
nodeDefinitions.map((definition) => [definition.type, { type: definition.type, data: definition.createData() }])
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const ComponentList = memo(function ComponentList({
|
|
149
|
+
onAdd,
|
|
150
|
+
startDrag,
|
|
151
|
+
sections: { collapsed, toggle },
|
|
152
|
+
}: {
|
|
153
|
+
onAdd: (type: string) => void;
|
|
154
|
+
startDrag: StartDrag;
|
|
155
|
+
sections: Collapsed;
|
|
156
|
+
}) {
|
|
157
|
+
return (
|
|
158
|
+
<div className="min-h-0 flex-1 space-y-3 overflow-y-auto px-3 pb-3">
|
|
159
|
+
{listedDefinitions.map((category) => {
|
|
160
|
+
const id = `components:${category.id}`;
|
|
161
|
+
const open = !collapsed.has(id);
|
|
162
|
+
return (
|
|
163
|
+
<section key={category.id} className="space-y-1.5">
|
|
164
|
+
<SectionHeading open={open} onToggle={() => toggle(id)}>
|
|
165
|
+
{category.label}
|
|
166
|
+
</SectionHeading>
|
|
167
|
+
{open &&
|
|
168
|
+
category.definitions.map(({ type, label }) => {
|
|
169
|
+
const { icon: Icon, className } = NODE_ICONS[type];
|
|
170
|
+
return (
|
|
171
|
+
<div
|
|
172
|
+
key={type}
|
|
173
|
+
draggable
|
|
174
|
+
onDragStart={startDrag(COMPONENT_DRAG_TYPE, type, componentPreviews.get(type)!)}
|
|
175
|
+
className={itemClass}
|
|
176
|
+
>
|
|
177
|
+
<span className="flex items-center gap-2 text-xs font-medium">
|
|
178
|
+
<Icon size={16} className={className} />
|
|
179
|
+
{label}
|
|
180
|
+
</span>
|
|
181
|
+
<button onClick={() => onAdd(type)} title={`Add ${label}`} className={addButtonClass}>
|
|
182
|
+
<Plus size={14} />
|
|
183
|
+
</button>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
})}
|
|
187
|
+
</section>
|
|
188
|
+
);
|
|
189
|
+
})}
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const CatalogList = memo(function CatalogList({
|
|
195
|
+
resources,
|
|
196
|
+
keysOnCanvas,
|
|
197
|
+
onAdd,
|
|
198
|
+
startDrag,
|
|
199
|
+
sections: { collapsed, toggle },
|
|
200
|
+
}: {
|
|
201
|
+
resources: CatalogResource[];
|
|
202
|
+
keysOnCanvas: Set<string>;
|
|
203
|
+
onAdd: (key: string) => void;
|
|
204
|
+
startDrag: StartDrag;
|
|
205
|
+
sections: Collapsed;
|
|
206
|
+
}) {
|
|
207
|
+
const [search, setSearch] = useState('');
|
|
208
|
+
const [expanded, setExpanded] = useState<Set<string>>(() => new Set());
|
|
209
|
+
const searching = search.trim() !== '';
|
|
210
|
+
const groups = useMemo(() => {
|
|
211
|
+
const term = search.trim().toLowerCase();
|
|
212
|
+
const matches = term
|
|
213
|
+
? resources.filter((resource) => resource.name.toLowerCase().includes(term) || resource.id.toLowerCase().includes(term))
|
|
214
|
+
: resources;
|
|
215
|
+
return CATALOG_GROUPS.map((group) => ({
|
|
216
|
+
...group,
|
|
217
|
+
items: matches.filter((resource) => resource.collection === group.collection),
|
|
218
|
+
})).filter((group) => group.items.length > 0);
|
|
219
|
+
}, [resources, search]);
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
223
|
+
<div className="relative mx-3 mb-2">
|
|
224
|
+
<Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[rgb(var(--ec-icon-color))]" />
|
|
225
|
+
<input
|
|
226
|
+
value={search}
|
|
227
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
228
|
+
placeholder="Search the catalog"
|
|
229
|
+
className="w-full rounded-md border py-1.5 pl-8 pr-2 text-xs bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))] text-[rgb(var(--ec-input-text))]"
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
<div className="min-h-0 flex-1 space-y-3 overflow-y-auto px-3 pb-3">
|
|
233
|
+
{groups.length === 0 && (
|
|
234
|
+
<p className="py-6 text-center text-xs text-[rgb(var(--ec-page-text-muted))]">No resources found.</p>
|
|
235
|
+
)}
|
|
236
|
+
{groups.map(({ collection, label, icon, className, items }) => {
|
|
237
|
+
const id = `catalog:${collection}`;
|
|
238
|
+
// While searching, every group with matches shows them
|
|
239
|
+
const open = searching || !collapsed.has(id);
|
|
240
|
+
const shown = !open ? [] : expanded.has(collection) ? items : items.slice(0, GROUP_PREVIEW_SIZE);
|
|
241
|
+
return (
|
|
242
|
+
<section key={collection} className="space-y-1.5">
|
|
243
|
+
<SectionHeading open={open} onToggle={() => toggle(id)}>
|
|
244
|
+
{label} <span className="font-normal">({items.length})</span>
|
|
245
|
+
</SectionHeading>
|
|
246
|
+
{shown.map((resource) => (
|
|
247
|
+
<CatalogItem
|
|
248
|
+
key={resource.key}
|
|
249
|
+
resource={resource}
|
|
250
|
+
icon={icon}
|
|
251
|
+
iconClassName={className}
|
|
252
|
+
onCanvas={keysOnCanvas.has(resource.key)}
|
|
253
|
+
onAdd={onAdd}
|
|
254
|
+
startDrag={startDrag}
|
|
255
|
+
/>
|
|
256
|
+
))}
|
|
257
|
+
{open && shown.length < items.length && (
|
|
258
|
+
<button
|
|
259
|
+
onClick={() => setExpanded((current) => new Set(current).add(collection))}
|
|
260
|
+
className="w-full rounded-md px-2 py-1.5 text-xs font-medium text-[rgb(var(--ec-accent))] hover:bg-[rgb(var(--ec-accent-subtle))]"
|
|
261
|
+
>
|
|
262
|
+
Show all {items.length} {label.toLowerCase()}
|
|
263
|
+
</button>
|
|
264
|
+
)}
|
|
265
|
+
</section>
|
|
266
|
+
);
|
|
267
|
+
})}
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const CatalogItem = memo(function CatalogItem({
|
|
274
|
+
resource,
|
|
275
|
+
icon: Icon,
|
|
276
|
+
iconClassName,
|
|
277
|
+
onCanvas,
|
|
278
|
+
onAdd,
|
|
279
|
+
startDrag,
|
|
280
|
+
}: {
|
|
281
|
+
resource: CatalogResource;
|
|
282
|
+
icon: (typeof CATALOG_GROUPS)[number]['icon'];
|
|
283
|
+
iconClassName: string;
|
|
284
|
+
onCanvas: boolean;
|
|
285
|
+
onAdd: (key: string) => void;
|
|
286
|
+
startDrag: StartDrag;
|
|
287
|
+
}) {
|
|
288
|
+
// Drawn like the card it becomes (domains and systems can become containers too, but start as cards)
|
|
289
|
+
const preview = useMemo(() => ({ type: resource.node.type, data: catalogNodeData(resource) }), [resource]);
|
|
290
|
+
return (
|
|
291
|
+
<div
|
|
292
|
+
draggable
|
|
293
|
+
onDragStart={startDrag(CATALOG_DRAG_TYPE, resource.key, preview)}
|
|
294
|
+
title={resource.summary}
|
|
295
|
+
className={itemClass}
|
|
296
|
+
>
|
|
297
|
+
<span className="flex min-w-0 items-center gap-2 text-xs font-medium">
|
|
298
|
+
<Icon size={16} className={`shrink-0 ${iconClassName}`} />
|
|
299
|
+
<span className="truncate">{resource.name}</span>
|
|
300
|
+
<span className="shrink-0 text-[10px] text-[rgb(var(--ec-page-text-muted))]">v{resource.version}</span>
|
|
301
|
+
</span>
|
|
302
|
+
<span className="flex shrink-0 items-center gap-1">
|
|
303
|
+
{onCanvas && (
|
|
304
|
+
<span title="On the canvas">
|
|
305
|
+
<Check size={14} className={STATUS.success.text} />
|
|
306
|
+
</span>
|
|
307
|
+
)}
|
|
308
|
+
<button onClick={() => onAdd(resource.key)} title={`Add ${resource.name}`} className={addButtonClass}>
|
|
309
|
+
<Plus size={14} />
|
|
310
|
+
</button>
|
|
311
|
+
</span>
|
|
312
|
+
</div>
|
|
313
|
+
);
|
|
314
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Someone's picture from their sign-in provider (SSO), loaded from there. By default it covers their avatar's
|
|
5
|
+
* initials, which show again if it doesn't load (blocked, expired, or no longer there).
|
|
6
|
+
*/
|
|
7
|
+
export default function Picture({ src, className = 'absolute inset-0 h-full w-full' }: { src?: string; className?: string }) {
|
|
8
|
+
const [failed, setFailed] = useState<string>();
|
|
9
|
+
if (!src || failed === src) return null;
|
|
10
|
+
return (
|
|
11
|
+
<img
|
|
12
|
+
src={src}
|
|
13
|
+
alt=""
|
|
14
|
+
draggable={false}
|
|
15
|
+
// Some providers (e.g. Google) refuse pictures loaded with a referrer from another site
|
|
16
|
+
referrerPolicy="no-referrer"
|
|
17
|
+
onError={() => setFailed(src)}
|
|
18
|
+
className={`rounded-full object-cover ${className}`}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|