@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,381 @@
|
|
|
1
|
+
import type { Edge, Node, XYPosition } from '@xyflow/react';
|
|
2
|
+
import type * as Y from 'yjs';
|
|
3
|
+
import {
|
|
4
|
+
addNodes,
|
|
5
|
+
connectNodes,
|
|
6
|
+
createThread,
|
|
7
|
+
deleteEdges,
|
|
8
|
+
deleteNodes,
|
|
9
|
+
fitContainersAround,
|
|
10
|
+
getCanvasMaps,
|
|
11
|
+
moveNode,
|
|
12
|
+
resizeNode,
|
|
13
|
+
setMeta,
|
|
14
|
+
type Author,
|
|
15
|
+
type CommentAnchor,
|
|
16
|
+
} from './canvas-doc';
|
|
17
|
+
import {
|
|
18
|
+
describeNode,
|
|
19
|
+
nodeCenter,
|
|
20
|
+
planNodes,
|
|
21
|
+
resolveEdgeRefs,
|
|
22
|
+
topLeftFor,
|
|
23
|
+
updateCanvasNode,
|
|
24
|
+
type CatalogIndex,
|
|
25
|
+
type EdgeSpecs,
|
|
26
|
+
type NodeSpecs,
|
|
27
|
+
type NodeUpdate,
|
|
28
|
+
} from './canvas-actions';
|
|
29
|
+
import { getRelatedEdges } from './catalog';
|
|
30
|
+
import { getAbsolutePosition, sizeOf } from './grouping';
|
|
31
|
+
import type { LayoutResult } from './layout';
|
|
32
|
+
import { getNodeName } from './node-types';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Agents work on a canvas like a person would: their cursor travels to where they're going, they drop one node,
|
|
36
|
+
* drag a connection from it to the next, and so on, rather than everything appearing at once. Used by the MCP
|
|
37
|
+
* tools (on the server) and the WebMCP tools (in the browser), with a stage that makes the changes and shows
|
|
38
|
+
* the agent's presence there.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/** What everyone sees of an agent at work */
|
|
42
|
+
export type AgentPresence = {
|
|
43
|
+
pointer: XYPosition | null;
|
|
44
|
+
selection: string[];
|
|
45
|
+
activity?: string;
|
|
46
|
+
/** Where a connection being dragged starts: drawn from there to the pointer */
|
|
47
|
+
connecting?: XYPosition | null;
|
|
48
|
+
/** Nodes being moved, by id, and where they are (relative to their containers), until they're dropped */
|
|
49
|
+
moving?: Record<string, XYPosition> | null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type AgentStage = {
|
|
53
|
+
/** Change the canvas (and read it) */
|
|
54
|
+
change: <T>(fn: (doc: Y.Doc) => T) => Promise<T> | T;
|
|
55
|
+
/** Show the agent on the canvas */
|
|
56
|
+
present: (presence: AgentPresence) => void;
|
|
57
|
+
/** Where the agent's pointer was left last time, if it's been here */
|
|
58
|
+
lastPointer?: XYPosition | null;
|
|
59
|
+
/**
|
|
60
|
+
* Everyone sees the agent's presence, so nodes it moves are shown moving with it and only saved where they
|
|
61
|
+
* end up, like a person's drag. Otherwise each step of a move is saved, so others see it move.
|
|
62
|
+
*/
|
|
63
|
+
sharesMoves?: boolean;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const FRAME_MS = 40;
|
|
67
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
68
|
+
const easeInOut = (t: number) => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2);
|
|
69
|
+
const distance = (a: XYPosition, b: XYPosition) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
70
|
+
|
|
71
|
+
type Lookup = Map<string, Node>;
|
|
72
|
+
/** Where a node is on the canvas (nodes in containers are positioned relative to them) */
|
|
73
|
+
const positionOf = (node: Node, nodes: Lookup) => getAbsolutePosition(node, nodes);
|
|
74
|
+
/** Where connections leave a node (its right side) and arrive (its left side) */
|
|
75
|
+
const sourceHandle = (node: Node, nodes: Lookup) => {
|
|
76
|
+
const position = positionOf(node, nodes);
|
|
77
|
+
return { x: position.x + sizeOf(node).width, y: position.y + sizeOf(node).height / 2 };
|
|
78
|
+
};
|
|
79
|
+
const targetHandle = (node: Node, nodes: Lookup) => {
|
|
80
|
+
const position = positionOf(node, nodes);
|
|
81
|
+
return { x: position.x, y: position.y + sizeOf(node).height / 2 };
|
|
82
|
+
};
|
|
83
|
+
const nameOf = (node: Node) => getNodeName(node.type, node.data);
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The agent's hand: moves its pointer smoothly (in frames, so it glides for everyone) and shows what it's doing.
|
|
87
|
+
* `pace` slows down or speeds up everything (big changes go faster, so a tool call doesn't take too long).
|
|
88
|
+
*/
|
|
89
|
+
export function createAgentHand(stage: AgentStage, { pace = 1 }: { pace?: number } = {}) {
|
|
90
|
+
const presence: AgentPresence = { pointer: stage.lastPointer ?? null, selection: [] };
|
|
91
|
+
const show = () => stage.present({ ...presence });
|
|
92
|
+
|
|
93
|
+
const hand = {
|
|
94
|
+
get pointer() {
|
|
95
|
+
return presence.pointer;
|
|
96
|
+
},
|
|
97
|
+
async pause(ms: number) {
|
|
98
|
+
await sleep(ms * pace);
|
|
99
|
+
},
|
|
100
|
+
say(activity: string) {
|
|
101
|
+
presence.activity = activity;
|
|
102
|
+
show();
|
|
103
|
+
},
|
|
104
|
+
select(ids: string[]) {
|
|
105
|
+
presence.selection = ids;
|
|
106
|
+
show();
|
|
107
|
+
},
|
|
108
|
+
/**
|
|
109
|
+
* Glide the pointer to a point, e.g. dragging a connection from `connecting`, or carrying a node held at
|
|
110
|
+
* `grab` from its position
|
|
111
|
+
*/
|
|
112
|
+
async moveTo(
|
|
113
|
+
target: XYPosition,
|
|
114
|
+
{ connecting, carry }: { connecting?: XYPosition; carry?: { nodeId: string; grab: XYPosition } } = {}
|
|
115
|
+
) {
|
|
116
|
+
// The first time, come in from a little way off rather than appearing on the spot
|
|
117
|
+
const from = presence.pointer ?? { x: target.x - 160, y: target.y - 120 };
|
|
118
|
+
const duration = Math.min(900, Math.max(250, 200 + distance(from, target) * 0.6)) * pace;
|
|
119
|
+
const frames = Math.max(1, Math.round(duration / FRAME_MS));
|
|
120
|
+
presence.connecting = connecting ?? null;
|
|
121
|
+
for (let frame = 1; frame <= frames; frame++) {
|
|
122
|
+
const t = easeInOut(frame / frames);
|
|
123
|
+
presence.pointer = { x: from.x + (target.x - from.x) * t, y: from.y + (target.y - from.y) * t };
|
|
124
|
+
if (carry)
|
|
125
|
+
presence.moving = {
|
|
126
|
+
[carry.nodeId]: { x: presence.pointer.x - carry.grab.x, y: presence.pointer.y - carry.grab.y },
|
|
127
|
+
};
|
|
128
|
+
show();
|
|
129
|
+
await sleep(FRAME_MS);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
/** Let go of a connection being dragged, or nodes being carried (once where they're dropped is saved) */
|
|
133
|
+
release() {
|
|
134
|
+
presence.connecting = null;
|
|
135
|
+
presence.moving = null;
|
|
136
|
+
show();
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
return hand;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Faster for bigger changes, so a tool call stays well within client timeouts */
|
|
143
|
+
const paceFor = (steps: number) => Math.max(0.25, Math.min(1, 10 / Math.max(steps, 1)));
|
|
144
|
+
|
|
145
|
+
const readNodes = (doc: Y.Doc) => new Map<string, Node>(getCanvasMaps(doc).nodes.entries());
|
|
146
|
+
|
|
147
|
+
/** Drag a connection from one node to another, then make it */
|
|
148
|
+
async function playConnection(
|
|
149
|
+
stage: AgentStage,
|
|
150
|
+
hand: ReturnType<typeof createAgentHand>,
|
|
151
|
+
connect: (doc: Y.Doc) => Edge | { error: string },
|
|
152
|
+
sourceId: string,
|
|
153
|
+
targetId: string
|
|
154
|
+
) {
|
|
155
|
+
const nodes = await stage.change(readNodes);
|
|
156
|
+
const source = nodes.get(sourceId);
|
|
157
|
+
const target = nodes.get(targetId);
|
|
158
|
+
if (!source || !target) return stage.change(connect);
|
|
159
|
+
hand.say(`Connecting ${nameOf(source)} to ${nameOf(target)}`);
|
|
160
|
+
const start = sourceHandle(source, nodes);
|
|
161
|
+
await hand.moveTo(start);
|
|
162
|
+
await hand.pause(120);
|
|
163
|
+
await hand.moveTo(targetHandle(target, nodes), { connecting: start });
|
|
164
|
+
const outcome = await stage.change(connect);
|
|
165
|
+
hand.release();
|
|
166
|
+
await hand.pause(180);
|
|
167
|
+
return outcome;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** addToCanvas, played out: each node dropped in turn, connected (to related resources and as asked) as soon as it can be */
|
|
171
|
+
export async function playAddToCanvas(
|
|
172
|
+
stage: AgentStage,
|
|
173
|
+
{ nodes: specs, edges = [] }: { nodes: NodeSpecs; edges?: EdgeSpecs },
|
|
174
|
+
catalog: CatalogIndex
|
|
175
|
+
) {
|
|
176
|
+
const existing = await stage.change((doc) => [...readNodes(doc).values()]);
|
|
177
|
+
const { planned, errors } = planNodes(existing, specs, catalog, edges);
|
|
178
|
+
const pending = resolveEdgeRefs(edges, planned);
|
|
179
|
+
const hand = createAgentHand(stage, { pace: paceFor(planned.length * 2 + pending.length) });
|
|
180
|
+
const connections: string[] = [];
|
|
181
|
+
|
|
182
|
+
for (const { node: plannedNode, canvasPosition, name, resource } of planned) {
|
|
183
|
+
hand.say(`Adding ${name}`);
|
|
184
|
+
const { width, height } = sizeOf(plannedNode);
|
|
185
|
+
await hand.moveTo({ x: canvasPosition.x + width / 2, y: canvasPosition.y + height / 2 });
|
|
186
|
+
await hand.pause(150);
|
|
187
|
+
const { node, related } = await stage.change((doc) => {
|
|
188
|
+
const onCanvas = readNodes(doc);
|
|
189
|
+
// Where it goes on the canvas, relative to its container as it is now (containers move as they grow)
|
|
190
|
+
const parent = plannedNode.parentId ? onCanvas.get(plannedNode.parentId) : undefined;
|
|
191
|
+
const origin = parent ? getAbsolutePosition(parent, onCanvas) : { x: 0, y: 0 };
|
|
192
|
+
const node = { ...plannedNode, position: { x: canvasPosition.x - origin.x, y: canvasPosition.y - origin.y } };
|
|
193
|
+
addNodes(doc, [node]);
|
|
194
|
+
// The containers it's in grow to fit it
|
|
195
|
+
fitContainersAround(doc, node.id);
|
|
196
|
+
const related = resource
|
|
197
|
+
? getRelatedEdges(resource, node.id, [...onCanvas.values()], catalog.relationsByKey, node.type)
|
|
198
|
+
: [];
|
|
199
|
+
return { node, related };
|
|
200
|
+
});
|
|
201
|
+
hand.select([node.id]);
|
|
202
|
+
await hand.pause(300);
|
|
203
|
+
|
|
204
|
+
// Its connections to what's already there: related catalog resources, then the ones asked for
|
|
205
|
+
for (const edge of related) {
|
|
206
|
+
await playConnection(stage, hand, (doc) => (addNodes(doc, [], [edge]), edge), edge.source, edge.target);
|
|
207
|
+
connections.push(edge.id);
|
|
208
|
+
}
|
|
209
|
+
const onCanvas = await stage.change((doc) => new Set(readNodes(doc).keys()));
|
|
210
|
+
for (const edge of pending.filter((edge) => onCanvas.has(edge.from) && onCanvas.has(edge.to))) {
|
|
211
|
+
pending.splice(pending.indexOf(edge), 1);
|
|
212
|
+
const outcome = await playConnection(
|
|
213
|
+
stage,
|
|
214
|
+
hand,
|
|
215
|
+
(doc) => connectNodes(doc, { source: edge.from, target: edge.to }, edge.label),
|
|
216
|
+
edge.from,
|
|
217
|
+
edge.to
|
|
218
|
+
);
|
|
219
|
+
if ('error' in outcome) errors.push(outcome.error);
|
|
220
|
+
else connections.push(outcome.id);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Connections to nodes that aren't on the canvas
|
|
225
|
+
for (const edge of pending) {
|
|
226
|
+
const outcome = await stage.change((doc) => connectNodes(doc, { source: edge.from, target: edge.to }, edge.label));
|
|
227
|
+
if ('error' in outcome) errors.push(outcome.error);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
hand.say('Done');
|
|
231
|
+
return { created: planned.map(({ ref, node, name }) => ({ ref, nodeId: node.id, name })), connections, errors };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** connectCanvasNodes, played out: each connection dragged from one node to the other */
|
|
235
|
+
export async function playConnectAll(stage: AgentStage, edges: EdgeSpecs) {
|
|
236
|
+
const hand = createAgentHand(stage, { pace: paceFor(edges.length) });
|
|
237
|
+
const connections: string[] = [];
|
|
238
|
+
const errors: string[] = [];
|
|
239
|
+
for (const edge of edges) {
|
|
240
|
+
const outcome = await playConnection(
|
|
241
|
+
stage,
|
|
242
|
+
hand,
|
|
243
|
+
(doc) => connectNodes(doc, { source: edge.from, target: edge.to }, edge.label),
|
|
244
|
+
edge.from,
|
|
245
|
+
edge.to
|
|
246
|
+
);
|
|
247
|
+
if ('error' in outcome) errors.push(outcome.error);
|
|
248
|
+
else connections.push(outcome.id);
|
|
249
|
+
}
|
|
250
|
+
return { connections, errors };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Moving a node, played out: grab it and drag it there */
|
|
254
|
+
export async function playMoveNode(stage: AgentStage, nodeId: string, to: XYPosition, hand = createAgentHand(stage)) {
|
|
255
|
+
const nodes = await stage.change(readNodes);
|
|
256
|
+
const node = nodes.get(nodeId);
|
|
257
|
+
if (!node) return false;
|
|
258
|
+
// Positions are relative to the node's container, the pointer is on the canvas
|
|
259
|
+
const parent = node.parentId ? nodes.get(node.parentId) : undefined;
|
|
260
|
+
const origin = parent ? positionOf(parent, nodes) : { x: 0, y: 0 };
|
|
261
|
+
const grab = { x: origin.x + sizeOf(node).width / 2, y: origin.y + 24 };
|
|
262
|
+
hand.say(`Moving ${nameOf(node)}`);
|
|
263
|
+
await hand.moveTo({ x: node.position.x + grab.x, y: node.position.y + grab.y });
|
|
264
|
+
hand.select([nodeId]);
|
|
265
|
+
await hand.pause(120);
|
|
266
|
+
if (stage.sharesMoves) {
|
|
267
|
+
await hand.moveTo({ x: to.x + grab.x, y: to.y + grab.y }, { carry: { nodeId, grab } });
|
|
268
|
+
await stage.change((doc) => moveNode(doc, nodeId, to));
|
|
269
|
+
hand.release();
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
const from = node.position;
|
|
273
|
+
const frames = 12;
|
|
274
|
+
for (let frame = 1; frame <= frames; frame++) {
|
|
275
|
+
const t = easeInOut(frame / frames);
|
|
276
|
+
const position = { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t };
|
|
277
|
+
await stage.change((doc) => moveNode(doc, nodeId, position));
|
|
278
|
+
await hand.moveTo({ x: position.x + grab.x, y: position.y + grab.y });
|
|
279
|
+
}
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Laying the canvas out: everything moves in one change, and everyone's canvas glides the nodes into place
|
|
285
|
+
* (the layout time in the canvas's meta tells them to)
|
|
286
|
+
*/
|
|
287
|
+
export async function playLayout(stage: AgentStage, layout: LayoutResult) {
|
|
288
|
+
const hand = createAgentHand(stage);
|
|
289
|
+
hand.say('Tidying the layout');
|
|
290
|
+
await stage.change((doc) => applyLayout(doc, layout));
|
|
291
|
+
await hand.pause(500);
|
|
292
|
+
hand.say('Done');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Moves nodes and resizes containers from a layout, in one change, and marks when it happened */
|
|
296
|
+
export const applyLayout = (doc: Y.Doc, layout: LayoutResult) =>
|
|
297
|
+
doc.transact(() => {
|
|
298
|
+
layout.forEach(({ position, size }, id) => {
|
|
299
|
+
moveNode(doc, id, position);
|
|
300
|
+
if (size) resizeNode(doc, id, size);
|
|
301
|
+
});
|
|
302
|
+
setMeta(doc, { layoutAt: Date.now() });
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
/** Removing things, played out: point at each, then remove it */
|
|
306
|
+
export async function playRemove(stage: AgentStage, nodeIds: string[], connectionIds: string[]) {
|
|
307
|
+
const hand = createAgentHand(stage, { pace: paceFor(nodeIds.length) });
|
|
308
|
+
if (connectionIds.length) await stage.change((doc) => deleteEdges(doc, connectionIds));
|
|
309
|
+
for (const id of nodeIds) {
|
|
310
|
+
const nodes = await stage.change(readNodes);
|
|
311
|
+
const node = nodes.get(id);
|
|
312
|
+
if (!node) continue;
|
|
313
|
+
hand.say(`Removing ${nameOf(node)}`);
|
|
314
|
+
await hand.moveTo(nodeCenter(node, nodes));
|
|
315
|
+
hand.select([id]);
|
|
316
|
+
await hand.pause(250);
|
|
317
|
+
await stage.change((doc) => deleteNodes(doc, [id]));
|
|
318
|
+
hand.select([]);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Commenting, played out: go to where the comment goes, then leave it */
|
|
323
|
+
export async function playComment(
|
|
324
|
+
stage: AgentStage,
|
|
325
|
+
{ text, nodeId, x, y }: { text: string; nodeId?: string; x?: number; y?: number },
|
|
326
|
+
author: Author
|
|
327
|
+
) {
|
|
328
|
+
const nodes = await stage.change(readNodes);
|
|
329
|
+
const node = nodeId ? nodes.get(nodeId) : undefined;
|
|
330
|
+
if (nodeId && !node) return { error: `No node "${nodeId}" on the canvas` };
|
|
331
|
+
const offset = node ? { x: sizeOf(node).width - 8, y: 8 } : undefined;
|
|
332
|
+
const position = node && positionOf(node, nodes);
|
|
333
|
+
const anchor: CommentAnchor =
|
|
334
|
+
node && offset && position
|
|
335
|
+
? { position: { x: position.x + offset.x, y: position.y + offset.y }, nodeId: node.id, offset }
|
|
336
|
+
: { position: { x: x ?? 0, y: y ?? 0 } };
|
|
337
|
+
const hand = createAgentHand(stage);
|
|
338
|
+
hand.say('Commenting');
|
|
339
|
+
await hand.moveTo(anchor.position);
|
|
340
|
+
await hand.pause(400);
|
|
341
|
+
const threadId = await stage.change((doc) => createThread(doc, anchor, text, author));
|
|
342
|
+
return { threadId };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Pointing at a node while changing it (e.g. a rename) */
|
|
346
|
+
export async function playOnNode<T>(stage: AgentStage, nodeId: string, activity: string, change: (doc: Y.Doc) => T) {
|
|
347
|
+
const nodes = await stage.change(readNodes);
|
|
348
|
+
const node = nodes.get(nodeId);
|
|
349
|
+
const hand = createAgentHand(stage);
|
|
350
|
+
if (node) {
|
|
351
|
+
hand.say(activity);
|
|
352
|
+
await hand.moveTo(nodeCenter(node, nodes));
|
|
353
|
+
hand.select([nodeId]);
|
|
354
|
+
await hand.pause(250);
|
|
355
|
+
}
|
|
356
|
+
return stage.change(change);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Updating a node, played out: point at it to rename or re-describe it, then drag it to its new centre (x/y) */
|
|
360
|
+
export async function playUpdateNode(
|
|
361
|
+
stage: AgentStage,
|
|
362
|
+
{ nodeId, name, summary, version, x, y }: NodeUpdate & { x?: number; y?: number }
|
|
363
|
+
) {
|
|
364
|
+
const missing = { error: `No node "${nodeId}" on the canvas` };
|
|
365
|
+
if (name !== undefined || summary !== undefined || version !== undefined) {
|
|
366
|
+
const outcome = await playOnNode(stage, nodeId, 'Editing', (doc) =>
|
|
367
|
+
updateCanvasNode(doc, { nodeId, name, summary, version })
|
|
368
|
+
);
|
|
369
|
+
if ('error' in outcome) return outcome;
|
|
370
|
+
}
|
|
371
|
+
if (x !== undefined || y !== undefined) {
|
|
372
|
+
const nodes = await stage.change(readNodes);
|
|
373
|
+
const node = nodes.get(nodeId);
|
|
374
|
+
if (!node) return missing;
|
|
375
|
+
const center = nodeCenter(node, nodes);
|
|
376
|
+
await playMoveNode(stage, nodeId, topLeftFor(node, { x: x ?? center.x, y: y ?? center.y }, nodes));
|
|
377
|
+
}
|
|
378
|
+
const nodes = await stage.change(readNodes);
|
|
379
|
+
const node = nodes.get(nodeId);
|
|
380
|
+
return node ? describeNode(node, nodes) : missing;
|
|
381
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Hono, type Context } from 'hono';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { paginate } from '@features/tools/catalog-tools';
|
|
4
|
+
import { MAX_TITLE_LENGTH, type CanvasStatus } from '../canvas-doc';
|
|
5
|
+
import { MAX_NAME_LENGTH } from '../identity';
|
|
6
|
+
import type { CanvasSummary, StudioRuntime } from '../server/runtime';
|
|
7
|
+
import type { SignedInUser } from '../server/sign-in';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The Studio API: canvases as a whole (listing, creating, reading and deleting them). What's on a canvas is changed
|
|
11
|
+
* over the collaboration connection, or by agents through the MCP tools, never here: they're live documents that
|
|
12
|
+
* people and agents edit together.
|
|
13
|
+
*
|
|
14
|
+
* GET /api/studio/canvases Canvases, most recently changed first (?pageSize=, ?cursor= for the next page)
|
|
15
|
+
* (not ones left empty and untitled: GET /:id still finds them)
|
|
16
|
+
* POST /api/studio/canvases Creates one: { "title"?: string, "createdBy"?: string }
|
|
17
|
+
* GET /api/studio/canvases/:id One canvas
|
|
18
|
+
* POST /api/studio/canvases/:id/copy Copies its design (nodes and connections, not comments or status) to a new
|
|
19
|
+
* draft: { "title"?: string, "createdBy"?: string }
|
|
20
|
+
* DELETE /api/studio/canvases/:id Deletes it for everyone (anyone who has it open is told), for good
|
|
21
|
+
*
|
|
22
|
+
* Errors are `{ "error": "..." }` with the HTTP status. With sign-in on, only signed-in people can use it (401
|
|
23
|
+
* otherwise), and canvases are created by whoever is signed in. Requests that change something (POST, DELETE) from
|
|
24
|
+
* outside a browser send `Content-Type: application/json`: Astro refuses ones with no content type that don't come
|
|
25
|
+
* from the catalog's own pages.
|
|
26
|
+
*/
|
|
27
|
+
export const CANVASES_API_PATH = '/api/studio/canvases';
|
|
28
|
+
|
|
29
|
+
const CANVAS_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
30
|
+
const MAX_PAGE_SIZE = 100;
|
|
31
|
+
|
|
32
|
+
/** A canvas, as the API gives it */
|
|
33
|
+
export type ApiCanvas = {
|
|
34
|
+
id: string;
|
|
35
|
+
title: string | null;
|
|
36
|
+
status: CanvasStatus;
|
|
37
|
+
/** ISO 8601 */
|
|
38
|
+
createdAt: string | null;
|
|
39
|
+
/** Who started it (a person, or an agent) */
|
|
40
|
+
createdBy: string | null;
|
|
41
|
+
/** When anyone (a person, or an agent) last changed it (ISO 8601) */
|
|
42
|
+
updatedAt: string | null;
|
|
43
|
+
nodeCount: number;
|
|
44
|
+
edgeCount: number;
|
|
45
|
+
openComments: number;
|
|
46
|
+
/** Where people open it */
|
|
47
|
+
url: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type CanvasesApiEnv = {
|
|
51
|
+
Bindings: {
|
|
52
|
+
runtime: StudioRuntime;
|
|
53
|
+
/** Who's signed in (sign-in on) */
|
|
54
|
+
signedInAs?: SignedInUser;
|
|
55
|
+
/** A canvas's page */
|
|
56
|
+
canvasUrl: (canvasId: string) => string;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const NewCanvas = z.object({
|
|
61
|
+
title: z.string().trim().max(MAX_TITLE_LENGTH).optional(),
|
|
62
|
+
// Ignored when someone's signed in: it's them
|
|
63
|
+
createdBy: z.string().trim().max(MAX_NAME_LENGTH).optional(),
|
|
64
|
+
});
|
|
65
|
+
const ListQuery = z.object({
|
|
66
|
+
cursor: z.string().optional(),
|
|
67
|
+
pageSize: z.coerce.number().int().min(1).max(MAX_PAGE_SIZE).optional(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const iso = (time?: number) => (time ? new Date(time).toISOString() : null);
|
|
71
|
+
const toApiCanvas = (canvas: CanvasSummary, canvasUrl: (canvasId: string) => string): ApiCanvas => ({
|
|
72
|
+
id: canvas.canvasId,
|
|
73
|
+
title: canvas.title ?? null,
|
|
74
|
+
status: canvas.status,
|
|
75
|
+
createdAt: iso(canvas.createdAt),
|
|
76
|
+
createdBy: canvas.createdBy ?? null,
|
|
77
|
+
updatedAt: iso(canvas.updatedAt),
|
|
78
|
+
nodeCount: canvas.nodeCount,
|
|
79
|
+
edgeCount: canvas.edgeCount,
|
|
80
|
+
openComments: canvas.openComments,
|
|
81
|
+
url: canvasUrl(canvas.canvasId),
|
|
82
|
+
});
|
|
83
|
+
const describeIssues = (error: z.ZodError) =>
|
|
84
|
+
error.issues.map((issue) => (issue.path.length ? `${issue.path.join('.')}: ${issue.message}` : issue.message)).join('; ');
|
|
85
|
+
|
|
86
|
+
type ApiContext = Context<CanvasesApiEnv>;
|
|
87
|
+
const noCanvas = (c: ApiContext, id: string) => c.json({ error: `No canvas "${id}"` }, 404);
|
|
88
|
+
|
|
89
|
+
/** A new canvas's details from the request (optional), or the response saying what's wrong with them */
|
|
90
|
+
const readNewCanvas = async (c: ApiContext): Promise<{ title?: string; createdBy?: string } | Response> => {
|
|
91
|
+
// JSON only: a page on another site can send a form or plain text here without asking first, but not JSON
|
|
92
|
+
const body = await c.req.text();
|
|
93
|
+
if (body && !c.req.header('content-type')?.toLowerCase().startsWith('application/json')) {
|
|
94
|
+
return c.json({ error: 'Send the canvas as JSON (Content-Type: application/json)' }, 415);
|
|
95
|
+
}
|
|
96
|
+
let json: unknown = {};
|
|
97
|
+
try {
|
|
98
|
+
if (body) json = JSON.parse(body);
|
|
99
|
+
} catch {
|
|
100
|
+
return c.json({ error: 'The body is not valid JSON' }, 400);
|
|
101
|
+
}
|
|
102
|
+
const parsed = NewCanvas.safeParse(json);
|
|
103
|
+
if (!parsed.success) return c.json({ error: describeIssues(parsed.error) }, 400);
|
|
104
|
+
return {
|
|
105
|
+
title: parsed.data.title || undefined,
|
|
106
|
+
// Whoever's signed in, if anyone
|
|
107
|
+
createdBy: c.env.signedInAs?.name ?? (parsed.data.createdBy || undefined),
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** The API, served under `basePath` (the catalog's base URL, then CANVASES_API_PATH) */
|
|
112
|
+
export const createCanvasesApi = (basePath: string) => {
|
|
113
|
+
const app = new Hono<CanvasesApiEnv>({ strict: false }).basePath(basePath);
|
|
114
|
+
|
|
115
|
+
app.get('/', (c) => {
|
|
116
|
+
const query = ListQuery.safeParse(c.req.query());
|
|
117
|
+
if (!query.success) return c.json({ error: describeIssues(query.error) }, 400);
|
|
118
|
+
const { runtime, canvasUrl } = c.env;
|
|
119
|
+
const page = paginate(runtime.listCanvases(), query.data.cursor, query.data.pageSize);
|
|
120
|
+
if ('error' in page) return c.json({ error: page.error }, 400);
|
|
121
|
+
return c.json({
|
|
122
|
+
canvases: page.items.map((canvas) => toApiCanvas(canvas, canvasUrl)),
|
|
123
|
+
totalCount: page.totalCount,
|
|
124
|
+
...(page.nextCursor && { nextCursor: page.nextCursor }),
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/** A canvas just made, as created */
|
|
129
|
+
const created = (c: ApiContext, canvasId: string) => {
|
|
130
|
+
c.header('Location', `${basePath}/${canvasId}`);
|
|
131
|
+
return c.json({ canvas: toApiCanvas(c.env.runtime.getCanvas(canvasId)!, c.env.canvasUrl) }, 201);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
app.post('/', async (c) => {
|
|
135
|
+
const details = await readNewCanvas(c);
|
|
136
|
+
if (details instanceof Response) return details;
|
|
137
|
+
return created(c, await c.env.runtime.createCanvas(details));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
app.post('/:id/copy', async (c) => {
|
|
141
|
+
const id = c.req.param('id');
|
|
142
|
+
if (!CANVAS_ID.test(id)) return noCanvas(c, id);
|
|
143
|
+
const details = await readNewCanvas(c);
|
|
144
|
+
if (details instanceof Response) return details;
|
|
145
|
+
const copyId = await c.env.runtime.copyCanvas(id, details);
|
|
146
|
+
return copyId ? created(c, copyId) : noCanvas(c, id);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
app.get('/:id', (c) => {
|
|
150
|
+
const id = c.req.param('id');
|
|
151
|
+
const canvas = CANVAS_ID.test(id) ? c.env.runtime.getCanvas(id) : undefined;
|
|
152
|
+
if (!canvas) return noCanvas(c, id);
|
|
153
|
+
return c.json({ canvas: toApiCanvas(canvas, c.env.canvasUrl) });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
app.delete('/:id', (c) => {
|
|
157
|
+
const id = c.req.param('id');
|
|
158
|
+
if (!CANVAS_ID.test(id)) return noCanvas(c, id);
|
|
159
|
+
try {
|
|
160
|
+
if (!c.env.runtime.deleteCanvas(id)) return noCanvas(c, id);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error(`[studio] Could not delete canvas ${id}`, error);
|
|
163
|
+
return c.json({ error: `Could not delete the canvas: ${(error as Error).message}` }, 500);
|
|
164
|
+
}
|
|
165
|
+
return c.body(null, 204);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
app.notFound((c) => c.json({ error: 'Not found' }, 404));
|
|
169
|
+
return app;
|
|
170
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { buildUrl } from '@utils/url-builder';
|
|
3
|
+
import { startStudio } from '../server/runtime';
|
|
4
|
+
import { getSignedInUser, isSignedIn } from '../server/sign-in';
|
|
5
|
+
import { CANVASES_API_PATH, createCanvasesApi } from './canvases-api';
|
|
6
|
+
|
|
7
|
+
// The Studio API (see canvases-api.ts)
|
|
8
|
+
const api = createCanvasesApi(buildUrl(CANVASES_API_PATH, true));
|
|
9
|
+
|
|
10
|
+
export const ALL: APIRoute = async ({ request, locals, url }) => {
|
|
11
|
+
let runtime: Awaited<ReturnType<typeof startStudio>>;
|
|
12
|
+
try {
|
|
13
|
+
runtime = await startStudio({ isSignedIn });
|
|
14
|
+
} catch (error) {
|
|
15
|
+
// Its storage can't be used (e.g. the database was updated by a newer EventCatalog)
|
|
16
|
+
return Response.json({ error: `Studio isn't available: ${(error as Error).message}` }, { status: 503 });
|
|
17
|
+
}
|
|
18
|
+
return api.fetch(request, {
|
|
19
|
+
runtime,
|
|
20
|
+
signedInAs: getSignedInUser(locals),
|
|
21
|
+
canvasUrl: (canvasId: string) => `${url.origin}${buildUrl(`/studio/${canvasId}`)}`,
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const prerender = false;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ApiCanvas } from './canvases-api';
|
|
2
|
+
|
|
3
|
+
/** Using the Studio API from the browser (Studio's pages), at `apiUrl` (its address on this catalog) */
|
|
4
|
+
|
|
5
|
+
const errorOf = async (response: Response, fallback: string) => {
|
|
6
|
+
const body = (await response.json().catch(() => null)) as { error?: string } | null;
|
|
7
|
+
return new Error(body?.error ?? `${fallback} (${response.status})`);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const createCanvasThroughApi = async (apiUrl: string, canvas: { title?: string; createdBy?: string }) => {
|
|
11
|
+
const response = await fetch(apiUrl, {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
14
|
+
body: JSON.stringify(canvas),
|
|
15
|
+
});
|
|
16
|
+
if (!response.ok) throw await errorOf(response, 'Could not create the canvas');
|
|
17
|
+
return ((await response.json()) as { canvas: ApiCanvas }).canvas;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Copies a canvas's design to a new draft */
|
|
21
|
+
export const copyCanvasThroughApi = async (apiUrl: string, canvasId: string, copy: { createdBy?: string } = {}) => {
|
|
22
|
+
const response = await fetch(`${apiUrl}/${canvasId}/copy`, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
25
|
+
body: JSON.stringify(copy),
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok) throw await errorOf(response, 'Could not copy the canvas');
|
|
28
|
+
return ((await response.json()) as { canvas: ApiCanvas }).canvas;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Deletes a canvas (one that's already gone counts as deleted) */
|
|
32
|
+
export const deleteCanvasThroughApi = async (apiUrl: string, canvasId: string) => {
|
|
33
|
+
const response = await fetch(`${apiUrl}/${canvasId}`, { method: 'DELETE', headers: { Accept: 'application/json' } });
|
|
34
|
+
if (!response.ok && response.status !== 404) throw await errorOf(response, 'Could not delete the canvas');
|
|
35
|
+
};
|