infrahub-schema-visualizer 0.0.1
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 +190 -0
- package/README.md +121 -0
- package/dist/webview/schema-visualizer.css +1 -0
- package/dist/webview/schema-visualizer.js +17 -0
- package/index.ts +38 -0
- package/package.json +66 -0
- package/src/components/BottomToolbar.tsx +195 -0
- package/src/components/EdgeContextMenu.tsx +134 -0
- package/src/components/FilterPanel.tsx +291 -0
- package/src/components/FloatingEdge.tsx +231 -0
- package/src/components/LegendPanel.tsx +191 -0
- package/src/components/NodeContextMenu.tsx +114 -0
- package/src/components/NodeDetailsPanel.tsx +200 -0
- package/src/components/SchemaNode.tsx +260 -0
- package/src/components/SchemaVisualizer.tsx +1027 -0
- package/src/components/index.ts +12 -0
- package/src/types/index.ts +1 -0
- package/src/types/schema.ts +73 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/index.ts +20 -0
- package/src/utils/layout.ts +60 -0
- package/src/utils/persistence.ts +69 -0
- package/src/utils/schema-to-flow.ts +623 -0
- package/src/webview-entry.tsx +94 -0
- package/src/webview.css +152 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
import type { Edge, Node } from "@xyflow/react";
|
|
2
|
+
import type {
|
|
3
|
+
GenericSchema,
|
|
4
|
+
NodeSchema,
|
|
5
|
+
ProfileSchema,
|
|
6
|
+
SchemaVisualizerData,
|
|
7
|
+
TemplateSchema,
|
|
8
|
+
} from "../types/schema";
|
|
9
|
+
|
|
10
|
+
export interface SchemaNodeData extends Record<string, unknown> {
|
|
11
|
+
kind: string;
|
|
12
|
+
label: string;
|
|
13
|
+
namespace: string;
|
|
14
|
+
description?: string | null;
|
|
15
|
+
icon?: string | null;
|
|
16
|
+
attributes: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
kind: string;
|
|
19
|
+
label?: string | null;
|
|
20
|
+
optional?: boolean;
|
|
21
|
+
inherited?: boolean;
|
|
22
|
+
}>;
|
|
23
|
+
relationships: Array<{
|
|
24
|
+
name: string;
|
|
25
|
+
peer: string;
|
|
26
|
+
cardinality: "one" | "many";
|
|
27
|
+
label?: string | null;
|
|
28
|
+
inherited?: boolean;
|
|
29
|
+
}>;
|
|
30
|
+
inheritFrom?: string[];
|
|
31
|
+
schemaType: "node" | "generic" | "profile" | "template";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type SchemaFlowNode = Node<SchemaNodeData, "schemaNode">;
|
|
35
|
+
|
|
36
|
+
export interface SchemaFlowData {
|
|
37
|
+
nodes: SchemaFlowNode[];
|
|
38
|
+
edges: Edge[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the kind identifier for a schema
|
|
43
|
+
*/
|
|
44
|
+
export function getSchemaKind(
|
|
45
|
+
schema: NodeSchema | GenericSchema | ProfileSchema | TemplateSchema,
|
|
46
|
+
): string {
|
|
47
|
+
return schema.kind ?? `${schema.namespace}${schema.name}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Find generics that a node inherits from
|
|
52
|
+
*/
|
|
53
|
+
function findInheritedGenerics(
|
|
54
|
+
node: NodeSchema,
|
|
55
|
+
genericsMap: Map<string, GenericSchema>,
|
|
56
|
+
): string[] {
|
|
57
|
+
if (!node.inherit_from || node.inherit_from.length === 0) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return node.inherit_from.filter((kind) => genericsMap.has(kind));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Converts schema data to React Flow nodes and edges.
|
|
66
|
+
* Only nodes are rendered - generics are shown through inheritance indicators.
|
|
67
|
+
*/
|
|
68
|
+
export function schemaToFlow(
|
|
69
|
+
data: SchemaVisualizerData,
|
|
70
|
+
options: {
|
|
71
|
+
nodeSpacing?: number;
|
|
72
|
+
rowSize?: number;
|
|
73
|
+
} = {},
|
|
74
|
+
): SchemaFlowData {
|
|
75
|
+
const { nodeSpacing = 350, rowSize = 4 } = options;
|
|
76
|
+
|
|
77
|
+
const nodes: SchemaFlowNode[] = [];
|
|
78
|
+
const edges: Edge[] = [];
|
|
79
|
+
|
|
80
|
+
// Create a map of generics for quick lookup
|
|
81
|
+
const genericsMap = new Map<string, GenericSchema>();
|
|
82
|
+
for (const generic of data.generics) {
|
|
83
|
+
const kind = getSchemaKind(generic);
|
|
84
|
+
genericsMap.set(kind, generic);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Create a map of all nodes for relationship edge creation
|
|
88
|
+
const nodeKinds = new Set<string>();
|
|
89
|
+
for (const node of data.nodes) {
|
|
90
|
+
nodeKinds.add(getSchemaKind(node));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Create a map of nodes to their relationships for quick lookup
|
|
94
|
+
const nodeRelationshipsMap = new Map<string, Set<string>>();
|
|
95
|
+
for (const node of data.nodes) {
|
|
96
|
+
const relNames = new Set<string>();
|
|
97
|
+
for (const rel of node.relationships ?? []) {
|
|
98
|
+
relNames.add(rel.name);
|
|
99
|
+
}
|
|
100
|
+
nodeRelationshipsMap.set(getSchemaKind(node), relNames);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Create a map of generic kind -> nodes that inherit from it
|
|
104
|
+
const genericToInheritingNodes = new Map<string, string[]>();
|
|
105
|
+
for (const node of data.nodes) {
|
|
106
|
+
for (const inheritedKind of node.inherit_from ?? []) {
|
|
107
|
+
if (genericsMap.has(inheritedKind)) {
|
|
108
|
+
if (!genericToInheritingNodes.has(inheritedKind)) {
|
|
109
|
+
genericToInheritingNodes.set(inheritedKind, []);
|
|
110
|
+
}
|
|
111
|
+
genericToInheritingNodes.get(inheritedKind)?.push(getSchemaKind(node));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Process nodes
|
|
117
|
+
data.nodes.forEach((node, index) => {
|
|
118
|
+
const kind = getSchemaKind(node);
|
|
119
|
+
const inheritedGenerics = findInheritedGenerics(node, genericsMap);
|
|
120
|
+
|
|
121
|
+
// Calculate position in grid layout
|
|
122
|
+
const col = index % rowSize;
|
|
123
|
+
const row = Math.floor(index / rowSize);
|
|
124
|
+
|
|
125
|
+
const flowNode: SchemaFlowNode = {
|
|
126
|
+
id: kind,
|
|
127
|
+
type: "schemaNode",
|
|
128
|
+
position: {
|
|
129
|
+
x: col * nodeSpacing,
|
|
130
|
+
y: row * nodeSpacing,
|
|
131
|
+
},
|
|
132
|
+
data: {
|
|
133
|
+
kind,
|
|
134
|
+
label: node.label ?? node.name,
|
|
135
|
+
namespace: node.namespace,
|
|
136
|
+
description: node.description,
|
|
137
|
+
icon: node.icon,
|
|
138
|
+
attributes: (node.attributes ?? []).map((attr) => ({
|
|
139
|
+
name: attr.name,
|
|
140
|
+
kind: attr.kind,
|
|
141
|
+
label: attr.label,
|
|
142
|
+
optional: attr.optional,
|
|
143
|
+
inherited: attr.inherited,
|
|
144
|
+
})),
|
|
145
|
+
relationships: (node.relationships ?? []).map((rel) => ({
|
|
146
|
+
name: rel.name,
|
|
147
|
+
peer: rel.peer,
|
|
148
|
+
cardinality: rel.cardinality,
|
|
149
|
+
label: rel.label,
|
|
150
|
+
inherited: rel.inherited,
|
|
151
|
+
})),
|
|
152
|
+
inheritFrom: inheritedGenerics,
|
|
153
|
+
schemaType: "node",
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
nodes.push(flowNode);
|
|
158
|
+
|
|
159
|
+
// Create edges for relationships
|
|
160
|
+
for (const rel of node.relationships ?? []) {
|
|
161
|
+
// Check if the peer is a node
|
|
162
|
+
if (nodeKinds.has(rel.peer)) {
|
|
163
|
+
// Check if target node has a matching relationship (for rel-to-rel connection)
|
|
164
|
+
const targetRelationships = nodeRelationshipsMap.get(rel.peer);
|
|
165
|
+
const hasMatchingRelationship = targetRelationships?.has(rel.name);
|
|
166
|
+
|
|
167
|
+
edges.push({
|
|
168
|
+
id: `${kind}-${rel.name}-${rel.peer}`,
|
|
169
|
+
source: kind,
|
|
170
|
+
target: rel.peer,
|
|
171
|
+
sourceHandle: `rel-${rel.name}-right`,
|
|
172
|
+
targetHandle: hasMatchingRelationship
|
|
173
|
+
? `rel-${rel.name}-left`
|
|
174
|
+
: "node-target",
|
|
175
|
+
label: rel.label ?? rel.name,
|
|
176
|
+
type: "smoothstep",
|
|
177
|
+
animated: rel.cardinality === "many",
|
|
178
|
+
style: {
|
|
179
|
+
stroke: rel.inherited ? "#9ca3af" : "#6366f1",
|
|
180
|
+
strokeWidth: 2,
|
|
181
|
+
},
|
|
182
|
+
labelStyle: {
|
|
183
|
+
fontSize: 10,
|
|
184
|
+
fill: "#374151",
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// Check if the peer is a generic - create edges to all inheriting nodes
|
|
189
|
+
else if (genericsMap.has(rel.peer)) {
|
|
190
|
+
const inheritingNodes = genericToInheritingNodes.get(rel.peer) ?? [];
|
|
191
|
+
for (const inheritingNodeKind of inheritingNodes) {
|
|
192
|
+
// Check if inheriting node has a matching relationship
|
|
193
|
+
const targetRelationships =
|
|
194
|
+
nodeRelationshipsMap.get(inheritingNodeKind);
|
|
195
|
+
const hasMatchingRelationship = targetRelationships?.has(rel.name);
|
|
196
|
+
|
|
197
|
+
edges.push({
|
|
198
|
+
id: `${kind}-${rel.name}-${inheritingNodeKind}`,
|
|
199
|
+
source: kind,
|
|
200
|
+
target: inheritingNodeKind,
|
|
201
|
+
sourceHandle: `rel-${rel.name}-right`,
|
|
202
|
+
targetHandle: hasMatchingRelationship
|
|
203
|
+
? `rel-${rel.name}-left`
|
|
204
|
+
: "node-target",
|
|
205
|
+
label: `${rel.label ?? rel.name} (via ${rel.peer})`,
|
|
206
|
+
type: "smoothstep",
|
|
207
|
+
animated: rel.cardinality === "many",
|
|
208
|
+
style: {
|
|
209
|
+
stroke: "#10b981", // Green for generic-inherited relationships
|
|
210
|
+
strokeWidth: 2,
|
|
211
|
+
strokeDasharray: "5,5", // Dashed line for inherited
|
|
212
|
+
},
|
|
213
|
+
labelStyle: {
|
|
214
|
+
fontSize: 10,
|
|
215
|
+
fill: "#059669",
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return { nodes, edges };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Groups nodes by namespace for organized layout
|
|
228
|
+
*/
|
|
229
|
+
export function groupByNamespace(
|
|
230
|
+
nodes: SchemaFlowNode[],
|
|
231
|
+
): Map<string, SchemaFlowNode[]> {
|
|
232
|
+
const groups = new Map<string, SchemaFlowNode[]>();
|
|
233
|
+
|
|
234
|
+
for (const node of nodes) {
|
|
235
|
+
const namespace = node.data.namespace;
|
|
236
|
+
if (!groups.has(namespace)) {
|
|
237
|
+
groups.set(namespace, []);
|
|
238
|
+
}
|
|
239
|
+
groups.get(namespace)?.push(node);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return groups;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Applies a namespace-grouped layout to nodes
|
|
247
|
+
*/
|
|
248
|
+
export function applyNamespaceLayout(
|
|
249
|
+
data: SchemaFlowData,
|
|
250
|
+
options: {
|
|
251
|
+
nodeSpacing?: number;
|
|
252
|
+
namespaceSpacing?: number;
|
|
253
|
+
rowSize?: number;
|
|
254
|
+
} = {},
|
|
255
|
+
): SchemaFlowData {
|
|
256
|
+
const { nodeSpacing = 350, namespaceSpacing = 100, rowSize = 4 } = options;
|
|
257
|
+
|
|
258
|
+
const groups = groupByNamespace(data.nodes);
|
|
259
|
+
const updatedNodes: SchemaFlowNode[] = [];
|
|
260
|
+
|
|
261
|
+
let currentY = 0;
|
|
262
|
+
|
|
263
|
+
for (const [, groupNodes] of groups) {
|
|
264
|
+
groupNodes.forEach((node, index) => {
|
|
265
|
+
const col = index % rowSize;
|
|
266
|
+
const row = Math.floor(index / rowSize);
|
|
267
|
+
|
|
268
|
+
updatedNodes.push({
|
|
269
|
+
...node,
|
|
270
|
+
position: {
|
|
271
|
+
x: col * nodeSpacing,
|
|
272
|
+
y: currentY + row * nodeSpacing,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Calculate height of this namespace group
|
|
278
|
+
const groupRows = Math.ceil(groupNodes.length / rowSize);
|
|
279
|
+
currentY += groupRows * nodeSpacing + namespaceSpacing;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
nodes: updatedNodes,
|
|
284
|
+
edges: data.edges,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Converts schema data to React Flow nodes and edges with visibility filtering.
|
|
290
|
+
* Only visible nodes (not in hiddenNodes set) are rendered.
|
|
291
|
+
* Supports nodes, profiles, and templates.
|
|
292
|
+
*/
|
|
293
|
+
export function schemaToFlowFiltered(
|
|
294
|
+
nodes: NodeSchema[],
|
|
295
|
+
generics: GenericSchema[],
|
|
296
|
+
hiddenNodes: Set<string>,
|
|
297
|
+
options: {
|
|
298
|
+
nodeSpacing?: number;
|
|
299
|
+
rowSize?: number;
|
|
300
|
+
profiles?: ProfileSchema[];
|
|
301
|
+
templates?: TemplateSchema[];
|
|
302
|
+
} = {},
|
|
303
|
+
): SchemaFlowData {
|
|
304
|
+
const {
|
|
305
|
+
nodeSpacing = 400,
|
|
306
|
+
rowSize = 4,
|
|
307
|
+
profiles = [],
|
|
308
|
+
templates = [],
|
|
309
|
+
} = options;
|
|
310
|
+
|
|
311
|
+
const flowNodes: SchemaFlowNode[] = [];
|
|
312
|
+
const edges: Edge[] = [];
|
|
313
|
+
|
|
314
|
+
// Create a map of generics for quick lookup
|
|
315
|
+
const genericsMap = new Map<string, GenericSchema>();
|
|
316
|
+
for (const generic of generics) {
|
|
317
|
+
const kind = getSchemaKind(generic);
|
|
318
|
+
genericsMap.set(kind, generic);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Filter nodes based on visibility - only check hiddenNodes
|
|
322
|
+
const visibleNodes = nodes.filter((node) => {
|
|
323
|
+
const kind = getSchemaKind(node);
|
|
324
|
+
return !hiddenNodes.has(kind);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Filter profiles based on visibility
|
|
328
|
+
const visibleProfiles = profiles.filter((profile) => {
|
|
329
|
+
const kind = getSchemaKind(profile);
|
|
330
|
+
return !hiddenNodes.has(kind);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Filter templates based on visibility
|
|
334
|
+
const visibleTemplates = templates.filter((template) => {
|
|
335
|
+
const kind = getSchemaKind(template);
|
|
336
|
+
return !hiddenNodes.has(kind);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Create a map of all visible schema kinds for relationship edge creation
|
|
340
|
+
const allVisibleKinds = new Set<string>();
|
|
341
|
+
for (const node of visibleNodes) {
|
|
342
|
+
allVisibleKinds.add(getSchemaKind(node));
|
|
343
|
+
}
|
|
344
|
+
for (const profile of visibleProfiles) {
|
|
345
|
+
allVisibleKinds.add(getSchemaKind(profile));
|
|
346
|
+
}
|
|
347
|
+
for (const template of visibleTemplates) {
|
|
348
|
+
allVisibleKinds.add(getSchemaKind(template));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Create a map of visible schemas to their relationships for quick lookup
|
|
352
|
+
const schemaRelationshipsMap = new Map<string, Set<string>>();
|
|
353
|
+
for (const node of visibleNodes) {
|
|
354
|
+
const relNames = new Set<string>();
|
|
355
|
+
for (const rel of node.relationships ?? []) {
|
|
356
|
+
relNames.add(rel.name);
|
|
357
|
+
}
|
|
358
|
+
schemaRelationshipsMap.set(getSchemaKind(node), relNames);
|
|
359
|
+
}
|
|
360
|
+
for (const profile of visibleProfiles) {
|
|
361
|
+
const relNames = new Set<string>();
|
|
362
|
+
for (const rel of profile.relationships ?? []) {
|
|
363
|
+
relNames.add(rel.name);
|
|
364
|
+
}
|
|
365
|
+
schemaRelationshipsMap.set(getSchemaKind(profile), relNames);
|
|
366
|
+
}
|
|
367
|
+
for (const template of visibleTemplates) {
|
|
368
|
+
const relNames = new Set<string>();
|
|
369
|
+
for (const rel of template.relationships ?? []) {
|
|
370
|
+
relNames.add(rel.name);
|
|
371
|
+
}
|
|
372
|
+
schemaRelationshipsMap.set(getSchemaKind(template), relNames);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Create a map of generic kind -> visible nodes that inherit from it
|
|
376
|
+
const genericToInheritingNodes = new Map<string, string[]>();
|
|
377
|
+
for (const node of visibleNodes) {
|
|
378
|
+
for (const inheritedKind of node.inherit_from ?? []) {
|
|
379
|
+
if (genericsMap.has(inheritedKind)) {
|
|
380
|
+
if (!genericToInheritingNodes.has(inheritedKind)) {
|
|
381
|
+
genericToInheritingNodes.set(inheritedKind, []);
|
|
382
|
+
}
|
|
383
|
+
genericToInheritingNodes.get(inheritedKind)?.push(getSchemaKind(node));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
for (const template of visibleTemplates) {
|
|
388
|
+
for (const inheritedKind of template.inherit_from ?? []) {
|
|
389
|
+
if (genericsMap.has(inheritedKind)) {
|
|
390
|
+
if (!genericToInheritingNodes.has(inheritedKind)) {
|
|
391
|
+
genericToInheritingNodes.set(inheritedKind, []);
|
|
392
|
+
}
|
|
393
|
+
genericToInheritingNodes
|
|
394
|
+
.get(inheritedKind)
|
|
395
|
+
?.push(getSchemaKind(template));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Helper function to create edges for a schema's relationships
|
|
401
|
+
const createEdgesForSchema = (
|
|
402
|
+
schema: NodeSchema | ProfileSchema | TemplateSchema,
|
|
403
|
+
kind: string,
|
|
404
|
+
schemaType: "node" | "profile" | "template",
|
|
405
|
+
) => {
|
|
406
|
+
for (const rel of schema.relationships ?? []) {
|
|
407
|
+
// Check if the peer is a visible schema
|
|
408
|
+
if (allVisibleKinds.has(rel.peer)) {
|
|
409
|
+
// Find matching relationship on target for bidirectional connections
|
|
410
|
+
const targetRelationships = schemaRelationshipsMap.get(rel.peer);
|
|
411
|
+
// Look for a relationship on the target that points back to this schema
|
|
412
|
+
let targetRelName: string | null = null;
|
|
413
|
+
if (targetRelationships) {
|
|
414
|
+
// Find the first relationship on target that has this schema as its peer
|
|
415
|
+
const allSchemas = [
|
|
416
|
+
...visibleNodes,
|
|
417
|
+
...visibleProfiles,
|
|
418
|
+
...visibleTemplates,
|
|
419
|
+
];
|
|
420
|
+
const targetSchema = allSchemas.find(
|
|
421
|
+
(s) => getSchemaKind(s) === rel.peer,
|
|
422
|
+
);
|
|
423
|
+
if (targetSchema) {
|
|
424
|
+
const matchingRel = (targetSchema.relationships ?? []).find(
|
|
425
|
+
(r) => r.peer === kind,
|
|
426
|
+
);
|
|
427
|
+
if (matchingRel) {
|
|
428
|
+
targetRelName = matchingRel.name;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
edges.push({
|
|
434
|
+
id: `${kind}-${rel.name}-${rel.peer}`,
|
|
435
|
+
source: kind,
|
|
436
|
+
target: rel.peer,
|
|
437
|
+
type: "floating",
|
|
438
|
+
animated: rel.cardinality === "many",
|
|
439
|
+
data: {
|
|
440
|
+
sourceRelName: rel.name,
|
|
441
|
+
targetRelName,
|
|
442
|
+
sourceCardinality: "one", // Source side is always "one" from this node's perspective
|
|
443
|
+
targetCardinality: rel.cardinality, // The cardinality defined on this relationship
|
|
444
|
+
},
|
|
445
|
+
style: {
|
|
446
|
+
stroke: rel.inherited ? "#9ca3af" : getEdgeColorForType(schemaType),
|
|
447
|
+
strokeWidth: 2,
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
// Check if the peer is a generic - create edges to all inheriting schemas
|
|
452
|
+
else if (genericsMap.has(rel.peer)) {
|
|
453
|
+
const inheritingSchemas = genericToInheritingNodes.get(rel.peer) ?? [];
|
|
454
|
+
for (const inheritingSchemaKind of inheritingSchemas) {
|
|
455
|
+
// Find matching relationship on target for bidirectional connections
|
|
456
|
+
let targetRelName: string | null = null;
|
|
457
|
+
const allSchemas = [
|
|
458
|
+
...visibleNodes,
|
|
459
|
+
...visibleProfiles,
|
|
460
|
+
...visibleTemplates,
|
|
461
|
+
];
|
|
462
|
+
const targetSchema = allSchemas.find(
|
|
463
|
+
(s) => getSchemaKind(s) === inheritingSchemaKind,
|
|
464
|
+
);
|
|
465
|
+
if (targetSchema) {
|
|
466
|
+
const matchingRel = (targetSchema.relationships ?? []).find(
|
|
467
|
+
(r) => r.peer === kind,
|
|
468
|
+
);
|
|
469
|
+
if (matchingRel) {
|
|
470
|
+
targetRelName = matchingRel.name;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
edges.push({
|
|
475
|
+
id: `${kind}-${rel.name}-${inheritingSchemaKind}`,
|
|
476
|
+
source: kind,
|
|
477
|
+
target: inheritingSchemaKind,
|
|
478
|
+
type: "floating",
|
|
479
|
+
animated: rel.cardinality === "many",
|
|
480
|
+
data: {
|
|
481
|
+
sourceRelName: rel.name,
|
|
482
|
+
targetRelName,
|
|
483
|
+
sourceCardinality: "one",
|
|
484
|
+
targetCardinality: rel.cardinality,
|
|
485
|
+
},
|
|
486
|
+
style: {
|
|
487
|
+
stroke: "#10b981", // Green for generic-inherited relationships
|
|
488
|
+
strokeWidth: 2,
|
|
489
|
+
strokeDasharray: "5,5", // Dashed line for inherited
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// Helper function to get edge color based on schema type
|
|
498
|
+
const getEdgeColorForType = (
|
|
499
|
+
schemaType: "node" | "profile" | "template",
|
|
500
|
+
): string => {
|
|
501
|
+
switch (schemaType) {
|
|
502
|
+
case "profile":
|
|
503
|
+
return "#ec4899"; // Pink for profiles
|
|
504
|
+
case "template":
|
|
505
|
+
return "#f59e0b"; // Amber for templates
|
|
506
|
+
default:
|
|
507
|
+
return "#6366f1"; // Indigo for nodes
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
// Helper function to find inherited generics for any schema with inherit_from
|
|
512
|
+
const findInheritedGenericsForSchema = (
|
|
513
|
+
schema: NodeSchema | TemplateSchema,
|
|
514
|
+
): string[] => {
|
|
515
|
+
if (
|
|
516
|
+
!("inherit_from" in schema) ||
|
|
517
|
+
!schema.inherit_from ||
|
|
518
|
+
schema.inherit_from.length === 0
|
|
519
|
+
) {
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
return schema.inherit_from.filter((kind) => genericsMap.has(kind));
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
// Group all schemas by namespace for layout
|
|
526
|
+
const namespaceGroups = new Map<
|
|
527
|
+
string,
|
|
528
|
+
Array<{
|
|
529
|
+
schema: NodeSchema | ProfileSchema | TemplateSchema;
|
|
530
|
+
type: "node" | "profile" | "template";
|
|
531
|
+
}>
|
|
532
|
+
>();
|
|
533
|
+
|
|
534
|
+
for (const node of visibleNodes) {
|
|
535
|
+
if (!namespaceGroups.has(node.namespace)) {
|
|
536
|
+
namespaceGroups.set(node.namespace, []);
|
|
537
|
+
}
|
|
538
|
+
namespaceGroups.get(node.namespace)?.push({ schema: node, type: "node" });
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
for (const profile of visibleProfiles) {
|
|
542
|
+
if (!namespaceGroups.has(profile.namespace)) {
|
|
543
|
+
namespaceGroups.set(profile.namespace, []);
|
|
544
|
+
}
|
|
545
|
+
namespaceGroups
|
|
546
|
+
.get(profile.namespace)
|
|
547
|
+
?.push({ schema: profile, type: "profile" });
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
for (const template of visibleTemplates) {
|
|
551
|
+
if (!namespaceGroups.has(template.namespace)) {
|
|
552
|
+
namespaceGroups.set(template.namespace, []);
|
|
553
|
+
}
|
|
554
|
+
namespaceGroups
|
|
555
|
+
.get(template.namespace)
|
|
556
|
+
?.push({ schema: template, type: "template" });
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Layout schemas by namespace
|
|
560
|
+
let currentY = 0;
|
|
561
|
+
const namespaceSpacing = 150;
|
|
562
|
+
|
|
563
|
+
for (const [, groupItems] of namespaceGroups) {
|
|
564
|
+
groupItems.forEach((item, index) => {
|
|
565
|
+
const { schema, type } = item;
|
|
566
|
+
const kind = getSchemaKind(schema);
|
|
567
|
+
|
|
568
|
+
// Find inherited generics (only for nodes and templates)
|
|
569
|
+
const inheritedGenerics =
|
|
570
|
+
type === "node" || type === "template"
|
|
571
|
+
? findInheritedGenericsForSchema(
|
|
572
|
+
schema as NodeSchema | TemplateSchema,
|
|
573
|
+
)
|
|
574
|
+
: [];
|
|
575
|
+
|
|
576
|
+
const col = index % rowSize;
|
|
577
|
+
const row = Math.floor(index / rowSize);
|
|
578
|
+
|
|
579
|
+
const flowNode: SchemaFlowNode = {
|
|
580
|
+
id: kind,
|
|
581
|
+
type: "schemaNode",
|
|
582
|
+
position: {
|
|
583
|
+
x: col * nodeSpacing,
|
|
584
|
+
y: currentY + row * nodeSpacing,
|
|
585
|
+
},
|
|
586
|
+
data: {
|
|
587
|
+
kind,
|
|
588
|
+
label: schema.label ?? schema.name,
|
|
589
|
+
namespace: schema.namespace,
|
|
590
|
+
description: schema.description,
|
|
591
|
+
icon: schema.icon,
|
|
592
|
+
attributes: (schema.attributes ?? []).map((attr) => ({
|
|
593
|
+
name: attr.name,
|
|
594
|
+
kind: attr.kind,
|
|
595
|
+
label: attr.label,
|
|
596
|
+
optional: attr.optional,
|
|
597
|
+
inherited: attr.inherited,
|
|
598
|
+
})),
|
|
599
|
+
relationships: (schema.relationships ?? []).map((rel) => ({
|
|
600
|
+
name: rel.name,
|
|
601
|
+
peer: rel.peer,
|
|
602
|
+
cardinality: rel.cardinality,
|
|
603
|
+
label: rel.label,
|
|
604
|
+
inherited: rel.inherited,
|
|
605
|
+
})),
|
|
606
|
+
inheritFrom: inheritedGenerics,
|
|
607
|
+
schemaType: type,
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
flowNodes.push(flowNode);
|
|
612
|
+
|
|
613
|
+
// Create edges for relationships
|
|
614
|
+
createEdgesForSchema(schema, kind, type);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// Calculate height of this namespace group
|
|
618
|
+
const groupRows = Math.ceil(groupItems.length / rowSize);
|
|
619
|
+
currentY += groupRows * nodeSpacing + namespaceSpacing;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return { nodes: flowNodes, edges };
|
|
623
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webview entry point for VSCode extension.
|
|
3
|
+
* This file creates a global function that can be called from the webview HTML
|
|
4
|
+
* to render the SchemaVisualizer component.
|
|
5
|
+
*
|
|
6
|
+
* The bundle is completely self-contained with all styles and dependencies.
|
|
7
|
+
*/
|
|
8
|
+
import { createRoot } from "react-dom/client";
|
|
9
|
+
import { SchemaVisualizer } from "./components/SchemaVisualizer";
|
|
10
|
+
import type { SchemaVisualizerData } from "./types/schema";
|
|
11
|
+
import "./webview.css";
|
|
12
|
+
|
|
13
|
+
// Define the global interface for VSCode communication
|
|
14
|
+
declare global {
|
|
15
|
+
interface Window {
|
|
16
|
+
acquireVsCodeApi?: () => {
|
|
17
|
+
postMessage: (message: unknown) => void;
|
|
18
|
+
getState: () => unknown;
|
|
19
|
+
setState: (state: unknown) => void;
|
|
20
|
+
};
|
|
21
|
+
__vscodeApi?: {
|
|
22
|
+
postMessage: (message: unknown) => void;
|
|
23
|
+
getState: () => unknown;
|
|
24
|
+
setState: (state: unknown) => void;
|
|
25
|
+
};
|
|
26
|
+
renderSchemaVisualizer: (
|
|
27
|
+
container: HTMLElement,
|
|
28
|
+
data: SchemaVisualizerData,
|
|
29
|
+
options?: {
|
|
30
|
+
onNodeClick?: (nodeId: string, schema: unknown) => void;
|
|
31
|
+
},
|
|
32
|
+
) => void;
|
|
33
|
+
schemaVisualizerData?: SchemaVisualizerData;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Cache the VSCode API to prevent double-acquisition error
|
|
38
|
+
function getVsCodeApi() {
|
|
39
|
+
if (!window.__vscodeApi && window.acquireVsCodeApi) {
|
|
40
|
+
try {
|
|
41
|
+
window.__vscodeApi = window.acquireVsCodeApi();
|
|
42
|
+
} catch {
|
|
43
|
+
// API already acquired, ignore
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return window.__vscodeApi;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Create the render function that will be called from the webview
|
|
50
|
+
window.renderSchemaVisualizer = (
|
|
51
|
+
container: HTMLElement,
|
|
52
|
+
data: SchemaVisualizerData,
|
|
53
|
+
options?: {
|
|
54
|
+
onNodeClick?: (nodeId: string, schema: unknown) => void;
|
|
55
|
+
},
|
|
56
|
+
) => {
|
|
57
|
+
// Clear any existing content
|
|
58
|
+
container.innerHTML = "";
|
|
59
|
+
|
|
60
|
+
// Create a wrapper div with the root class for styling
|
|
61
|
+
const wrapper = document.createElement("div");
|
|
62
|
+
wrapper.className = "schema-visualizer-root";
|
|
63
|
+
wrapper.style.width = "100%";
|
|
64
|
+
wrapper.style.height = "100%";
|
|
65
|
+
container.appendChild(wrapper);
|
|
66
|
+
|
|
67
|
+
const root = createRoot(wrapper);
|
|
68
|
+
|
|
69
|
+
// Get cached VSCode API if available
|
|
70
|
+
const vscode = getVsCodeApi();
|
|
71
|
+
|
|
72
|
+
const handleNodeClick = (nodeId: string, schema: unknown) => {
|
|
73
|
+
// Call the provided callback
|
|
74
|
+
options?.onNodeClick?.(nodeId, schema);
|
|
75
|
+
|
|
76
|
+
// Also post message to VSCode if available
|
|
77
|
+
vscode?.postMessage({
|
|
78
|
+
type: "nodeClick",
|
|
79
|
+
nodeId,
|
|
80
|
+
schema,
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
root.render(
|
|
85
|
+
<SchemaVisualizer
|
|
86
|
+
data={data}
|
|
87
|
+
onNodeClick={handleNodeClick}
|
|
88
|
+
showBackground={true}
|
|
89
|
+
showNodeDetails={true}
|
|
90
|
+
showToolbar={true}
|
|
91
|
+
showStats={true}
|
|
92
|
+
/>,
|
|
93
|
+
);
|
|
94
|
+
};
|