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.
@@ -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
+ };