@rebasepro/studio 0.17.3 → 0.18.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 +0 -1
- package/README.md +12 -4
- package/dist/{ApiKeysView-DiCurTEU.js → ApiKeysView-EIvegrUk.js} +54 -39
- package/dist/ApiKeysView-EIvegrUk.js.map +1 -0
- package/dist/{BackupsView-DNS6LdVg.js → BackupsView-DVkKzaPR.js} +29 -31
- package/dist/BackupsView-DVkKzaPR.js.map +1 -0
- package/dist/{BranchesView-CM3B-ER3.js → BranchesView-Df8hQ8wE.js} +85 -24
- package/dist/BranchesView-Df8hQ8wE.js.map +1 -0
- package/dist/{CronJobsView-BHtJZeJZ.js → CronJobsView-CLOGjb-L.js} +32 -27
- package/dist/CronJobsView-CLOGjb-L.js.map +1 -0
- package/dist/{RLSEditor-CBX4uD66.js → RLSEditor-Sa1D34ny.js} +724 -615
- package/dist/RLSEditor-Sa1D34ny.js.map +1 -0
- package/dist/{SQLEditor-ByecwX8B.js → SQLEditor-BfNA8xM3.js} +9 -4
- package/dist/{SQLEditor-ByecwX8B.js.map → SQLEditor-BfNA8xM3.js.map} +1 -1
- package/dist/{SchemaVisualizer-D6tMZgeC.js → SchemaVisualizer-DTDMYL6o.js} +84 -110
- package/dist/SchemaVisualizer-DTDMYL6o.js.map +1 -0
- package/dist/{StorageView-BGDeC5GI.js → StorageView-Ed9Q_VlB.js} +5 -32
- package/dist/StorageView-Ed9Q_VlB.js.map +1 -0
- package/dist/components/RLSEditor/saveRules.d.ts +38 -0
- package/dist/components/RebaseStudio.d.ts +1 -1
- package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +5 -2
- package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +0 -4
- package/dist/components/load-failure-view.d.ts +17 -0
- package/dist/components/load-failure.d.ts +47 -0
- package/dist/index.es.js +56 -36
- package/dist/index.es.js.map +1 -1
- package/dist/load-failure-C6OMJSGC.js +31 -0
- package/dist/load-failure-C6OMJSGC.js.map +1 -0
- package/dist/load-failure-view-CBTB_h-h.js +48 -0
- package/dist/load-failure-view-CBTB_h-h.js.map +1 -0
- package/package.json +36 -21
- package/dist/ApiKeysView-DiCurTEU.js.map +0 -1
- package/dist/BackupsView-DNS6LdVg.js.map +0 -1
- package/dist/BranchesView-CM3B-ER3.js.map +0 -1
- package/dist/CronJobsView-BHtJZeJZ.js.map +0 -1
- package/dist/RLSEditor-CBX4uD66.js.map +0 -1
- package/dist/SchemaVisualizer-D6tMZgeC.js.map +0 -1
- package/dist/StorageView-BGDeC5GI.js.map +0 -1
- package/dist/components/StorageView/storage-failure.d.ts +0 -34
- package/src/components/ApiExplorer/ApiExplorer.tsx +0 -299
- package/src/components/ApiExplorer/EndpointDetail.tsx +0 -279
- package/src/components/ApiExplorer/TryItPanel.tsx +0 -499
- package/src/components/ApiExplorer/parseSpec.ts +0 -104
- package/src/components/ApiExplorer/types.ts +0 -84
- package/src/components/ApiKeys/ApiKeysView.tsx +0 -479
- package/src/components/ApiKeys/CreateApiKeyDialog.tsx +0 -711
- package/src/components/ApiKeys/permissions.ts +0 -145
- package/src/components/AuthSimulationSelector.tsx +0 -72
- package/src/components/Backups/BackupsView.tsx +0 -185
- package/src/components/Branches/BranchesView.tsx +0 -386
- package/src/components/CronJobs/CronJobsView.tsx +0 -413
- package/src/components/JSEditor/JSEditor.tsx +0 -1100
- package/src/components/JSEditor/JSEditorSidebar.tsx +0 -350
- package/src/components/JSEditor/JSMonacoEditor.tsx +0 -410
- package/src/components/LogsExplorer/LogsExplorer.tsx +0 -273
- package/src/components/LogsExplorer/useLogTail.ts +0 -356
- package/src/components/RLSEditor/PolicyEditor.tsx +0 -399
- package/src/components/RLSEditor/RLSEditor.tsx +0 -1128
- package/src/components/RLSEditor/index.ts +0 -1
- package/src/components/RLSEditor/policy-presets.ts +0 -176
- package/src/components/RLSEditor/saveSecurityRules.ts +0 -48
- package/src/components/RebaseStudio.tsx +0 -174
- package/src/components/SQLEditor/ExplainVisualizer.tsx +0 -128
- package/src/components/SQLEditor/MonacoEditor.tsx +0 -203
- package/src/components/SQLEditor/SQLEditor.tsx +0 -1518
- package/src/components/SQLEditor/SQLEditorSidebar.tsx +0 -173
- package/src/components/SQLEditor/SchemaBrowser.tsx +0 -168
- package/src/components/SQLEditor/sql_editor_types.ts +0 -18
- package/src/components/SchemaVisualizer/RelationEdge.tsx +0 -102
- package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +0 -710
- package/src/components/SchemaVisualizer/TableNode.tsx +0 -257
- package/src/components/SchemaVisualizer/schema-visualizer.utils.ts +0 -140
- package/src/components/SchemaVisualizer/useLiveRls.ts +0 -86
- package/src/components/SchemaVisualizer/useSchemaGraph.ts +0 -426
- package/src/components/StorageView/StorageView.tsx +0 -1588
- package/src/components/StorageView/storage-failure.ts +0 -63
- package/src/components/StudioHomePage.tsx +0 -430
- package/src/index.ts +0 -31
- package/src/utils/parseSpec.test.ts +0 -309
- package/src/utils/sql_utils.test.ts +0 -265
- 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
|
-
};
|