@petrarca/sonnet-playground 0.4.1 → 0.5.0
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/dist/{PlayCoreComponents-GXFMB24T.js → PlayCoreComponents-AG5D4SBS.js} +2 -2
- package/dist/PlayGraph-M7UE5MZC.js +7 -0
- package/dist/PlaySchemaDesigner-B2RVRRCH.js +7 -0
- package/dist/PlaySchemaDesigner-B2RVRRCH.js.map +1 -0
- package/dist/chunk-C2P3UQMZ.js +1058 -0
- package/dist/chunk-C2P3UQMZ.js.map +1 -0
- package/dist/chunk-PBT6HTKY.js +191 -0
- package/dist/chunk-PBT6HTKY.js.map +1 -0
- package/dist/{chunk-YOOWJORD.js → chunk-Q6NLY2BZ.js} +53 -2
- package/dist/chunk-Q6NLY2BZ.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/dist/pages/index.d.ts +10 -2
- package/dist/pages/index.js +6 -2
- package/package.json +7 -6
- package/dist/PlayGraph-ADRTDRA7.js +0 -7
- package/dist/chunk-4ORSUS65.js +0 -359
- package/dist/chunk-4ORSUS65.js.map +0 -1
- package/dist/chunk-YOOWJORD.js.map +0 -1
- /package/dist/{PlayCoreComponents-GXFMB24T.js.map → PlayCoreComponents-AG5D4SBS.js.map} +0 -0
- /package/dist/{PlayGraph-ADRTDRA7.js.map → PlayGraph-M7UE5MZC.js.map} +0 -0
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
// src/pages/PlayGraph.tsx
|
|
2
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
GraphVisualizer,
|
|
5
|
+
ActionPanel,
|
|
6
|
+
GraphA11yList,
|
|
7
|
+
GraphRendererProvider,
|
|
8
|
+
GraphStoresContext,
|
|
9
|
+
GraphEditToolbar,
|
|
10
|
+
createGraphDataStore,
|
|
11
|
+
createGraphFilterStore,
|
|
12
|
+
createGraphExplorationStore,
|
|
13
|
+
createSelectionStore,
|
|
14
|
+
createGraphFocusStore,
|
|
15
|
+
enrichNodes,
|
|
16
|
+
enrichEdges,
|
|
17
|
+
buildColorPalette,
|
|
18
|
+
GraphModelEditor
|
|
19
|
+
} from "@petrarca/sonnet-graph";
|
|
20
|
+
import { DEFAULT_WIDGETS } from "@petrarca/sonnet-forms";
|
|
21
|
+
|
|
22
|
+
// src/pages/graph/inMemoryGraphEditing.ts
|
|
23
|
+
function createIdAllocator(store) {
|
|
24
|
+
const allIds = [
|
|
25
|
+
...store.nodes.map((n) => n.id_ ?? 0),
|
|
26
|
+
...store.edges.map((e) => e.id_ ?? 0)
|
|
27
|
+
];
|
|
28
|
+
const max = allIds.reduce((m, id) => id > m ? id : m, 0);
|
|
29
|
+
let next = max + 1;
|
|
30
|
+
return () => next++;
|
|
31
|
+
}
|
|
32
|
+
function edgesForNode(store, id) {
|
|
33
|
+
return {
|
|
34
|
+
inbound: store.edges.filter((e) => e.end_id_ === id),
|
|
35
|
+
outbound: store.edges.filter((e) => e.start_id_ === id)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function diffRelations(store, nodeId, relations, nextId) {
|
|
39
|
+
const created = [];
|
|
40
|
+
const deleted = [];
|
|
41
|
+
for (const [relType, targetIds] of Object.entries(relations)) {
|
|
42
|
+
const existing = store.edges.filter(
|
|
43
|
+
(e) => e.label_ === relType && e.start_id_ === nodeId
|
|
44
|
+
);
|
|
45
|
+
const desired = new Set(targetIds);
|
|
46
|
+
for (const edge of existing) {
|
|
47
|
+
if (!desired.has(edge.end_id_) && edge.id_ != null) {
|
|
48
|
+
deleted.push(edge.id_);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const present = new Set(existing.map((e) => e.end_id_));
|
|
52
|
+
for (const targetId of targetIds) {
|
|
53
|
+
if (!present.has(targetId)) {
|
|
54
|
+
const edge = {
|
|
55
|
+
id_: nextId(),
|
|
56
|
+
label_: relType,
|
|
57
|
+
start_id_: nodeId,
|
|
58
|
+
end_id_: targetId,
|
|
59
|
+
properties_: {}
|
|
60
|
+
};
|
|
61
|
+
created.push(edge);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
store.edges = store.edges.filter((e) => e.id_ == null || !deleted.includes(e.id_)).concat(created);
|
|
66
|
+
return { created, deleted };
|
|
67
|
+
}
|
|
68
|
+
function createInMemoryPort(store, onChange) {
|
|
69
|
+
const nextId = createIdAllocator(store);
|
|
70
|
+
const createNode = (input) => {
|
|
71
|
+
const node = {
|
|
72
|
+
id_: nextId(),
|
|
73
|
+
label_: input.label_,
|
|
74
|
+
properties_: input.properties_
|
|
75
|
+
};
|
|
76
|
+
store.nodes = [...store.nodes, node];
|
|
77
|
+
return node;
|
|
78
|
+
};
|
|
79
|
+
const updateNode = (input) => {
|
|
80
|
+
const node = {
|
|
81
|
+
id_: input.id_,
|
|
82
|
+
label_: input.label_,
|
|
83
|
+
properties_: input.properties_
|
|
84
|
+
};
|
|
85
|
+
store.nodes = store.nodes.map((n) => n.id_ === input.id_ ? node : n);
|
|
86
|
+
return node;
|
|
87
|
+
};
|
|
88
|
+
const createEdge = (input) => {
|
|
89
|
+
const edge = {
|
|
90
|
+
id_: nextId(),
|
|
91
|
+
label_: input.label_,
|
|
92
|
+
start_id_: input.start_id_,
|
|
93
|
+
end_id_: input.end_id_,
|
|
94
|
+
properties_: input.properties_ ?? {}
|
|
95
|
+
};
|
|
96
|
+
store.edges = [...store.edges, edge];
|
|
97
|
+
return edge;
|
|
98
|
+
};
|
|
99
|
+
const updateEdge = (input) => {
|
|
100
|
+
let updated;
|
|
101
|
+
store.edges = store.edges.map((e) => {
|
|
102
|
+
if (e.id_ !== input.id_) return e;
|
|
103
|
+
updated = { ...e, properties_: input.properties_ };
|
|
104
|
+
return updated;
|
|
105
|
+
});
|
|
106
|
+
if (!updated) throw new Error(`Edge ${input.id_} not found`);
|
|
107
|
+
return updated;
|
|
108
|
+
};
|
|
109
|
+
const withRefresh = (fn) => {
|
|
110
|
+
return (...args) => {
|
|
111
|
+
const result = fn(...args);
|
|
112
|
+
onChange();
|
|
113
|
+
return Promise.resolve(result);
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
createNode: withRefresh(createNode),
|
|
118
|
+
updateNode: withRefresh(updateNode),
|
|
119
|
+
deleteNode: withRefresh((id) => {
|
|
120
|
+
store.nodes = store.nodes.filter((n) => n.id_ !== id);
|
|
121
|
+
store.edges = store.edges.filter(
|
|
122
|
+
(e) => e.start_id_ !== id && e.end_id_ !== id
|
|
123
|
+
);
|
|
124
|
+
}),
|
|
125
|
+
createEdge: withRefresh(createEdge),
|
|
126
|
+
updateEdge: withRefresh(updateEdge),
|
|
127
|
+
deleteEdge: withRefresh((id) => {
|
|
128
|
+
store.edges = store.edges.filter((e) => e.id_ !== id);
|
|
129
|
+
}),
|
|
130
|
+
createNodeWithRelations: withRefresh(
|
|
131
|
+
(input) => {
|
|
132
|
+
const node = createNode(input);
|
|
133
|
+
const { created } = diffRelations(
|
|
134
|
+
store,
|
|
135
|
+
node.id_,
|
|
136
|
+
input.relations,
|
|
137
|
+
nextId
|
|
138
|
+
);
|
|
139
|
+
return { node, edges: created };
|
|
140
|
+
}
|
|
141
|
+
),
|
|
142
|
+
updateNodeWithRelations: withRefresh(
|
|
143
|
+
(input) => {
|
|
144
|
+
const node = updateNode(input);
|
|
145
|
+
const { created, deleted } = diffRelations(
|
|
146
|
+
store,
|
|
147
|
+
input.id_,
|
|
148
|
+
input.relations,
|
|
149
|
+
nextId
|
|
150
|
+
);
|
|
151
|
+
return { node, edges: { created, deleted } };
|
|
152
|
+
}
|
|
153
|
+
),
|
|
154
|
+
getInboundEdges: (id) => Promise.resolve(edgesForNode(store, id).inbound),
|
|
155
|
+
getOutboundEdges: (id) => Promise.resolve(edgesForNode(store, id).outbound),
|
|
156
|
+
getPairEdges: (a, b) => Promise.resolve(
|
|
157
|
+
store.edges.filter(
|
|
158
|
+
(e) => e.start_id_ === a && e.end_id_ === b || e.start_id_ === b && e.end_id_ === a
|
|
159
|
+
)
|
|
160
|
+
),
|
|
161
|
+
getNodes: (ids) => Promise.resolve(
|
|
162
|
+
store.nodes.filter((n) => n.id_ != null && ids.includes(n.id_))
|
|
163
|
+
)
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function toPropertySchema(type) {
|
|
167
|
+
if (!type) return null;
|
|
168
|
+
return {
|
|
169
|
+
type: "object",
|
|
170
|
+
properties: type.properties ?? {},
|
|
171
|
+
required: type.required ?? []
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function createModelSchemaProvider(model) {
|
|
175
|
+
const byLabel = new Map(model.map((t) => [t["x-graph"].label, t]));
|
|
176
|
+
const nodeLabels = model.filter((t) => t["x-graph"].type !== "EDGE").map((t) => t["x-graph"].label);
|
|
177
|
+
return {
|
|
178
|
+
getNodeSchema: (label) => Promise.resolve(toPropertySchema(byLabel.get(label))),
|
|
179
|
+
getEdgeSchema: (type) => Promise.resolve(toPropertySchema(byLabel.get(type))),
|
|
180
|
+
listNodeLabels: () => Promise.resolve(nodeLabels),
|
|
181
|
+
relationsFor: (label) => {
|
|
182
|
+
const relations = byLabel.get(label)?.["x-graph"].relations ?? [];
|
|
183
|
+
return Promise.resolve(
|
|
184
|
+
relations.map((r) => ({
|
|
185
|
+
rel_type_name: r.rel_type_name,
|
|
186
|
+
to_type_name: r.to_type_name,
|
|
187
|
+
form_field: r.form_field,
|
|
188
|
+
// cardinality drives single- vs multi-select in the form.
|
|
189
|
+
cardinality: r.cardinality
|
|
190
|
+
}))
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/pages/graph/relationWidget.tsx
|
|
197
|
+
import {
|
|
198
|
+
FormSelect,
|
|
199
|
+
FormMultiSelect
|
|
200
|
+
} from "@petrarca/sonnet-forms";
|
|
201
|
+
import { extractDisplayLabel } from "@petrarca/sonnet-graph";
|
|
202
|
+
import { jsx } from "react/jsx-runtime";
|
|
203
|
+
function optionsForType(store, targetType) {
|
|
204
|
+
return store.nodes.filter((n) => n.label_ === targetType && n.id_ != null).map((n) => ({ value: String(n.id_), label: extractDisplayLabel(n) }));
|
|
205
|
+
}
|
|
206
|
+
function toStringArray(value) {
|
|
207
|
+
if (Array.isArray(value)) return value.map(String);
|
|
208
|
+
if (value == null || value === "") return [];
|
|
209
|
+
return [String(value)];
|
|
210
|
+
}
|
|
211
|
+
function createRelationWidget(store) {
|
|
212
|
+
return function RelationWidget({
|
|
213
|
+
value,
|
|
214
|
+
onChange,
|
|
215
|
+
label,
|
|
216
|
+
options,
|
|
217
|
+
placeholder
|
|
218
|
+
}) {
|
|
219
|
+
const targetType = String(options?.targetType ?? "");
|
|
220
|
+
const multiple = options?.multiple !== false;
|
|
221
|
+
const selectOptions = optionsForType(store, targetType);
|
|
222
|
+
if (multiple) {
|
|
223
|
+
return /* @__PURE__ */ jsx(
|
|
224
|
+
FormMultiSelect,
|
|
225
|
+
{
|
|
226
|
+
label: label || false,
|
|
227
|
+
options: selectOptions,
|
|
228
|
+
value: toStringArray(value),
|
|
229
|
+
onChange: (v) => onChange(v),
|
|
230
|
+
placeholder: placeholder ?? `Select ${targetType}...`
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const single = toStringArray(value)[0] ?? null;
|
|
235
|
+
return /* @__PURE__ */ jsx(
|
|
236
|
+
FormSelect,
|
|
237
|
+
{
|
|
238
|
+
label: label || false,
|
|
239
|
+
options: selectOptions,
|
|
240
|
+
value: single,
|
|
241
|
+
onChange: (v) => onChange(v ?? ""),
|
|
242
|
+
placeholder: placeholder ?? `Select ${targetType}...`
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/pages/PlayGraph.tsx
|
|
249
|
+
import {
|
|
250
|
+
Button,
|
|
251
|
+
Alert,
|
|
252
|
+
AlertDescription,
|
|
253
|
+
Tabs,
|
|
254
|
+
TabsContent,
|
|
255
|
+
TabsList,
|
|
256
|
+
TabsTrigger,
|
|
257
|
+
ResizeHandle
|
|
258
|
+
} from "@petrarca/sonnet-ui";
|
|
259
|
+
import { FormSelect as FormSelect2 } from "@petrarca/sonnet-forms";
|
|
260
|
+
import { Play, RotateCcw } from "lucide-react";
|
|
261
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
262
|
+
var TECH_NODES = [
|
|
263
|
+
{
|
|
264
|
+
id_: 1,
|
|
265
|
+
label_: "Product",
|
|
266
|
+
properties_: { key: "product-a", name: "Product A" }
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
id_: 2,
|
|
270
|
+
label_: "Product",
|
|
271
|
+
properties_: { key: "product-b", name: "Product B" }
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id_: 3,
|
|
275
|
+
label_: "TechnologyStack",
|
|
276
|
+
properties_: { key: "product-a-stack", name: "Product A stack" }
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
id_: 4,
|
|
280
|
+
label_: "TechnologyStack",
|
|
281
|
+
properties_: { key: "product-b-stack", name: "Product B stack" }
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id_: 5,
|
|
285
|
+
label_: "Technology",
|
|
286
|
+
properties_: { key: "react", name: "React" }
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id_: 6,
|
|
290
|
+
label_: "Technology",
|
|
291
|
+
properties_: { key: "postgresql", name: "PostgreSQL" }
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
id_: 7,
|
|
295
|
+
label_: "Technology",
|
|
296
|
+
properties_: { key: "kafka", name: "Kafka" }
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id_: 8,
|
|
300
|
+
label_: "Technology",
|
|
301
|
+
properties_: { key: "go", name: "Go" }
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
id_: 9,
|
|
305
|
+
label_: "TechnologyCategory",
|
|
306
|
+
properties_: { key: "frontend-framework", name: "Frontend Framework" }
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
id_: 10,
|
|
310
|
+
label_: "TechnologyCategory",
|
|
311
|
+
properties_: { key: "database", name: "Database" }
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
id_: 11,
|
|
315
|
+
label_: "TechnologyCategory",
|
|
316
|
+
properties_: { key: "messaging", name: "Messaging" }
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
id_: 12,
|
|
320
|
+
label_: "TechnologyCategory",
|
|
321
|
+
properties_: { key: "language", name: "Language" }
|
|
322
|
+
}
|
|
323
|
+
];
|
|
324
|
+
var TECH_EDGES = [
|
|
325
|
+
// Two products, each with its own stack; both stacks share PostgreSQL.
|
|
326
|
+
{
|
|
327
|
+
id_: 101,
|
|
328
|
+
label_: "HAS_TECH_STACK",
|
|
329
|
+
start_id_: 1,
|
|
330
|
+
end_id_: 3,
|
|
331
|
+
properties_: {}
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
id_: 102,
|
|
335
|
+
label_: "HAS_TECH_STACK",
|
|
336
|
+
start_id_: 2,
|
|
337
|
+
end_id_: 4,
|
|
338
|
+
properties_: {}
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
id_: 103,
|
|
342
|
+
label_: "INTEGRATES_WITH",
|
|
343
|
+
start_id_: 1,
|
|
344
|
+
end_id_: 2,
|
|
345
|
+
properties_: {}
|
|
346
|
+
},
|
|
347
|
+
// Product A stack: React, PostgreSQL, Kafka.
|
|
348
|
+
{
|
|
349
|
+
id_: 104,
|
|
350
|
+
label_: "USES_TECHNOLOGY",
|
|
351
|
+
start_id_: 3,
|
|
352
|
+
end_id_: 5,
|
|
353
|
+
properties_: {}
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
id_: 105,
|
|
357
|
+
label_: "USES_TECHNOLOGY",
|
|
358
|
+
start_id_: 3,
|
|
359
|
+
end_id_: 6,
|
|
360
|
+
properties_: {}
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
id_: 106,
|
|
364
|
+
label_: "USES_TECHNOLOGY",
|
|
365
|
+
start_id_: 3,
|
|
366
|
+
end_id_: 7,
|
|
367
|
+
properties_: {}
|
|
368
|
+
},
|
|
369
|
+
// Product B stack: Go + the shared PostgreSQL.
|
|
370
|
+
{
|
|
371
|
+
id_: 107,
|
|
372
|
+
label_: "USES_TECHNOLOGY",
|
|
373
|
+
start_id_: 4,
|
|
374
|
+
end_id_: 6,
|
|
375
|
+
properties_: {}
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
id_: 108,
|
|
379
|
+
label_: "USES_TECHNOLOGY",
|
|
380
|
+
start_id_: 4,
|
|
381
|
+
end_id_: 8,
|
|
382
|
+
properties_: {}
|
|
383
|
+
},
|
|
384
|
+
// Technology -> category.
|
|
385
|
+
{ id_: 109, label_: "BELONGS_TO", start_id_: 5, end_id_: 9, properties_: {} },
|
|
386
|
+
{
|
|
387
|
+
id_: 110,
|
|
388
|
+
label_: "BELONGS_TO",
|
|
389
|
+
start_id_: 6,
|
|
390
|
+
end_id_: 10,
|
|
391
|
+
properties_: {}
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
id_: 111,
|
|
395
|
+
label_: "BELONGS_TO",
|
|
396
|
+
start_id_: 7,
|
|
397
|
+
end_id_: 11,
|
|
398
|
+
properties_: {}
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
id_: 112,
|
|
402
|
+
label_: "BELONGS_TO",
|
|
403
|
+
start_id_: 8,
|
|
404
|
+
end_id_: 12,
|
|
405
|
+
properties_: {}
|
|
406
|
+
}
|
|
407
|
+
];
|
|
408
|
+
function createPlaygroundStores() {
|
|
409
|
+
const graphFilter = createGraphFilterStore();
|
|
410
|
+
const graphData = createGraphDataStore();
|
|
411
|
+
const selection = createSelectionStore();
|
|
412
|
+
const graphFocus = createGraphFocusStore();
|
|
413
|
+
const graphExploration = createGraphExplorationStore({
|
|
414
|
+
graphData,
|
|
415
|
+
selection,
|
|
416
|
+
graphFocus,
|
|
417
|
+
graphFilter,
|
|
418
|
+
// Static data -- no server expansion available in the playground
|
|
419
|
+
expandNode: async () => ({ nodes: [], edges: [], has_more: false }),
|
|
420
|
+
getNodes: async () => []
|
|
421
|
+
});
|
|
422
|
+
return { graphFilter, graphData, graphExploration, selection };
|
|
423
|
+
}
|
|
424
|
+
var PLAYGROUND_STORES = createPlaygroundStores();
|
|
425
|
+
var EMPTY_DATA = { nodes: [], edges: [] };
|
|
426
|
+
function serializeGraphData(data) {
|
|
427
|
+
return JSON.stringify(data, null, 2);
|
|
428
|
+
}
|
|
429
|
+
function validateNode(n, i) {
|
|
430
|
+
if (!n || typeof n !== "object") return `nodes[${i}] must be an object`;
|
|
431
|
+
const node = n;
|
|
432
|
+
if (node.id_ === void 0) return `nodes[${i}] is missing id_`;
|
|
433
|
+
if (typeof node.label_ !== "string") return `nodes[${i}] is missing label_`;
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
function validateEdge(e, i) {
|
|
437
|
+
if (!e || typeof e !== "object") return `edges[${i}] must be an object`;
|
|
438
|
+
const edge = e;
|
|
439
|
+
if (edge.id_ === void 0) return `edges[${i}] is missing id_`;
|
|
440
|
+
if (typeof edge.label_ !== "string") return `edges[${i}] is missing label_`;
|
|
441
|
+
if (edge.start_id_ === void 0) return `edges[${i}] is missing start_id_`;
|
|
442
|
+
if (edge.end_id_ === void 0) return `edges[${i}] is missing end_id_`;
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
function validateGraphInput(value) {
|
|
446
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
447
|
+
return "JSON must be an object with nodes and edges arrays";
|
|
448
|
+
}
|
|
449
|
+
const obj = value;
|
|
450
|
+
if (!Array.isArray(obj.nodes)) return '"nodes" must be an array';
|
|
451
|
+
if (!Array.isArray(obj.edges)) return '"edges" must be an array';
|
|
452
|
+
for (const [i, n] of obj.nodes.entries()) {
|
|
453
|
+
const err = validateNode(n, i);
|
|
454
|
+
if (err) return err;
|
|
455
|
+
}
|
|
456
|
+
for (const [i, e] of obj.edges.entries()) {
|
|
457
|
+
const err = validateEdge(e, i);
|
|
458
|
+
if (err) return err;
|
|
459
|
+
}
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
function loadIntoStore(stores, data) {
|
|
463
|
+
const labelCounts = {};
|
|
464
|
+
for (const n of data.nodes) {
|
|
465
|
+
labelCounts[n.label_] = (labelCounts[n.label_] ?? 0) + 1;
|
|
466
|
+
}
|
|
467
|
+
const palette = buildColorPalette(labelCounts);
|
|
468
|
+
const nodes = enrichNodes(data.nodes, palette);
|
|
469
|
+
const edges = enrichEdges(data.edges, palette);
|
|
470
|
+
stores.graphFilter.getState().resetFilters();
|
|
471
|
+
stores.graphData.getState().setQueryData(nodes, edges, {
|
|
472
|
+
nodes: nodes.length,
|
|
473
|
+
edges: edges.length
|
|
474
|
+
});
|
|
475
|
+
stores.graphFilter.getState().notifyDataChange();
|
|
476
|
+
}
|
|
477
|
+
var LIVE_STORE = { nodes: [], edges: [] };
|
|
478
|
+
function setLiveData(stores, data) {
|
|
479
|
+
LIVE_STORE.nodes = data.nodes.map((n) => ({ ...n }));
|
|
480
|
+
LIVE_STORE.edges = data.edges.map((e) => ({ ...e }));
|
|
481
|
+
loadIntoStore(stores, LIVE_STORE);
|
|
482
|
+
}
|
|
483
|
+
setLiveData(PLAYGROUND_STORES, { nodes: TECH_NODES, edges: TECH_EDGES });
|
|
484
|
+
var TECH_MODEL = [
|
|
485
|
+
{
|
|
486
|
+
title: "Product",
|
|
487
|
+
type: "object",
|
|
488
|
+
properties: { key: { type: "string" }, name: { type: "string" } },
|
|
489
|
+
required: ["key", "name"],
|
|
490
|
+
"x-graph": {
|
|
491
|
+
type: "NODE",
|
|
492
|
+
label: "Product",
|
|
493
|
+
relations: [
|
|
494
|
+
{
|
|
495
|
+
rel_type_name: "HAS_TECH_STACK",
|
|
496
|
+
to_type_name: "TechnologyStack",
|
|
497
|
+
cardinality: "ONE_TO_ONE",
|
|
498
|
+
form_field: true
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
// Fictitious second Product -> TechnologyStack relation: a product
|
|
502
|
+
// may also provision a stack it does not own (one-to-many).
|
|
503
|
+
rel_type_name: "PROVIDES_STACK_FOR",
|
|
504
|
+
to_type_name: "TechnologyStack",
|
|
505
|
+
cardinality: "ONE_TO_MANY",
|
|
506
|
+
form_field: true
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
rel_type_name: "INTEGRATES_WITH",
|
|
510
|
+
to_type_name: "Product",
|
|
511
|
+
cardinality: "ONE_TO_MANY",
|
|
512
|
+
form_field: true
|
|
513
|
+
}
|
|
514
|
+
]
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
title: "TechnologyStack",
|
|
519
|
+
type: "object",
|
|
520
|
+
properties: { key: { type: "string" }, name: { type: "string" } },
|
|
521
|
+
"x-graph": {
|
|
522
|
+
type: "NODE",
|
|
523
|
+
label: "TechnologyStack",
|
|
524
|
+
relations: [
|
|
525
|
+
{
|
|
526
|
+
rel_type_name: "USES_TECHNOLOGY",
|
|
527
|
+
to_type_name: "Technology",
|
|
528
|
+
cardinality: "ONE_TO_MANY",
|
|
529
|
+
form_field: true
|
|
530
|
+
}
|
|
531
|
+
]
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
title: "Technology",
|
|
536
|
+
type: "object",
|
|
537
|
+
properties: { key: { type: "string" }, name: { type: "string" } },
|
|
538
|
+
required: ["key", "name"],
|
|
539
|
+
"x-graph": {
|
|
540
|
+
type: "NODE",
|
|
541
|
+
label: "Technology",
|
|
542
|
+
relations: [
|
|
543
|
+
{
|
|
544
|
+
rel_type_name: "BELONGS_TO",
|
|
545
|
+
to_type_name: "TechnologyCategory",
|
|
546
|
+
cardinality: "ONE_TO_ONE",
|
|
547
|
+
form_field: true
|
|
548
|
+
}
|
|
549
|
+
]
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
title: "TechnologyCategory",
|
|
554
|
+
type: "object",
|
|
555
|
+
properties: { key: { type: "string" }, name: { type: "string" } },
|
|
556
|
+
required: ["key", "name"],
|
|
557
|
+
"x-graph": { type: "NODE", label: "TechnologyCategory" }
|
|
558
|
+
}
|
|
559
|
+
];
|
|
560
|
+
var ORG_MODEL = [
|
|
561
|
+
{
|
|
562
|
+
title: "Network",
|
|
563
|
+
type: "object",
|
|
564
|
+
"x-graph": { type: "NODE", label: "Network" }
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
title: "Hospital",
|
|
568
|
+
type: "object",
|
|
569
|
+
"x-graph": {
|
|
570
|
+
type: "NODE",
|
|
571
|
+
label: "Hospital",
|
|
572
|
+
relations: [
|
|
573
|
+
{
|
|
574
|
+
rel_type_name: "partOf",
|
|
575
|
+
to_type_name: "Network",
|
|
576
|
+
cardinality: "ONE_TO_ONE"
|
|
577
|
+
}
|
|
578
|
+
]
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
title: "Department",
|
|
583
|
+
type: "object",
|
|
584
|
+
"x-graph": {
|
|
585
|
+
type: "NODE",
|
|
586
|
+
label: "Department",
|
|
587
|
+
relations: [
|
|
588
|
+
{
|
|
589
|
+
rel_type_name: "partOf",
|
|
590
|
+
to_type_name: "Hospital",
|
|
591
|
+
cardinality: "ONE_TO_ONE"
|
|
592
|
+
}
|
|
593
|
+
]
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
title: "Practice",
|
|
598
|
+
type: "object",
|
|
599
|
+
"x-graph": {
|
|
600
|
+
type: "NODE",
|
|
601
|
+
label: "Practice",
|
|
602
|
+
relations: [
|
|
603
|
+
{
|
|
604
|
+
rel_type_name: "partOf",
|
|
605
|
+
to_type_name: "Network",
|
|
606
|
+
cardinality: "ONE_TO_ONE"
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
rel_type_name: "affiliation",
|
|
610
|
+
to_type_name: "Hospital",
|
|
611
|
+
cardinality: "ONE_TO_MANY"
|
|
612
|
+
}
|
|
613
|
+
]
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
// Pre-defined relation-type catalog. With edgeTypes "catalog", relations may
|
|
617
|
+
// only use these types; no new edge types can be created.
|
|
618
|
+
{
|
|
619
|
+
title: "partOf",
|
|
620
|
+
type: "object",
|
|
621
|
+
"x-graph": { type: "EDGE", label: "partOf" }
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
title: "affiliation",
|
|
625
|
+
type: "object",
|
|
626
|
+
"x-graph": { type: "EDGE", label: "affiliation" }
|
|
627
|
+
}
|
|
628
|
+
];
|
|
629
|
+
var ORG_NODES = [
|
|
630
|
+
{
|
|
631
|
+
id_: 1,
|
|
632
|
+
label_: "Network",
|
|
633
|
+
properties_: { name: "Northwind Care Network" }
|
|
634
|
+
},
|
|
635
|
+
{ id_: 2, label_: "Hospital", properties_: { name: "Riverside General" } },
|
|
636
|
+
{ id_: 3, label_: "Hospital", properties_: { name: "Lakeside Clinic" } },
|
|
637
|
+
{ id_: 4, label_: "Department", properties_: { name: "Cardiology" } },
|
|
638
|
+
{ id_: 5, label_: "Department", properties_: { name: "Radiology" } },
|
|
639
|
+
{ id_: 6, label_: "Practice", properties_: { name: "Maple Street GP" } }
|
|
640
|
+
];
|
|
641
|
+
var ORG_EDGES = [
|
|
642
|
+
// partOf: child -> parent (containment).
|
|
643
|
+
{ id_: 201, label_: "partOf", start_id_: 2, end_id_: 1, properties_: {} },
|
|
644
|
+
{ id_: 202, label_: "partOf", start_id_: 3, end_id_: 1, properties_: {} },
|
|
645
|
+
{ id_: 203, label_: "partOf", start_id_: 4, end_id_: 2, properties_: {} },
|
|
646
|
+
{ id_: 204, label_: "partOf", start_id_: 5, end_id_: 2, properties_: {} },
|
|
647
|
+
// affiliation: a practice affiliated with a hospital (non-hierarchical).
|
|
648
|
+
{
|
|
649
|
+
id_: 205,
|
|
650
|
+
label_: "affiliation",
|
|
651
|
+
start_id_: 6,
|
|
652
|
+
end_id_: 2,
|
|
653
|
+
properties_: {}
|
|
654
|
+
}
|
|
655
|
+
];
|
|
656
|
+
var DATASETS = {
|
|
657
|
+
empty: {
|
|
658
|
+
label: "Empty",
|
|
659
|
+
data: EMPTY_DATA,
|
|
660
|
+
model: [],
|
|
661
|
+
capabilities: { readOnly: false, capabilities: {} },
|
|
662
|
+
// No model to drive forms, so editing stays off.
|
|
663
|
+
editCapabilities: {}
|
|
664
|
+
},
|
|
665
|
+
techStack: {
|
|
666
|
+
label: "Technology Stack",
|
|
667
|
+
data: { nodes: TECH_NODES, edges: TECH_EDGES },
|
|
668
|
+
model: TECH_MODEL,
|
|
669
|
+
capabilities: { readOnly: false, capabilities: {} },
|
|
670
|
+
// Full instance editing: create / edit / delete nodes and edges.
|
|
671
|
+
editCapabilities: {
|
|
672
|
+
createNode: "edit",
|
|
673
|
+
updateNode: "edit",
|
|
674
|
+
deleteNode: "edit",
|
|
675
|
+
createEdge: "edit",
|
|
676
|
+
updateEdge: "edit",
|
|
677
|
+
deleteEdge: "edit"
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
organization: {
|
|
681
|
+
label: "Organization model",
|
|
682
|
+
data: { nodes: ORG_NODES, edges: ORG_EDGES },
|
|
683
|
+
model: ORG_MODEL,
|
|
684
|
+
// Simulates the planned Sestina org editor: fixed edge-type catalog (no new
|
|
685
|
+
// edge types) and no property editing (node/edge properties hidden).
|
|
686
|
+
capabilities: {
|
|
687
|
+
readOnly: false,
|
|
688
|
+
capabilities: {
|
|
689
|
+
edgeTypes: "catalog",
|
|
690
|
+
nodeProperties: "hidden",
|
|
691
|
+
edgeProperties: "hidden"
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
// A constrained instance-editing profile: edges may be linked / unlinked
|
|
695
|
+
// (partOf / affiliation), but nodes are not created or deleted here.
|
|
696
|
+
editCapabilities: {
|
|
697
|
+
updateNode: "edit",
|
|
698
|
+
createEdge: "edit",
|
|
699
|
+
deleteEdge: "edit"
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
var DATASET_OPTIONS = Object.entries(DATASETS).map(([value, d]) => ({
|
|
704
|
+
value,
|
|
705
|
+
label: d.label
|
|
706
|
+
}));
|
|
707
|
+
var DEFAULT_DATASET = "techStack";
|
|
708
|
+
function useModelState(initialModel) {
|
|
709
|
+
const [modelText, setModelText] = useState(
|
|
710
|
+
() => JSON.stringify(initialModel, null, 2)
|
|
711
|
+
);
|
|
712
|
+
const [model, setModel] = useState(initialModel);
|
|
713
|
+
const [modelError, setModelError] = useState(null);
|
|
714
|
+
const applyModel = useCallback(() => {
|
|
715
|
+
let parsed;
|
|
716
|
+
try {
|
|
717
|
+
parsed = JSON.parse(modelText);
|
|
718
|
+
} catch (e) {
|
|
719
|
+
setModelError(e instanceof Error ? e.message : "Invalid JSON");
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (!Array.isArray(parsed)) {
|
|
723
|
+
setModelError("A graph model must be a JSON array of type schemas.");
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
setModelError(null);
|
|
727
|
+
setModel(parsed);
|
|
728
|
+
}, [modelText]);
|
|
729
|
+
const loadModel = useCallback((next) => {
|
|
730
|
+
setModelText(JSON.stringify(next, null, 2));
|
|
731
|
+
setModel(next);
|
|
732
|
+
setModelError(null);
|
|
733
|
+
}, []);
|
|
734
|
+
const updateModel = useCallback((next) => {
|
|
735
|
+
setModel(next);
|
|
736
|
+
setModelText(JSON.stringify(next, null, 2));
|
|
737
|
+
setModelError(null);
|
|
738
|
+
}, []);
|
|
739
|
+
return {
|
|
740
|
+
model,
|
|
741
|
+
modelText,
|
|
742
|
+
modelError,
|
|
743
|
+
setModelText,
|
|
744
|
+
applyModel,
|
|
745
|
+
loadModel,
|
|
746
|
+
updateModel
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function ModelGraphTab({
|
|
750
|
+
model,
|
|
751
|
+
onChange,
|
|
752
|
+
readOnly,
|
|
753
|
+
capabilities
|
|
754
|
+
}) {
|
|
755
|
+
return /* @__PURE__ */ jsx2("div", { className: "h-[640px] rounded-lg border overflow-hidden", children: /* @__PURE__ */ jsx2(
|
|
756
|
+
GraphModelEditor,
|
|
757
|
+
{
|
|
758
|
+
value: model,
|
|
759
|
+
onChange,
|
|
760
|
+
readOnly,
|
|
761
|
+
capabilities,
|
|
762
|
+
widgetOptions: ["entity-select", "coded-value", "textarea"]
|
|
763
|
+
}
|
|
764
|
+
) });
|
|
765
|
+
}
|
|
766
|
+
function CapabilitiesTab({
|
|
767
|
+
config,
|
|
768
|
+
onApply
|
|
769
|
+
}) {
|
|
770
|
+
const [text, setText] = useState(() => JSON.stringify(config, null, 2));
|
|
771
|
+
const [error, setError] = useState(null);
|
|
772
|
+
const apply = useCallback(() => {
|
|
773
|
+
let parsed;
|
|
774
|
+
try {
|
|
775
|
+
parsed = JSON.parse(text);
|
|
776
|
+
} catch (e) {
|
|
777
|
+
setError(e instanceof Error ? e.message : "Invalid JSON");
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
setError(null);
|
|
781
|
+
const obj = parsed;
|
|
782
|
+
onApply({
|
|
783
|
+
readOnly: Boolean(obj.readOnly),
|
|
784
|
+
capabilities: obj.capabilities ?? {}
|
|
785
|
+
});
|
|
786
|
+
}, [text, onApply]);
|
|
787
|
+
const reset = useCallback(() => {
|
|
788
|
+
const fresh = { readOnly: false, capabilities: {} };
|
|
789
|
+
setText(JSON.stringify(fresh, null, 2));
|
|
790
|
+
setError(null);
|
|
791
|
+
onApply(fresh);
|
|
792
|
+
}, [onApply]);
|
|
793
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
794
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
795
|
+
/* @__PURE__ */ jsxs(Button, { size: "sm", onClick: apply, children: [
|
|
796
|
+
/* @__PURE__ */ jsx2(Play, { className: "h-4 w-4 mr-1" }),
|
|
797
|
+
"Apply"
|
|
798
|
+
] }),
|
|
799
|
+
/* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", onClick: reset, children: [
|
|
800
|
+
/* @__PURE__ */ jsx2(RotateCcw, { className: "h-4 w-4 mr-1" }),
|
|
801
|
+
"Reset"
|
|
802
|
+
] }),
|
|
803
|
+
/* @__PURE__ */ jsx2("span", { className: "text-xs text-muted-foreground ml-2", children: "Edit, Apply, then open the Model Editor tab and select a node." })
|
|
804
|
+
] }),
|
|
805
|
+
error && /* @__PURE__ */ jsx2(Alert, { variant: "destructive", children: /* @__PURE__ */ jsx2(AlertDescription, { children: error }) }),
|
|
806
|
+
/* @__PURE__ */ jsx2(
|
|
807
|
+
"textarea",
|
|
808
|
+
{
|
|
809
|
+
value: text,
|
|
810
|
+
onChange: (e) => setText(e.target.value),
|
|
811
|
+
spellCheck: false,
|
|
812
|
+
className: "w-full h-[280px] font-mono text-xs p-3 border rounded-lg bg-muted/30 resize-y focus:outline-none focus:ring-2 focus:ring-ring"
|
|
813
|
+
}
|
|
814
|
+
),
|
|
815
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
816
|
+
/* @__PURE__ */ jsx2("p", { className: "text-xs font-medium text-muted-foreground mb-1", children: "Effective config (applied to GraphModelEditor)" }),
|
|
817
|
+
/* @__PURE__ */ jsx2("pre", { className: "rounded-md bg-muted px-3 py-2 text-xs overflow-auto max-h-40", children: JSON.stringify(config, null, 2) }),
|
|
818
|
+
/* @__PURE__ */ jsx2("p", { className: "mt-2 text-xs text-muted-foreground", children: "readOnly = lock everything. capabilities: nodeTypes / relations (edit | readonly), edgeTypes (edit = select or create | catalog = select only | readonly), nodeProperties / edgeProperties (edit | readonly | hidden), showEdgeTypes (bool, default false starts edge-type nodes deselected; true shows them). More fields are added as the editor grows." })
|
|
819
|
+
] })
|
|
820
|
+
] });
|
|
821
|
+
}
|
|
822
|
+
function ModelJsonTab({
|
|
823
|
+
state,
|
|
824
|
+
onReset
|
|
825
|
+
}) {
|
|
826
|
+
const { modelText, modelError, setModelText, applyModel } = state;
|
|
827
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
828
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
829
|
+
/* @__PURE__ */ jsxs(Button, { size: "sm", onClick: applyModel, children: [
|
|
830
|
+
/* @__PURE__ */ jsx2(Play, { className: "h-4 w-4 mr-1" }),
|
|
831
|
+
"Apply"
|
|
832
|
+
] }),
|
|
833
|
+
/* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", onClick: onReset, children: [
|
|
834
|
+
/* @__PURE__ */ jsx2(RotateCcw, { className: "h-4 w-4 mr-1" }),
|
|
835
|
+
"Reset"
|
|
836
|
+
] }),
|
|
837
|
+
/* @__PURE__ */ jsx2("span", { className: "text-xs text-muted-foreground ml-2", children: "Edit the model, Apply, then switch to the Model tab" })
|
|
838
|
+
] }),
|
|
839
|
+
modelError && /* @__PURE__ */ jsx2(Alert, { variant: "destructive", children: /* @__PURE__ */ jsx2(AlertDescription, { children: modelError }) }),
|
|
840
|
+
/* @__PURE__ */ jsx2(
|
|
841
|
+
"textarea",
|
|
842
|
+
{
|
|
843
|
+
value: modelText,
|
|
844
|
+
onChange: (e) => setModelText(e.target.value),
|
|
845
|
+
spellCheck: false,
|
|
846
|
+
className: "w-full h-[600px] font-mono text-xs p-3 border rounded-lg bg-muted/30 resize-y focus:outline-none focus:ring-2 focus:ring-ring"
|
|
847
|
+
}
|
|
848
|
+
)
|
|
849
|
+
] });
|
|
850
|
+
}
|
|
851
|
+
function PlayGraph() {
|
|
852
|
+
const [datasetKey, setDatasetKey] = useState(DEFAULT_DATASET);
|
|
853
|
+
const [jsonText, setJsonText] = useState(
|
|
854
|
+
() => serializeGraphData(DATASETS[DEFAULT_DATASET].data)
|
|
855
|
+
);
|
|
856
|
+
const [parseError, setParseError] = useState(null);
|
|
857
|
+
const modelState = useModelState(DATASETS[DEFAULT_DATASET].model);
|
|
858
|
+
const { loadModel } = modelState;
|
|
859
|
+
const [capsConfig, setCapsConfig] = useState(
|
|
860
|
+
DATASETS[DEFAULT_DATASET].capabilities
|
|
861
|
+
);
|
|
862
|
+
const editPort = useMemo(
|
|
863
|
+
() => createInMemoryPort(
|
|
864
|
+
LIVE_STORE,
|
|
865
|
+
() => loadIntoStore(PLAYGROUND_STORES, LIVE_STORE)
|
|
866
|
+
),
|
|
867
|
+
[]
|
|
868
|
+
);
|
|
869
|
+
const editSchemaProvider = useMemo(
|
|
870
|
+
() => createModelSchemaProvider(modelState.model),
|
|
871
|
+
[modelState.model]
|
|
872
|
+
);
|
|
873
|
+
const handleMutated = useCallback(() => {
|
|
874
|
+
setJsonText(serializeGraphData(LIVE_STORE));
|
|
875
|
+
}, []);
|
|
876
|
+
const editWidgets = useMemo(
|
|
877
|
+
() => ({
|
|
878
|
+
...DEFAULT_WIDGETS,
|
|
879
|
+
"entity-select": createRelationWidget(LIVE_STORE)
|
|
880
|
+
}),
|
|
881
|
+
[]
|
|
882
|
+
);
|
|
883
|
+
const loadDataset = useCallback(
|
|
884
|
+
(key) => {
|
|
885
|
+
const ds = DATASETS[key];
|
|
886
|
+
if (!ds) return;
|
|
887
|
+
setDatasetKey(key);
|
|
888
|
+
setJsonText(serializeGraphData(ds.data));
|
|
889
|
+
setParseError(null);
|
|
890
|
+
setLiveData(PLAYGROUND_STORES, ds.data);
|
|
891
|
+
loadModel(ds.model);
|
|
892
|
+
setCapsConfig(ds.capabilities);
|
|
893
|
+
},
|
|
894
|
+
[loadModel]
|
|
895
|
+
);
|
|
896
|
+
const [panelWidth, setPanelWidth] = useState(300);
|
|
897
|
+
const rowRef = useRef(null);
|
|
898
|
+
const startResize = useCallback((e) => {
|
|
899
|
+
e.preventDefault();
|
|
900
|
+
const target = e.currentTarget;
|
|
901
|
+
target.setPointerCapture(e.pointerId);
|
|
902
|
+
const move = (ev) => {
|
|
903
|
+
const right = rowRef.current?.getBoundingClientRect().right ?? 0;
|
|
904
|
+
setPanelWidth(Math.min(560, Math.max(200, right - ev.clientX)));
|
|
905
|
+
};
|
|
906
|
+
const up = () => {
|
|
907
|
+
window.removeEventListener("pointermove", move);
|
|
908
|
+
window.removeEventListener("pointerup", up);
|
|
909
|
+
};
|
|
910
|
+
window.addEventListener("pointermove", move);
|
|
911
|
+
window.addEventListener("pointerup", up);
|
|
912
|
+
}, []);
|
|
913
|
+
const handleApply = useCallback(() => {
|
|
914
|
+
let parsed;
|
|
915
|
+
try {
|
|
916
|
+
parsed = JSON.parse(jsonText);
|
|
917
|
+
} catch (e) {
|
|
918
|
+
setParseError(e instanceof Error ? e.message : "Invalid JSON");
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const validationError = validateGraphInput(parsed);
|
|
922
|
+
if (validationError) {
|
|
923
|
+
setParseError(validationError);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
setParseError(null);
|
|
927
|
+
setLiveData(PLAYGROUND_STORES, parsed);
|
|
928
|
+
}, [jsonText]);
|
|
929
|
+
const handleReset = useCallback(() => {
|
|
930
|
+
const data = DATASETS[datasetKey].data;
|
|
931
|
+
setJsonText(serializeGraphData(data));
|
|
932
|
+
setParseError(null);
|
|
933
|
+
setLiveData(PLAYGROUND_STORES, data);
|
|
934
|
+
}, [datasetKey]);
|
|
935
|
+
const handleResetModel = useCallback(() => {
|
|
936
|
+
loadModel(DATASETS[datasetKey].model);
|
|
937
|
+
}, [datasetKey, loadModel]);
|
|
938
|
+
const handleKeyDown = useCallback(
|
|
939
|
+
(e) => {
|
|
940
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
941
|
+
e.preventDefault();
|
|
942
|
+
handleApply();
|
|
943
|
+
}
|
|
944
|
+
},
|
|
945
|
+
[handleApply]
|
|
946
|
+
);
|
|
947
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
948
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
949
|
+
/* @__PURE__ */ jsx2("h1", { children: "Graph Visualization" }),
|
|
950
|
+
/* @__PURE__ */ jsx2("p", { className: "text-sm text-muted-foreground mt-1", children: "Interactive property graph with dual renderer support. Edit the JSON data and apply to re-render. The Model tab renders a graph model (metamodel) with GraphModelEditor." })
|
|
951
|
+
] }),
|
|
952
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
953
|
+
/* @__PURE__ */ jsx2(
|
|
954
|
+
FormSelect2,
|
|
955
|
+
{
|
|
956
|
+
label: "Dataset",
|
|
957
|
+
options: DATASET_OPTIONS,
|
|
958
|
+
value: datasetKey,
|
|
959
|
+
onChange: (v) => v && loadDataset(v),
|
|
960
|
+
clearable: false,
|
|
961
|
+
wrapperClassName: "w-64"
|
|
962
|
+
}
|
|
963
|
+
),
|
|
964
|
+
/* @__PURE__ */ jsx2("span", { className: "text-xs text-muted-foreground mt-5", children: "A dataset bundles graph data + model + capability profile." })
|
|
965
|
+
] }),
|
|
966
|
+
/* @__PURE__ */ jsx2(GraphStoresContext.Provider, { value: PLAYGROUND_STORES, children: /* @__PURE__ */ jsxs(Tabs, { defaultValue: "graph", className: "space-y-3", children: [
|
|
967
|
+
/* @__PURE__ */ jsxs(TabsList, { children: [
|
|
968
|
+
/* @__PURE__ */ jsx2(TabsTrigger, { value: "graph", children: "Graph" }),
|
|
969
|
+
/* @__PURE__ */ jsx2(TabsTrigger, { value: "data", children: "Data JSON" }),
|
|
970
|
+
/* @__PURE__ */ jsx2(TabsTrigger, { value: "model", children: "Model Editor" }),
|
|
971
|
+
/* @__PURE__ */ jsx2(TabsTrigger, { value: "model-json", children: "Model JSON" }),
|
|
972
|
+
/* @__PURE__ */ jsx2(TabsTrigger, { value: "model-capabilities", children: "Model Editor Capabilities" })
|
|
973
|
+
] }),
|
|
974
|
+
/* @__PURE__ */ jsx2(TabsContent, { value: "graph", className: "mt-0", children: /* @__PURE__ */ jsx2(GraphRendererProvider, { defaultRenderer: "cytoscape", children: /* @__PURE__ */ jsxs("div", { ref: rowRef, className: "flex overflow-hidden h-[600px]", children: [
|
|
975
|
+
/* @__PURE__ */ jsx2("div", { className: "flex-1 min-w-0", children: /* @__PURE__ */ jsx2(GraphVisualizer, {}) }),
|
|
976
|
+
/* @__PURE__ */ jsx2(
|
|
977
|
+
ResizeHandle,
|
|
978
|
+
{
|
|
979
|
+
orientation: "vertical",
|
|
980
|
+
"aria-valuenow": panelWidth,
|
|
981
|
+
"aria-valuemin": 200,
|
|
982
|
+
"aria-valuemax": 560,
|
|
983
|
+
onPointerDown: startResize,
|
|
984
|
+
onDoubleClick: () => setPanelWidth(300)
|
|
985
|
+
}
|
|
986
|
+
),
|
|
987
|
+
/* @__PURE__ */ jsxs(
|
|
988
|
+
"div",
|
|
989
|
+
{
|
|
990
|
+
style: { width: panelWidth },
|
|
991
|
+
className: "flex-shrink-0 flex flex-col min-h-0",
|
|
992
|
+
children: [
|
|
993
|
+
/* @__PURE__ */ jsx2("div", { className: "flex-1 min-h-0 overflow-hidden", children: /* @__PURE__ */ jsx2(
|
|
994
|
+
ActionPanel,
|
|
995
|
+
{
|
|
996
|
+
actions: /* @__PURE__ */ jsx2(
|
|
997
|
+
GraphEditToolbar,
|
|
998
|
+
{
|
|
999
|
+
port: editPort,
|
|
1000
|
+
schemaProvider: editSchemaProvider,
|
|
1001
|
+
capabilities: DATASETS[datasetKey].editCapabilities,
|
|
1002
|
+
widgets: editWidgets,
|
|
1003
|
+
onMutated: handleMutated
|
|
1004
|
+
}
|
|
1005
|
+
)
|
|
1006
|
+
}
|
|
1007
|
+
) }),
|
|
1008
|
+
/* @__PURE__ */ jsxs("div", { className: "shrink-0 px-4 pb-4 pt-2", children: [
|
|
1009
|
+
/* @__PURE__ */ jsx2("p", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2", children: "Accessible list (demo)" }),
|
|
1010
|
+
/* @__PURE__ */ jsx2(GraphA11yList, { visible: true, maxHeight: 160 })
|
|
1011
|
+
] })
|
|
1012
|
+
]
|
|
1013
|
+
}
|
|
1014
|
+
)
|
|
1015
|
+
] }) }) }),
|
|
1016
|
+
/* @__PURE__ */ jsx2(TabsContent, { value: "data", className: "mt-0", children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
1017
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1018
|
+
/* @__PURE__ */ jsxs(Button, { size: "sm", onClick: handleApply, children: [
|
|
1019
|
+
/* @__PURE__ */ jsx2(Play, { className: "h-4 w-4 mr-1" }),
|
|
1020
|
+
"Apply"
|
|
1021
|
+
] }),
|
|
1022
|
+
/* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", onClick: handleReset, children: [
|
|
1023
|
+
/* @__PURE__ */ jsx2(RotateCcw, { className: "h-4 w-4 mr-1" }),
|
|
1024
|
+
"Reset"
|
|
1025
|
+
] }),
|
|
1026
|
+
/* @__PURE__ */ jsx2("span", { className: "text-xs text-muted-foreground ml-2", children: "Cmd+Enter to apply" })
|
|
1027
|
+
] }),
|
|
1028
|
+
parseError && /* @__PURE__ */ jsx2(Alert, { variant: "destructive", children: /* @__PURE__ */ jsx2(AlertDescription, { children: parseError }) }),
|
|
1029
|
+
/* @__PURE__ */ jsx2(
|
|
1030
|
+
"textarea",
|
|
1031
|
+
{
|
|
1032
|
+
value: jsonText,
|
|
1033
|
+
onChange: (e) => setJsonText(e.target.value),
|
|
1034
|
+
onKeyDown: handleKeyDown,
|
|
1035
|
+
spellCheck: false,
|
|
1036
|
+
className: "w-full h-[600px] font-mono text-xs p-3 border rounded-lg bg-muted/30 resize-y focus:outline-none focus:ring-2 focus:ring-ring"
|
|
1037
|
+
}
|
|
1038
|
+
)
|
|
1039
|
+
] }) }),
|
|
1040
|
+
/* @__PURE__ */ jsx2(TabsContent, { value: "model", className: "mt-0", children: /* @__PURE__ */ jsx2(
|
|
1041
|
+
ModelGraphTab,
|
|
1042
|
+
{
|
|
1043
|
+
model: modelState.model,
|
|
1044
|
+
onChange: modelState.updateModel,
|
|
1045
|
+
readOnly: capsConfig.readOnly,
|
|
1046
|
+
capabilities: capsConfig.capabilities
|
|
1047
|
+
}
|
|
1048
|
+
) }),
|
|
1049
|
+
/* @__PURE__ */ jsx2(TabsContent, { value: "model-json", className: "mt-0", children: /* @__PURE__ */ jsx2(ModelJsonTab, { state: modelState, onReset: handleResetModel }) }),
|
|
1050
|
+
/* @__PURE__ */ jsx2(TabsContent, { value: "model-capabilities", className: "mt-0", children: /* @__PURE__ */ jsx2(CapabilitiesTab, { config: capsConfig, onApply: setCapsConfig }) })
|
|
1051
|
+
] }) })
|
|
1052
|
+
] });
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
export {
|
|
1056
|
+
PlayGraph
|
|
1057
|
+
};
|
|
1058
|
+
//# sourceMappingURL=chunk-C2P3UQMZ.js.map
|