@eventcatalog/core 5.0.0-beta.1 → 5.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +227 -128
- package/dist/analytics/analytics.cjs +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-K2ACDO7Y.js → chunk-2LBDRHE4.js} +1 -1
- package/dist/{chunk-TQLRG35T.js → chunk-AWOO63ME.js} +71 -45
- package/dist/chunk-CPW6G62V.js +17 -0
- package/dist/{chunk-UDS3SYLZ.js → chunk-CRKJBDN7.js} +56 -33
- package/dist/{chunk-Q6SD5SNQ.js → chunk-EM36OL6A.js} +1 -1
- package/dist/{chunk-S2QGT7ZF.js → chunk-IONYKKOY.js} +1 -1
- package/dist/{chunk-KYJZJLBD.js → chunk-LJMXOEEC.js} +9 -3
- 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,286 @@
|
|
|
1
|
+
import { Fragment, memo, useEffect, useLayoutEffect, useRef, useSyncExternalStore, type MutableRefObject } from 'react';
|
|
2
|
+
import { useInternalNode, useStore, useStoreApi, ViewportPortal, type XYPosition } from '@xyflow/react';
|
|
3
|
+
import { Bot, Eye, EyeOff } from 'lucide-react';
|
|
4
|
+
import { sizeOf } from '../grouping';
|
|
5
|
+
import { usePeople, type Peer, type PresenceStore } from '../hooks/presence-store';
|
|
6
|
+
import { useFollowCamera } from '../hooks/use-follow-camera';
|
|
7
|
+
import Picture from './Picture';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Other people on the canvas: their pointers, selections and the connections they're dragging. Drawn in the
|
|
11
|
+
* canvas's own coordinates (React Flow's viewport portal), so panning and zooming move them without
|
|
12
|
+
* re-rendering, and counter-scaled so they stay the same size on screen. Each person's pointer and marks follow
|
|
13
|
+
* them on their own, so one pointer moving doesn't re-render anyone else's.
|
|
14
|
+
*/
|
|
15
|
+
export const RemotePresence = memo(function RemotePresence({
|
|
16
|
+
store,
|
|
17
|
+
clientId,
|
|
18
|
+
}: {
|
|
19
|
+
store: PresenceStore | null;
|
|
20
|
+
clientId: number | null;
|
|
21
|
+
}) {
|
|
22
|
+
const people = usePeople(store);
|
|
23
|
+
const zoom = useStore((state) => state.transform[2]);
|
|
24
|
+
const scale = 1 / zoom;
|
|
25
|
+
const others = store ? people.filter((peer) => peer.clientId !== clientId) : [];
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<ViewportPortal>
|
|
29
|
+
{/* Above nodes (selected ones are raised to 1000) */}
|
|
30
|
+
<div className="pointer-events-none absolute left-0 top-0" style={{ zIndex: 2000 }}>
|
|
31
|
+
{others.map((peer) => (
|
|
32
|
+
<Fragment key={peer.clientId}>
|
|
33
|
+
<PeerMarks store={store!} clientId={peer.clientId} scale={scale} />
|
|
34
|
+
{peer.pointer && <PeerPointer store={store!} peer={peer} scale={scale} />}
|
|
35
|
+
</Fragment>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
</ViewportPortal>
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const findPeer = (store: PresenceStore, clientId: number) => store.getPeers().find((peer) => peer.clientId === clientId);
|
|
43
|
+
|
|
44
|
+
/** What a peer's marks show: re-rendered only when it changes */
|
|
45
|
+
const marksKey = (peer: Peer | undefined) =>
|
|
46
|
+
peer
|
|
47
|
+
? [
|
|
48
|
+
peer.selection.join(','),
|
|
49
|
+
peer.connecting && peer.pointer ? [peer.connecting.x, peer.connecting.y, peer.pointer.x, peer.pointer.y].join(',') : '',
|
|
50
|
+
].join('|')
|
|
51
|
+
: '';
|
|
52
|
+
|
|
53
|
+
/** Someone's selection, and the connection they're dragging (a dashed line from where it starts to their pointer) */
|
|
54
|
+
const PeerMarks = memo(function PeerMarks({ store, clientId, scale }: { store: PresenceStore; clientId: number; scale: number }) {
|
|
55
|
+
useSyncExternalStore(store.subscribe, () => marksKey(findPeer(store, clientId)));
|
|
56
|
+
const peer = findPeer(store, clientId);
|
|
57
|
+
if (!peer) return null;
|
|
58
|
+
return (
|
|
59
|
+
<>
|
|
60
|
+
{peer.selection.map((id) => (
|
|
61
|
+
<SelectionOutline key={id} nodeId={id} name={peer.name} color={peer.color} scale={scale} />
|
|
62
|
+
))}
|
|
63
|
+
{peer.pointer && peer.connecting && (
|
|
64
|
+
<svg className="absolute left-0 top-0 overflow-visible" width={1} height={1}>
|
|
65
|
+
<line
|
|
66
|
+
x1={peer.connecting.x}
|
|
67
|
+
y1={peer.connecting.y}
|
|
68
|
+
x2={peer.pointer.x}
|
|
69
|
+
y2={peer.pointer.y}
|
|
70
|
+
stroke={peer.color}
|
|
71
|
+
strokeWidth={2 * scale}
|
|
72
|
+
strokeDasharray={`${6 * scale} ${4 * scale}`}
|
|
73
|
+
/>
|
|
74
|
+
<circle cx={peer.connecting.x} cy={peer.connecting.y} r={4 * scale} fill={peer.color} />
|
|
75
|
+
</svg>
|
|
76
|
+
)}
|
|
77
|
+
</>
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/** Longest a pointer takes to glide to where it was last seen (updates come about every 33 ms) */
|
|
82
|
+
const MAX_GLIDE_MS = 120;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Someone's pointer, gliding from where it was to each new position over the time since the last one (so a
|
|
86
|
+
* steady stream of updates looks like steady movement). Moved with a transform outside React: no re-renders.
|
|
87
|
+
*/
|
|
88
|
+
const PeerPointer = memo(function PeerPointer({ store, peer, scale }: { store: PresenceStore; peer: Peer; scale: number }) {
|
|
89
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
90
|
+
const shown = useRef<XYPosition | null>(null);
|
|
91
|
+
const scaleRef = useRef(scale);
|
|
92
|
+
scaleRef.current = scale;
|
|
93
|
+
|
|
94
|
+
const draw = () => {
|
|
95
|
+
if (ref.current && shown.current)
|
|
96
|
+
ref.current.style.transform = `translate(${shown.current.x}px, ${shown.current.y}px) scale(${scaleRef.current})`;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
useLayoutEffect(draw, [scale]);
|
|
100
|
+
|
|
101
|
+
useLayoutEffect(() => {
|
|
102
|
+
let target: XYPosition | null = null;
|
|
103
|
+
let from: XYPosition = { x: 0, y: 0 };
|
|
104
|
+
let start = 0;
|
|
105
|
+
let duration = 0;
|
|
106
|
+
let lastSeen = 0;
|
|
107
|
+
let frame = 0;
|
|
108
|
+
|
|
109
|
+
const step = (now: number) => {
|
|
110
|
+
const t = Math.min(1, (now - start) / duration);
|
|
111
|
+
if (target) shown.current = { x: from.x + (target.x - from.x) * t, y: from.y + (target.y - from.y) * t };
|
|
112
|
+
draw();
|
|
113
|
+
frame = t < 1 ? requestAnimationFrame(step) : 0;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const follow = () => {
|
|
117
|
+
const pointer = findPeer(store, peer.clientId)?.pointer;
|
|
118
|
+
if (!pointer || (target && pointer.x === target.x && pointer.y === target.y)) return;
|
|
119
|
+
const now = performance.now();
|
|
120
|
+
target = pointer;
|
|
121
|
+
if (!shown.current) {
|
|
122
|
+
shown.current = pointer;
|
|
123
|
+
lastSeen = now;
|
|
124
|
+
return draw();
|
|
125
|
+
}
|
|
126
|
+
from = shown.current;
|
|
127
|
+
start = now;
|
|
128
|
+
duration = Math.min(MAX_GLIDE_MS, Math.max(16, now - lastSeen));
|
|
129
|
+
lastSeen = now;
|
|
130
|
+
if (!frame) frame = requestAnimationFrame(step);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
follow();
|
|
134
|
+
const unsubscribe = store.subscribe(follow);
|
|
135
|
+
return () => {
|
|
136
|
+
unsubscribe();
|
|
137
|
+
cancelAnimationFrame(frame);
|
|
138
|
+
};
|
|
139
|
+
}, [store, peer.clientId]);
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div ref={ref} className="absolute left-0 top-0 will-change-transform" style={{ transformOrigin: '0 0' }}>
|
|
143
|
+
{/* The pointer's outline is a light border on any background */}
|
|
144
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill={peer.color} stroke="white" strokeWidth="1.5">
|
|
145
|
+
<path d="M4 2l16 9-7 2-3 7z" />
|
|
146
|
+
</svg>
|
|
147
|
+
<span
|
|
148
|
+
className="absolute left-4 top-4 flex items-center gap-1 whitespace-nowrap rounded px-1.5 py-0.5 text-[11px] font-medium text-white"
|
|
149
|
+
style={{ backgroundColor: peer.color }}
|
|
150
|
+
>
|
|
151
|
+
{peer.agent && <Bot size={12} />}
|
|
152
|
+
<Picture src={peer.picture} className="-ml-0.5 h-4 w-4 shrink-0" />
|
|
153
|
+
{peer.name}
|
|
154
|
+
{peer.activity && <span className="font-normal opacity-90">· {peer.activity}</span>}
|
|
155
|
+
</span>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
/** Someone's selection around a node: follows that node only, so other nodes moving don't re-render it */
|
|
161
|
+
const SelectionOutline = memo(function SelectionOutline({
|
|
162
|
+
nodeId,
|
|
163
|
+
name,
|
|
164
|
+
color,
|
|
165
|
+
scale,
|
|
166
|
+
}: {
|
|
167
|
+
nodeId: string;
|
|
168
|
+
name: string;
|
|
169
|
+
color: string;
|
|
170
|
+
scale: number;
|
|
171
|
+
}) {
|
|
172
|
+
const node = useInternalNode(nodeId);
|
|
173
|
+
if (!node) return null;
|
|
174
|
+
const { width, height } = sizeOf(node);
|
|
175
|
+
const { x, y } = node.internals.positionAbsolute;
|
|
176
|
+
return (
|
|
177
|
+
<div
|
|
178
|
+
className="absolute"
|
|
179
|
+
style={{
|
|
180
|
+
left: x - 4 * scale,
|
|
181
|
+
top: y - 4 * scale,
|
|
182
|
+
width: width + 8 * scale,
|
|
183
|
+
height: height + 8 * scale,
|
|
184
|
+
border: `${2 * scale}px solid ${color}`,
|
|
185
|
+
borderRadius: 8 * scale,
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
<span
|
|
189
|
+
className="absolute right-0 top-0 whitespace-nowrap rounded px-1.5 text-[10px] leading-4 text-white"
|
|
190
|
+
style={{ backgroundColor: color, transform: `translateY(-100%) scale(${scale})`, transformOrigin: 'bottom right' }}
|
|
191
|
+
>
|
|
192
|
+
{name}
|
|
193
|
+
</span>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Shares what we're looking at, so others can jump to it: the canvas position in the middle of the screen,
|
|
200
|
+
* and the zoom. Follows React Flow's store (people's moves and code's, e.g. fit view) without rendering.
|
|
201
|
+
*/
|
|
202
|
+
export const ViewportSharer = memo(function ViewportSharer({
|
|
203
|
+
onChange,
|
|
204
|
+
}: {
|
|
205
|
+
onChange: (viewport: { x: number; y: number; zoom: number }) => void;
|
|
206
|
+
}) {
|
|
207
|
+
const store = useStoreApi();
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
210
|
+
const share = () => {
|
|
211
|
+
const {
|
|
212
|
+
transform: [x, y, zoom],
|
|
213
|
+
width,
|
|
214
|
+
height,
|
|
215
|
+
} = store.getState();
|
|
216
|
+
onChange({ x: (width / 2 - x) / zoom, y: (height / 2 - y) / zoom, zoom });
|
|
217
|
+
};
|
|
218
|
+
const unsubscribe = store.subscribe((state, previous) => {
|
|
219
|
+
if (state.transform === previous.transform && state.width === previous.width && state.height === previous.height) return;
|
|
220
|
+
clearTimeout(timer);
|
|
221
|
+
timer = setTimeout(share, 150);
|
|
222
|
+
});
|
|
223
|
+
share();
|
|
224
|
+
return () => {
|
|
225
|
+
unsubscribe();
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
};
|
|
228
|
+
}, [store, onChange]);
|
|
229
|
+
return null;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Agents at work: who's working (with a button to follow them or stop), and the camera following them while
|
|
234
|
+
* they work (on by default inside a chat). `pause` is set for whoever moves the camera themselves.
|
|
235
|
+
*/
|
|
236
|
+
export const AgentActivity = memo(function AgentActivity({
|
|
237
|
+
store,
|
|
238
|
+
clientId,
|
|
239
|
+
enabled,
|
|
240
|
+
following,
|
|
241
|
+
onToggleFollowing,
|
|
242
|
+
pauseRef,
|
|
243
|
+
}: {
|
|
244
|
+
store: PresenceStore | null;
|
|
245
|
+
clientId: number | null;
|
|
246
|
+
/** Off in the read only levels */
|
|
247
|
+
enabled: boolean;
|
|
248
|
+
following: boolean;
|
|
249
|
+
onToggleFollowing: () => void;
|
|
250
|
+
pauseRef: MutableRefObject<() => void>;
|
|
251
|
+
}) {
|
|
252
|
+
const people = usePeople(store);
|
|
253
|
+
const working = people.filter((peer) => peer.clientId !== clientId && peer.agent && peer.pointer);
|
|
254
|
+
const camera = useFollowCamera({
|
|
255
|
+
enabled: following && enabled,
|
|
256
|
+
agentsWorking: working.length > 0,
|
|
257
|
+
getAgents: () => (store?.getPeers() ?? []).filter((peer: Peer) => peer.clientId !== clientId && peer.agent && peer.pointer),
|
|
258
|
+
});
|
|
259
|
+
pauseRef.current = camera.pause;
|
|
260
|
+
|
|
261
|
+
if (!enabled || working.length === 0) return null;
|
|
262
|
+
return (
|
|
263
|
+
<div className="absolute left-1/2 top-16 z-10 flex -translate-x-1/2 items-center gap-2 rounded-full border px-3 py-1 text-xs shadow-sm bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))]">
|
|
264
|
+
<span className="flex -space-x-1">
|
|
265
|
+
{working.map((agent) => (
|
|
266
|
+
<span
|
|
267
|
+
key={agent.clientId}
|
|
268
|
+
className="h-2.5 w-2.5 rounded-full ring-2 ring-[rgb(var(--ec-card-bg))]"
|
|
269
|
+
style={{ backgroundColor: agent.color }}
|
|
270
|
+
/>
|
|
271
|
+
))}
|
|
272
|
+
</span>
|
|
273
|
+
<span>
|
|
274
|
+
{following ? 'Following' : ''} <span className="font-semibold">{working.map((agent) => agent.name).join(', ')}</span>
|
|
275
|
+
{following ? '' : ` ${working.length === 1 ? 'is' : 'are'} working`}
|
|
276
|
+
</span>
|
|
277
|
+
<button
|
|
278
|
+
onClick={onToggleFollowing}
|
|
279
|
+
className="flex items-center gap-1 rounded-full px-2 py-0.5 font-medium text-[rgb(var(--ec-accent))] hover:bg-[rgb(var(--ec-accent-subtle))]"
|
|
280
|
+
>
|
|
281
|
+
{following ? <EyeOff size={12} /> : <Eye size={12} />}
|
|
282
|
+
{following ? 'Stop' : 'Follow'}
|
|
283
|
+
</button>
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { memo, useState, type FormEvent, type ReactNode } from 'react';
|
|
2
|
+
import type { Node } from '@xyflow/react';
|
|
3
|
+
import { Box, ExternalLink, Library, Trash2, X } from 'lucide-react';
|
|
4
|
+
import { getCatalogLink } from '../catalog';
|
|
5
|
+
import { getNodeLabel, getResource, isVersioned, updateResource } from '../node-types';
|
|
6
|
+
import type { UpdateNodeData } from './canvas-nodes';
|
|
7
|
+
import { NODE_ICONS } from './icons';
|
|
8
|
+
import { STATUS } from './status';
|
|
9
|
+
|
|
10
|
+
const inputClass =
|
|
11
|
+
'w-full rounded-lg border px-3 py-2 text-sm shadow-sm 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))] focus:ring-2 focus:ring-[rgb(var(--ec-accent)/0.25)] read-only:cursor-default read-only:opacity-70 read-only:shadow-none read-only:focus:ring-0 read-only:focus:border-[rgb(var(--ec-input-border))]';
|
|
12
|
+
const buttonClass = 'rounded-lg px-4 py-2 text-sm font-medium transition-colors';
|
|
13
|
+
|
|
14
|
+
// Versions are usually semantic versions (1.0.0), but any text is allowed
|
|
15
|
+
const LOOKS_LIKE_A_VERSION = /^v?\d+(\.\d+){0,2}([-+][\w.-]+)?$/;
|
|
16
|
+
|
|
17
|
+
/** How to name each type, shown under the name */
|
|
18
|
+
const NAME_HINTS: Record<string, string> = {
|
|
19
|
+
event: 'The name of the event. Use a verb-noun format in the past tense (e.g., OrderPlaced).',
|
|
20
|
+
command: 'The name of the command. Use an instruction (e.g., PlaceOrder).',
|
|
21
|
+
query: 'The name of the query. Ask for something (e.g., GetOrder).',
|
|
22
|
+
service: 'The name of the service (e.g., Orders Service).',
|
|
23
|
+
channel: 'The name of the channel (e.g., orders.events).',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type Field = 'name' | 'version' | 'summary';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The details of the node being edited: its name, version and summary, saved together with Save changes (one
|
|
30
|
+
* change for everyone, and one step to undo). Catalog resources are read only: they're changed in the catalog.
|
|
31
|
+
* Notes and text are edited on the canvas.
|
|
32
|
+
*/
|
|
33
|
+
export default memo(function PropertiesPanel({
|
|
34
|
+
node,
|
|
35
|
+
onChange,
|
|
36
|
+
onDelete,
|
|
37
|
+
onClose,
|
|
38
|
+
}: {
|
|
39
|
+
/** What's shown of the node (not its position, so dragging it doesn't re-render the panel) */
|
|
40
|
+
node: Pick<Node, 'id' | 'type' | 'data'>;
|
|
41
|
+
onChange: UpdateNodeData;
|
|
42
|
+
onDelete: (ids: string[]) => void;
|
|
43
|
+
onClose: () => void;
|
|
44
|
+
}) {
|
|
45
|
+
const resource = getResource(node.type, node.data);
|
|
46
|
+
const link = getCatalogLink(node);
|
|
47
|
+
const label = getNodeLabel(node.type);
|
|
48
|
+
const noun = label.toLowerCase();
|
|
49
|
+
const { icon: Icon, className: iconClassName } = NODE_ICONS[node.type ?? ''] ?? { icon: Box, className: '' };
|
|
50
|
+
const versioned = isVersioned(node.type);
|
|
51
|
+
|
|
52
|
+
// What's been changed here and not saved. Fields not changed show the node as it is, so changes others make while
|
|
53
|
+
// it's open show up.
|
|
54
|
+
const [draft, setDraft] = useState<Partial<Record<Field, string>>>({});
|
|
55
|
+
const saved: Record<Field, string> = {
|
|
56
|
+
name: String(resource.name ?? ''),
|
|
57
|
+
version: String(link?.version ?? resource.version ?? ''),
|
|
58
|
+
summary: String(resource.summary ?? ''),
|
|
59
|
+
};
|
|
60
|
+
const value = (field: Field) => draft[field] ?? saved[field];
|
|
61
|
+
const edit = (field: Field) => (event: { target: { value: string } }) =>
|
|
62
|
+
setDraft((current) => ({ ...current, [field]: event.target.value }));
|
|
63
|
+
|
|
64
|
+
const changes = Object.entries(draft).filter(([field, text]) => text !== saved[field as Field]);
|
|
65
|
+
const missing = !value('name').trim() || (versioned && !value('version').trim());
|
|
66
|
+
const canSave = !link && changes.length > 0 && !missing;
|
|
67
|
+
const version = value('version');
|
|
68
|
+
|
|
69
|
+
const save = (event?: FormEvent) => {
|
|
70
|
+
event?.preventDefault();
|
|
71
|
+
if (!canSave) return;
|
|
72
|
+
onChange(node.id, (data) => updateResource(node.type, data, Object.fromEntries(changes)));
|
|
73
|
+
setDraft({});
|
|
74
|
+
onClose();
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<aside className="flex w-[22rem] shrink-0 flex-col border-l bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))]">
|
|
79
|
+
<header className="flex items-center gap-3 border-b px-5 py-4 border-[rgb(var(--ec-page-border))]">
|
|
80
|
+
<Icon size={22} className={`shrink-0 ${iconClassName}`} />
|
|
81
|
+
<h2 className="min-w-0 flex-1 truncate text-lg font-semibold">{label}</h2>
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
onClick={onClose}
|
|
85
|
+
title="Close"
|
|
86
|
+
aria-label="Close"
|
|
87
|
+
className="shrink-0 rounded-md p-1 text-[rgb(var(--ec-icon-color))] hover:bg-[rgb(var(--ec-page-border)/0.5)] hover:text-[rgb(var(--ec-icon-hover))]"
|
|
88
|
+
>
|
|
89
|
+
<X size={18} />
|
|
90
|
+
</button>
|
|
91
|
+
</header>
|
|
92
|
+
|
|
93
|
+
<form
|
|
94
|
+
onSubmit={save}
|
|
95
|
+
// ⌘/Ctrl + Enter saves from any field (Enter alone adds a line to the summary)
|
|
96
|
+
onKeyDown={(event) => (event.metaKey || event.ctrlKey) && event.key === 'Enter' && save(event)}
|
|
97
|
+
className="flex min-h-0 flex-1 flex-col"
|
|
98
|
+
>
|
|
99
|
+
<div className="min-h-0 flex-1 space-y-5 overflow-y-auto px-5 py-5">
|
|
100
|
+
{link && (
|
|
101
|
+
<div className="space-y-2 rounded-lg border p-3 text-xs bg-[rgb(var(--ec-accent-subtle)/0.5)] border-[rgb(var(--ec-accent)/0.25)]">
|
|
102
|
+
<p className="flex items-center gap-1.5 font-semibold text-[rgb(var(--ec-page-text))]">
|
|
103
|
+
<Library size={14} className="text-[rgb(var(--ec-accent))]" />
|
|
104
|
+
From your catalog
|
|
105
|
+
</p>
|
|
106
|
+
<p className="text-[rgb(var(--ec-page-text-muted))]">
|
|
107
|
+
Its name, version and summary come from the catalog: change them there and they're kept in sync here.
|
|
108
|
+
</p>
|
|
109
|
+
<a
|
|
110
|
+
href={link.url}
|
|
111
|
+
target="_blank"
|
|
112
|
+
rel="noreferrer"
|
|
113
|
+
className="inline-flex items-center gap-1 font-medium text-[rgb(var(--ec-accent))] hover:underline"
|
|
114
|
+
>
|
|
115
|
+
Open in catalog <ExternalLink size={12} />
|
|
116
|
+
</a>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
<FormField label="Name" hint={NAME_HINTS[node.type ?? ''] ?? `The name of the ${noun}.`} required={!link}>
|
|
121
|
+
<input
|
|
122
|
+
autoFocus={!link}
|
|
123
|
+
value={value('name')}
|
|
124
|
+
readOnly={!!link}
|
|
125
|
+
onChange={edit('name')}
|
|
126
|
+
placeholder={`Name this ${noun}`}
|
|
127
|
+
className={inputClass}
|
|
128
|
+
/>
|
|
129
|
+
</FormField>
|
|
130
|
+
{versioned && (
|
|
131
|
+
<FormField label="Version" hint="The version number (e.g., 1.0.0)." required={!link}>
|
|
132
|
+
<input
|
|
133
|
+
value={version}
|
|
134
|
+
readOnly={!!link}
|
|
135
|
+
onChange={edit('version')}
|
|
136
|
+
placeholder="0.0.1"
|
|
137
|
+
spellCheck={false}
|
|
138
|
+
className={inputClass}
|
|
139
|
+
/>
|
|
140
|
+
{!link && version && !LOOKS_LIKE_A_VERSION.test(version) && (
|
|
141
|
+
<p className={`text-xs ${STATUS.warning.text}`}>Versions are usually like 1.0.0</p>
|
|
142
|
+
)}
|
|
143
|
+
</FormField>
|
|
144
|
+
)}
|
|
145
|
+
<FormField label="Summary" hint={`A brief summary of the ${noun}.`}>
|
|
146
|
+
<textarea
|
|
147
|
+
rows={5}
|
|
148
|
+
value={value('summary')}
|
|
149
|
+
readOnly={!!link}
|
|
150
|
+
onChange={edit('summary')}
|
|
151
|
+
placeholder={`What does this ${noun} do?`}
|
|
152
|
+
className={`${inputClass} resize-y`}
|
|
153
|
+
/>
|
|
154
|
+
</FormField>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<footer className="flex items-center gap-2 border-t px-5 py-4 border-[rgb(var(--ec-page-border))]">
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
onClick={() => onDelete([node.id])}
|
|
161
|
+
title={`Delete ${noun}`}
|
|
162
|
+
aria-label={`Delete ${noun}`}
|
|
163
|
+
className={`mr-auto rounded-lg p-2 ${STATUS.danger.text} ${STATUS.danger.hover}`}
|
|
164
|
+
>
|
|
165
|
+
<Trash2 size={16} />
|
|
166
|
+
</button>
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
onClick={onClose}
|
|
170
|
+
className={`${buttonClass} border border-[rgb(var(--ec-page-border))] hover:bg-[rgb(var(--ec-page-border)/0.4)]`}
|
|
171
|
+
>
|
|
172
|
+
{link ? 'Close' : 'Cancel'}
|
|
173
|
+
</button>
|
|
174
|
+
{!link && (
|
|
175
|
+
<button
|
|
176
|
+
type="submit"
|
|
177
|
+
disabled={!canSave}
|
|
178
|
+
className={`${buttonClass} bg-[rgb(var(--ec-button-bg))] text-[rgb(var(--ec-button-text))] hover:bg-[rgb(var(--ec-button-bg-hover))] disabled:cursor-not-allowed disabled:opacity-50`}
|
|
179
|
+
>
|
|
180
|
+
Save changes
|
|
181
|
+
</button>
|
|
182
|
+
)}
|
|
183
|
+
</footer>
|
|
184
|
+
</form>
|
|
185
|
+
</aside>
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
function FormField({
|
|
190
|
+
label,
|
|
191
|
+
hint,
|
|
192
|
+
required = false,
|
|
193
|
+
children,
|
|
194
|
+
}: {
|
|
195
|
+
label: string;
|
|
196
|
+
hint: string;
|
|
197
|
+
required?: boolean;
|
|
198
|
+
children: ReactNode;
|
|
199
|
+
}) {
|
|
200
|
+
return (
|
|
201
|
+
<label className="block space-y-2">
|
|
202
|
+
<span className="block">
|
|
203
|
+
<span className="block text-sm font-semibold text-[rgb(var(--ec-page-text))]">
|
|
204
|
+
{label}
|
|
205
|
+
{required && <span className={`ml-0.5 ${STATUS.danger.text}`}>*</span>}
|
|
206
|
+
</span>
|
|
207
|
+
<span className="mt-0.5 block text-xs text-[rgb(var(--ec-page-text-muted))]">{hint}</span>
|
|
208
|
+
</span>
|
|
209
|
+
{children}
|
|
210
|
+
</label>
|
|
211
|
+
);
|
|
212
|
+
}
|