@rebasepro/studio 0.17.3 → 0.18.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.
Files changed (81) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +12 -4
  3. package/dist/{ApiKeysView-DiCurTEU.js → ApiKeysView-EIvegrUk.js} +54 -39
  4. package/dist/ApiKeysView-EIvegrUk.js.map +1 -0
  5. package/dist/{BackupsView-DNS6LdVg.js → BackupsView-DVkKzaPR.js} +29 -31
  6. package/dist/BackupsView-DVkKzaPR.js.map +1 -0
  7. package/dist/{BranchesView-CM3B-ER3.js → BranchesView-Df8hQ8wE.js} +85 -24
  8. package/dist/BranchesView-Df8hQ8wE.js.map +1 -0
  9. package/dist/{CronJobsView-BHtJZeJZ.js → CronJobsView-CLOGjb-L.js} +32 -27
  10. package/dist/CronJobsView-CLOGjb-L.js.map +1 -0
  11. package/dist/{RLSEditor-CBX4uD66.js → RLSEditor-Sa1D34ny.js} +724 -615
  12. package/dist/RLSEditor-Sa1D34ny.js.map +1 -0
  13. package/dist/{SQLEditor-ByecwX8B.js → SQLEditor-BfNA8xM3.js} +9 -4
  14. package/dist/{SQLEditor-ByecwX8B.js.map → SQLEditor-BfNA8xM3.js.map} +1 -1
  15. package/dist/{SchemaVisualizer-D6tMZgeC.js → SchemaVisualizer-DTDMYL6o.js} +84 -110
  16. package/dist/SchemaVisualizer-DTDMYL6o.js.map +1 -0
  17. package/dist/{StorageView-BGDeC5GI.js → StorageView-Ed9Q_VlB.js} +5 -32
  18. package/dist/StorageView-Ed9Q_VlB.js.map +1 -0
  19. package/dist/components/RLSEditor/saveRules.d.ts +38 -0
  20. package/dist/components/RebaseStudio.d.ts +1 -1
  21. package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +5 -2
  22. package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +0 -4
  23. package/dist/components/load-failure-view.d.ts +17 -0
  24. package/dist/components/load-failure.d.ts +47 -0
  25. package/dist/index.es.js +56 -36
  26. package/dist/index.es.js.map +1 -1
  27. package/dist/load-failure-C6OMJSGC.js +31 -0
  28. package/dist/load-failure-C6OMJSGC.js.map +1 -0
  29. package/dist/load-failure-view-CBTB_h-h.js +48 -0
  30. package/dist/load-failure-view-CBTB_h-h.js.map +1 -0
  31. package/package.json +36 -21
  32. package/dist/ApiKeysView-DiCurTEU.js.map +0 -1
  33. package/dist/BackupsView-DNS6LdVg.js.map +0 -1
  34. package/dist/BranchesView-CM3B-ER3.js.map +0 -1
  35. package/dist/CronJobsView-BHtJZeJZ.js.map +0 -1
  36. package/dist/RLSEditor-CBX4uD66.js.map +0 -1
  37. package/dist/SchemaVisualizer-D6tMZgeC.js.map +0 -1
  38. package/dist/StorageView-BGDeC5GI.js.map +0 -1
  39. package/dist/components/StorageView/storage-failure.d.ts +0 -34
  40. package/src/components/ApiExplorer/ApiExplorer.tsx +0 -299
  41. package/src/components/ApiExplorer/EndpointDetail.tsx +0 -279
  42. package/src/components/ApiExplorer/TryItPanel.tsx +0 -499
  43. package/src/components/ApiExplorer/parseSpec.ts +0 -104
  44. package/src/components/ApiExplorer/types.ts +0 -84
  45. package/src/components/ApiKeys/ApiKeysView.tsx +0 -479
  46. package/src/components/ApiKeys/CreateApiKeyDialog.tsx +0 -711
  47. package/src/components/ApiKeys/permissions.ts +0 -145
  48. package/src/components/AuthSimulationSelector.tsx +0 -72
  49. package/src/components/Backups/BackupsView.tsx +0 -185
  50. package/src/components/Branches/BranchesView.tsx +0 -386
  51. package/src/components/CronJobs/CronJobsView.tsx +0 -413
  52. package/src/components/JSEditor/JSEditor.tsx +0 -1100
  53. package/src/components/JSEditor/JSEditorSidebar.tsx +0 -350
  54. package/src/components/JSEditor/JSMonacoEditor.tsx +0 -410
  55. package/src/components/LogsExplorer/LogsExplorer.tsx +0 -273
  56. package/src/components/LogsExplorer/useLogTail.ts +0 -356
  57. package/src/components/RLSEditor/PolicyEditor.tsx +0 -399
  58. package/src/components/RLSEditor/RLSEditor.tsx +0 -1128
  59. package/src/components/RLSEditor/index.ts +0 -1
  60. package/src/components/RLSEditor/policy-presets.ts +0 -176
  61. package/src/components/RLSEditor/saveSecurityRules.ts +0 -48
  62. package/src/components/RebaseStudio.tsx +0 -174
  63. package/src/components/SQLEditor/ExplainVisualizer.tsx +0 -128
  64. package/src/components/SQLEditor/MonacoEditor.tsx +0 -203
  65. package/src/components/SQLEditor/SQLEditor.tsx +0 -1518
  66. package/src/components/SQLEditor/SQLEditorSidebar.tsx +0 -173
  67. package/src/components/SQLEditor/SchemaBrowser.tsx +0 -168
  68. package/src/components/SQLEditor/sql_editor_types.ts +0 -18
  69. package/src/components/SchemaVisualizer/RelationEdge.tsx +0 -102
  70. package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +0 -710
  71. package/src/components/SchemaVisualizer/TableNode.tsx +0 -257
  72. package/src/components/SchemaVisualizer/schema-visualizer.utils.ts +0 -140
  73. package/src/components/SchemaVisualizer/useLiveRls.ts +0 -86
  74. package/src/components/SchemaVisualizer/useSchemaGraph.ts +0 -426
  75. package/src/components/StorageView/StorageView.tsx +0 -1588
  76. package/src/components/StorageView/storage-failure.ts +0 -63
  77. package/src/components/StudioHomePage.tsx +0 -430
  78. package/src/index.ts +0 -31
  79. package/src/utils/parseSpec.test.ts +0 -309
  80. package/src/utils/sql_utils.test.ts +0 -265
  81. package/src/utils/sql_utils.ts +0 -291
@@ -1,710 +0,0 @@
1
- import React, { useState, useCallback, useMemo, useEffect, useRef } from "react";
2
- import {
3
- ReactFlow,
4
- Controls,
5
- MiniMap,
6
- Background,
7
- BackgroundVariant,
8
- useReactFlow,
9
- useNodesInitialized,
10
- ReactFlowProvider,
11
- applyNodeChanges,
12
- applyEdgeChanges
13
- } from "@xyflow/react";
14
- import type { Node, Edge, NodeChange, EdgeChange } from "@xyflow/react";
15
- import "@xyflow/react/dist/style.css";
16
- import {
17
- Button,
18
- SearchBar,
19
- TextField,
20
- Tooltip,
21
- Alert,
22
- Typography,
23
- cls,
24
- defaultBorderMixin,
25
- CircularProgress,
26
- ResizablePanels,
27
- IconButton
28
- } from "@rebasepro/ui";
29
- import {
30
- useStudioCollectionRegistry,
31
- IconForView
32
- } from "@rebasepro/app";
33
-
34
- import { isPostgresCollectionConfig } from "@rebasepro/types";
35
- import { useSchemaGraph } from "./useSchemaGraph";
36
- import { useLiveRlsTables } from "./useLiveRls";
37
- import type { TableNodeData } from "./useSchemaGraph";
38
- import { TableNode } from "./TableNode";
39
- import { RelationEdge } from "./RelationEdge";
40
- import type { AdminCollection } from "@rebasepro/cms-types";
41
-
42
- // ─── Custom node / edge type registrations ────────────────────────────
43
-
44
- const nodeTypes = {
45
- tableNode: TableNode
46
- };
47
-
48
- const edgeTypes = {
49
- relationEdge: RelationEdge
50
- };
51
-
52
- // ─── Inner component (needs ReactFlowProvider) ─────────────────────────
53
-
54
- function SchemaVisualizerCanvas({
55
- collections
56
- }: {
57
- collections: AdminCollection[];
58
- }) {
59
- const reactFlowInstance = useReactFlow();
60
- const liveRls = useLiveRlsTables();
61
- const {
62
- nodes: layoutedNodes,
63
- edges: layoutedEdges,
64
- direction,
65
- setDirection,
66
- relayout,
67
- tableCount,
68
- relationCount
69
- } = useSchemaGraph(collections, liveRls);
70
-
71
- const [nodes, setNodes] = useState<Node[]>([]);
72
- const [edges, setEdges] = useState<Edge[]>([]);
73
- const [selectedTable, setSelectedTable] = useState<string | null>(null);
74
- const [searchQuery, setSearchQuery] = useState("");
75
- const initialFitDone = useRef(false);
76
-
77
- // Sync layouted data into state
78
- useEffect(() => {
79
- setNodes(layoutedNodes);
80
- setEdges(layoutedEdges);
81
- }, [layoutedNodes, layoutedEdges]);
82
-
83
- const onNodesChange = useCallback(
84
- (changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)),
85
- []
86
- );
87
- const onEdgesChange = useCallback(
88
- (changes: EdgeChange[]) => setEdges((eds) => applyEdgeChanges(changes, eds)),
89
- []
90
- );
91
-
92
- /**
93
- * Fit the graph into view, once, as soon as it can be measured.
94
- *
95
- * This used to fire on a 200ms timer. `fitView` needs every node's real
96
- * width and height, which React Flow only knows after it has laid them out
97
- * and measured them — with a dozen tables, a cold Monaco-sized bundle and
98
- * a slow first paint, 200ms is often too early. The fit then ran against
99
- * zero-sized nodes and produced a viewport nothing lines up with, and
100
- * `initialFitDone` made sure it was never retried: the ERD opened as a
101
- * scatter of specks in a corner and stayed there until you hit "fit" by
102
- * hand. `useNodesInitialized` is the event this was approximating.
103
- */
104
- const nodesInitialized = useNodesInitialized();
105
- useEffect(() => {
106
- if (!nodesInitialized || nodes.length === 0 || initialFitDone.current) return;
107
- initialFitDone.current = true;
108
- reactFlowInstance.fitView({ padding: 0.15,
109
- duration: 400 });
110
- }, [nodesInitialized, nodes.length, reactFlowInstance]);
111
-
112
- /**
113
- * Re-fit after a relayout.
114
- *
115
- * Switching LR↔TB transposes the graph — a tall column becomes a wide band
116
- * — so the viewport that framed the old shape frames the new one badly:
117
- * asking for a different layout used to leave most of it off-screen until
118
- * you pressed "fit" as a second step. Skipped for the very first layout,
119
- * which the initial fit above already handles.
120
- */
121
- const lastFittedDirection = useRef(direction);
122
- useEffect(() => {
123
- if (!nodesInitialized || nodes.length === 0) return;
124
- if (lastFittedDirection.current === direction) return;
125
- lastFittedDirection.current = direction;
126
- reactFlowInstance.fitView({ padding: 0.15,
127
- duration: 400 });
128
- }, [direction, nodesInitialized, nodes, reactFlowInstance]);
129
-
130
- const handleFitView = useCallback(() => {
131
- reactFlowInstance.fitView({ padding: 0.15,
132
- duration: 400 });
133
- }, [reactFlowInstance]);
134
-
135
- const handleNodeClick = useCallback((_: React.MouseEvent, node: Node) => {
136
- setSelectedTable(node.id);
137
- }, []);
138
-
139
- const handlePaneClick = useCallback(() => {
140
- setSelectedTable(null);
141
- }, []);
142
-
143
- // Focus on a table from the sidebar
144
- const handleTableSelect = useCallback(
145
- (nodeId: string) => {
146
- setSelectedTable(nodeId);
147
- const node = nodes.find((n) => n.id === nodeId);
148
- if (node) {
149
- reactFlowInstance.setCenter(
150
- node.position.x + 140,
151
- node.position.y + 60,
152
- { zoom: 1.2,
153
- duration: 400 }
154
- );
155
- }
156
- },
157
- [nodes, reactFlowInstance]
158
- );
159
-
160
- // Sidebar panel size
161
- const [sidebarSize, setSidebarSize] = useState(() => {
162
- try {
163
- const saved = localStorage.getItem("rebase_schema_viz_sidebar_size");
164
- return saved !== null ? parseFloat(saved) : 20;
165
- } catch {
166
- return 20;
167
- }
168
- });
169
-
170
- useEffect(() => {
171
- try {
172
- localStorage.setItem(
173
- "rebase_schema_viz_sidebar_size",
174
- sidebarSize.toString()
175
- );
176
- } catch {
177
- // Ignore
178
- }
179
- }, [sidebarSize]);
180
-
181
- // Group collections for sidebar
182
- const postgresCollections = useMemo(
183
- () => collections.filter(isPostgresCollectionConfig),
184
- [collections]
185
- );
186
-
187
- const filteredCollections = useMemo(() => {
188
- if (!searchQuery.trim()) return postgresCollections;
189
- const q = searchQuery.toLowerCase();
190
- return postgresCollections.filter(
191
- (c) =>
192
- c.name.toLowerCase().includes(q) ||
193
- c.table?.toLowerCase().includes(q) ||
194
- c.slug?.toLowerCase().includes(q)
195
- );
196
- }, [postgresCollections, searchQuery]);
197
-
198
- // Junction table nodes
199
- const junctionNodes = useMemo(
200
- () =>
201
- nodes.filter(
202
- (n) => (n.data as TableNodeData).isJunction
203
- ),
204
- [nodes]
205
- );
206
-
207
- /**
208
- * The RLS answer for one collection, from the same place the graph used.
209
- *
210
- * `nodes` already carries it, so look it up there rather than re-deriving —
211
- * that is what kept the sidebar dots, the node badges and the total from
212
- * agreeing.
213
- */
214
- const rlsEnabledFor = useCallback(
215
- (collection: AdminCollection): boolean => {
216
- const table = (isPostgresCollectionConfig(collection) ? collection.table : undefined) ?? collection.slug;
217
- const node = nodes.find((n) => n.id === `table-${table}`);
218
- return Boolean((node?.data as TableNodeData | undefined)?.rlsEnabled);
219
- },
220
- [nodes]
221
- );
222
-
223
- // Stats
224
- const stats = useMemo(
225
- () => ({
226
- tables: tableCount,
227
- relations: relationCount,
228
- junctions: junctionNodes.length,
229
- // Counted off the nodes, which already carry the database's answer
230
- // where there was one — the same number the per-table badges show.
231
- // Counting `securityRules` here instead made the total disagree
232
- // with the badges, and read 0 in the hosted console, where the
233
- // contract endpoint strips those rules.
234
- withRls: nodes.filter((n) => (n.data as TableNodeData | undefined)?.rlsEnabled).length
235
- }),
236
- [
237
- tableCount,
238
- relationCount,
239
- junctionNodes.length,
240
- nodes
241
- ]
242
- );
243
-
244
- return (
245
- <ResizablePanels
246
- orientation="horizontal"
247
- panelSizePercent={sidebarSize}
248
- onPanelSizeChange={setSidebarSize}
249
- minPanelSizePx={220}
250
- firstPanel={
251
- <div
252
- className={cls(
253
- "flex flex-col h-full w-full bg-white dark:bg-surface-950 border-r",
254
- defaultBorderMixin
255
- )}
256
- >
257
- {/* Sidebar header */}
258
- <div
259
- className={cls(
260
- "flex items-center justify-between px-3 py-2 border-b bg-surface-50 dark:bg-surface-900 min-h-[48px]",
261
- defaultBorderMixin
262
- )}
263
- >
264
- <Typography
265
- variant="caption"
266
- className="font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark"
267
- >
268
- Tables
269
- </Typography>
270
- <Typography
271
- variant="caption"
272
- className="text-[10px] text-text-disabled dark:text-text-disabled-dark font-mono"
273
- >
274
- {stats.tables}
275
- </Typography>
276
- </div>
277
-
278
- {/* Search */}
279
- <div className="px-2 py-1.5 border-b border-surface-200/40 dark:border-surface-700/40">
280
- <SearchBar
281
- size="smallest"
282
- placeholder="Filter tables…"
283
- onTextSearch={(val) => setSearchQuery(val ?? "")}
284
- innerClassName="text-xs"
285
- />
286
- </div>
287
-
288
- {/* Table list */}
289
- <div className="flex-grow overflow-y-auto no-scrollbar p-1">
290
- {/* Regular collections */}
291
- <div className="space-y-0.5">
292
- {filteredCollections.map((collection) => {
293
- const table = collection.table ?? collection.slug;
294
- const nodeId = `table-${table}`;
295
- const isSelected = selectedTable === nodeId;
296
- return (
297
- <div
298
- key={nodeId}
299
- onClick={() =>
300
- handleTableSelect(nodeId)
301
- }
302
- className={cls(
303
- "flex items-center p-1.5 cursor-pointer rounded transition-colors group",
304
- isSelected
305
- ? "bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-light"
306
- : "hover:bg-surface-100 dark:hover:bg-surface-950 text-text-secondary dark:text-text-secondary-dark"
307
- )}
308
- >
309
- <div className="shrink-0 mr-1.5 text-text-disabled dark:text-text-disabled-dark">
310
- <IconForView
311
- collectionOrView={{
312
- slug: collection.slug,
313
- name: collection.name,
314
- icon:
315
- typeof collection.icon ===
316
- "string"
317
- ? collection.icon
318
- : undefined
319
- }}
320
- size="smallest"
321
- />
322
- </div>
323
- <div className="flex flex-col min-w-0 flex-1">
324
- <Typography
325
- variant="caption"
326
- className="text-xs truncate font-medium"
327
- >
328
- {collection.name}
329
- </Typography>
330
- {table !== collection.name && (
331
- <Typography
332
- variant="caption"
333
- className="text-[10px] text-text-disabled dark:text-text-disabled-dark font-mono truncate"
334
- >
335
- {table}
336
- </Typography>
337
- )}
338
- </div>
339
- <div className="flex items-center gap-1 shrink-0 ml-1">
340
- {/* Same source as the node badge and the
341
- "RLS protected" total: the database when
342
- it could be asked, the config otherwise.
343
- Reading `securityRules` directly here made
344
- the three disagree. */}
345
- {rlsEnabledFor(collection) && (
346
- <Tooltip title="RLS enabled">
347
- <div className="w-1.5 h-1.5 rounded-full bg-green-500"/>
348
- </Tooltip>
349
- )}
350
- {collection.history && (
351
- <Tooltip title="History enabled">
352
- <div className="w-1.5 h-1.5 rounded-full bg-blue-400"/>
353
- </Tooltip>
354
- )}
355
- </div>
356
- </div>
357
- );
358
- })}
359
- </div>
360
-
361
- {/* Junction tables */}
362
- {junctionNodes.length > 0 && (
363
- <div className="mt-3">
364
- <Typography
365
- variant="caption"
366
- className="px-1.5 text-[10px] uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark font-medium"
367
- >
368
- Junction Tables
369
- </Typography>
370
- <div className="mt-1 space-y-0.5">
371
- {junctionNodes.map((node) => {
372
- const d =
373
- node.data as TableNodeData;
374
- const isSelected =
375
- selectedTable === node.id;
376
- return (
377
- <div
378
- key={node.id}
379
- onClick={() =>
380
- handleTableSelect(
381
- node.id
382
- )
383
- }
384
- className={cls(
385
- "flex items-center p-1.5 cursor-pointer rounded transition-colors",
386
- isSelected
387
- ? "bg-primary/10 text-primary dark:bg-primary/20"
388
- : "hover:bg-surface-100 dark:hover:bg-surface-950 text-text-disabled dark:text-text-disabled-dark"
389
- )}
390
- >
391
- <svg
392
- className="w-3.5 h-3.5 mr-1.5 shrink-0"
393
- fill="none"
394
- stroke="currentColor"
395
- viewBox="0 0 24 24"
396
- >
397
- <path
398
- strokeLinecap="round"
399
- strokeLinejoin="round"
400
- strokeWidth={2}
401
- d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"
402
- />
403
- </svg>
404
- <Typography
405
- variant="caption"
406
- className="text-xs font-mono truncate"
407
- >
408
- {d.tableName}
409
- </Typography>
410
- </div>
411
- );
412
- })}
413
- </div>
414
- </div>
415
- )}
416
- </div>
417
-
418
- {/* Stats footer */}
419
- <div
420
- className={cls(
421
- "px-3 py-2 border-t bg-surface-50 dark:bg-surface-900 space-y-1",
422
- defaultBorderMixin
423
- )}
424
- >
425
- <div className="flex items-center justify-between">
426
- <Typography
427
- variant="caption"
428
- className="text-[10px] text-text-disabled dark:text-text-disabled-dark"
429
- >
430
- Tables
431
- </Typography>
432
- <Typography
433
- variant="caption"
434
- className="text-[10px] font-mono font-medium"
435
- >
436
- {stats.tables}
437
- </Typography>
438
- </div>
439
- <div className="flex items-center justify-between">
440
- <Typography
441
- variant="caption"
442
- className="text-[10px] text-text-disabled dark:text-text-disabled-dark"
443
- >
444
- Relations
445
- </Typography>
446
- <Typography
447
- variant="caption"
448
- className="text-[10px] font-mono font-medium"
449
- >
450
- {stats.relations}
451
- </Typography>
452
- </div>
453
- <div className="flex items-center justify-between">
454
- <Typography
455
- variant="caption"
456
- className="text-[10px] text-text-disabled dark:text-text-disabled-dark"
457
- >
458
- RLS protected
459
- </Typography>
460
- <div className="flex items-center gap-1">
461
- <div className="w-1.5 h-1.5 rounded-full bg-green-500"/>
462
- <Typography
463
- variant="caption"
464
- className="text-[10px] font-mono font-medium"
465
- >
466
- {stats.withRls}
467
- </Typography>
468
- </div>
469
- </div>
470
- </div>
471
- </div>
472
- }
473
- secondPanel={
474
- <div className="flex-grow flex flex-col min-w-0 h-full w-full bg-white dark:bg-surface-950">
475
- {/* Toolbar */}
476
- <div
477
- className={cls(
478
- "flex items-center justify-between pr-2 border-b bg-white dark:bg-surface-950 min-h-[46px]",
479
- defaultBorderMixin
480
- )}
481
- >
482
- <div className="flex items-center gap-2 px-4">
483
- <Typography
484
- variant="subtitle2"
485
- className="font-mono text-text-secondary dark:text-text-secondary-dark"
486
- >
487
- Schema Visualizer
488
- </Typography>
489
- </div>
490
- <div className="flex shrink-0 items-center gap-1.5">
491
- {/* Direction toggle */}
492
- <div className="flex items-center bg-surface-100 dark:bg-surface-950 rounded-md border border-surface-200/40 dark:border-surface-700/40">
493
- <Tooltip title="Left to right layout">
494
- <Button
495
- size="small"
496
- variant={direction === "LR" ? "filled" : "text"}
497
- color={direction === "LR" ? "primary" : "neutral"}
498
- onClick={() => setDirection("LR")}
499
- className="!rounded-r-none !px-2 !py-1 !text-[10px] !font-mono"
500
- >
501
- LR
502
- </Button>
503
- </Tooltip>
504
- <Tooltip title="Top to bottom layout">
505
- <Button
506
- size="small"
507
- variant={direction === "TB" ? "filled" : "text"}
508
- color={direction === "TB" ? "primary" : "neutral"}
509
- onClick={() => setDirection("TB")}
510
- className="!rounded-l-none !px-2 !py-1 !text-[10px] !font-mono"
511
- >
512
- TB
513
- </Button>
514
- </Tooltip>
515
- </div>
516
-
517
- <div className="h-4 w-px bg-surface-200 dark:bg-surface-950 mx-0.5"/>
518
-
519
- {/* Fit view */}
520
- <Tooltip title="Fit to view">
521
- <IconButton
522
- size="small"
523
- onClick={handleFitView}
524
- >
525
- <svg
526
- className="w-4 h-4"
527
- fill="none"
528
- stroke="currentColor"
529
- viewBox="0 0 24 24"
530
- >
531
- <path
532
- strokeLinecap="round"
533
- strokeLinejoin="round"
534
- strokeWidth={2}
535
- d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
536
- />
537
- </svg>
538
- </IconButton>
539
- </Tooltip>
540
-
541
- {/* Re-layout */}
542
- <Tooltip title="Re-layout">
543
- <IconButton
544
- size="small"
545
- onClick={relayout}
546
- >
547
- <svg
548
- className="w-4 h-4"
549
- fill="none"
550
- stroke="currentColor"
551
- viewBox="0 0 24 24"
552
- >
553
- <path
554
- strokeLinecap="round"
555
- strokeLinejoin="round"
556
- strokeWidth={2}
557
- d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
558
- />
559
- </svg>
560
- </IconButton>
561
- </Tooltip>
562
- </div>
563
- </div>
564
-
565
- {/* Canvas */}
566
- <div className="flex-grow relative">
567
- <ReactFlow
568
- nodes={nodes}
569
- edges={edges}
570
- onNodesChange={onNodesChange}
571
- onEdgesChange={onEdgesChange}
572
- onNodeClick={handleNodeClick}
573
- onPaneClick={handlePaneClick}
574
- nodeTypes={nodeTypes}
575
- edgeTypes={edgeTypes}
576
- fitView
577
- fitViewOptions={{ padding: 0.15 }}
578
- minZoom={0.1}
579
- maxZoom={2}
580
- proOptions={{ hideAttribution: true }}
581
- className="bg-surface-50 dark:bg-surface-950"
582
- >
583
- <Background
584
- variant={BackgroundVariant.Dots}
585
- gap={20}
586
- size={1}
587
- className="!bg-surface-50 dark:!bg-surface-950"
588
- color="var(--rf-bg-dot, #d4d4d8)"
589
- />
590
- <Controls
591
- showInteractive={false}
592
- // mb clears the legend overlay pinned at bottom-left
593
- className="!mb-16 !bg-white dark:!bg-surface-900 !border !border-surface-200/40 dark:!border-surface-700/40 !shadow-sm !rounded-lg dark:[--xy-controls-button-background-color:var(--color-surface-900)] dark:[--xy-controls-button-background-color-hover:var(--color-surface-800)] dark:[--xy-controls-button-color:var(--color-surface-300)] dark:[--xy-controls-button-color-hover:var(--color-surface-50)] dark:[--xy-controls-button-border-color:var(--color-surface-700)]"
594
- />
595
- <MiniMap
596
- nodeStrokeColor={(n) => {
597
- const d = n.data as TableNodeData;
598
- if (d.isJunction) return "#a78bfa";
599
- if (d.rlsEnabled) return "#22c55e";
600
- return "#6366f1";
601
- }}
602
- nodeColor={(n) => {
603
- const d = n.data as TableNodeData;
604
- if (d.isJunction) return "#ede9fe";
605
- return "#eef2ff";
606
- }}
607
- maskColor="rgba(0,0,0,0.08)"
608
- className="!bg-white dark:!bg-surface-900 !border !border-surface-200/40 dark:!border-surface-700/40 !shadow-sm !rounded-lg"
609
- />
610
- </ReactFlow>
611
-
612
- {/* Legend overlay */}
613
- <div className="absolute bottom-4 left-4 flex items-center gap-3 px-3 py-2 bg-white/90 dark:bg-surface-900/90 backdrop-blur-sm rounded-lg border border-surface-200/40 dark:border-surface-700/40 shadow-sm">
614
- <div className="flex items-center gap-1.5">
615
- <div className="w-6 h-0.5 bg-indigo-500 rounded"/>
616
- <Typography
617
- variant="caption"
618
- className="text-[10px] text-text-disabled dark:text-text-disabled-dark"
619
- >
620
- Owning
621
- </Typography>
622
- </div>
623
- <div className="flex items-center gap-1.5">
624
- <div className="w-6 h-0.5 bg-violet-500 rounded"/>
625
- <Typography
626
- variant="caption"
627
- className="text-[10px] text-text-disabled dark:text-text-disabled-dark"
628
- >
629
- M:N
630
- </Typography>
631
- </div>
632
- <div className="flex items-center gap-1.5">
633
- <div
634
- className="w-6 h-0.5 rounded"
635
- style={{
636
- backgroundImage:
637
- "repeating-linear-gradient(90deg, #94a3b8, #94a3b8 4px, transparent 4px, transparent 7px)"
638
- }}
639
- />
640
- <Typography
641
- variant="caption"
642
- className="text-[10px] text-text-disabled dark:text-text-disabled-dark"
643
- >
644
- Inverse
645
- </Typography>
646
- </div>
647
- <div className="h-3 w-px bg-surface-200 dark:bg-surface-700"/>
648
- <div className="flex items-center gap-1">
649
- <span className="text-[9px]">🔑</span>
650
- <Typography
651
- variant="caption"
652
- className="text-[10px] text-text-disabled dark:text-text-disabled-dark"
653
- >
654
- PK
655
- </Typography>
656
- </div>
657
- <div className="flex items-center gap-1">
658
- <span className="text-[9px]">🔗</span>
659
- <Typography
660
- variant="caption"
661
- className="text-[10px] text-text-disabled dark:text-text-disabled-dark"
662
- >
663
- FK
664
- </Typography>
665
- </div>
666
- </div>
667
- </div>
668
- </div>
669
- }
670
- />
671
- );
672
- }
673
-
674
- // ─── Outer wrapper (provides ReactFlowProvider) ────────────────────────
675
-
676
- export const SchemaVisualizer = () => {
677
- const { collections: registryCollections } =
678
- useStudioCollectionRegistry() as {
679
- collections?: AdminCollection[];
680
- };
681
-
682
- // Merge registry collections with any passed collections
683
- const collections = useMemo(() => {
684
- return registryCollections ?? [];
685
- }, [registryCollections]);
686
-
687
- if (!collections || collections.length === 0) {
688
- return (
689
- <div className="flex items-center justify-center h-full w-full">
690
- <div className="text-center space-y-3">
691
- <CircularProgress size="small"/>
692
- <Typography
693
- variant="body2"
694
- color="secondary"
695
- >
696
- Loading schema…
697
- </Typography>
698
- </div>
699
- </div>
700
- );
701
- }
702
-
703
- return (
704
- <div className="flex h-full w-full bg-white dark:bg-surface-950 overflow-hidden text-text-primary dark:text-text-primary-dark">
705
- <ReactFlowProvider>
706
- <SchemaVisualizerCanvas collections={collections}/>
707
- </ReactFlowProvider>
708
- </div>
709
- );
710
- };