@rebasepro/studio 0.24.0 → 0.24.1-canary.gcb918fc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ApiExplorer-CCtczj3F.js → ApiExplorer-Bzt7Ye49.js} +5 -3
- package/dist/ApiExplorer-Bzt7Ye49.js.map +1 -0
- package/dist/{ApiKeysView-B66TdzhH.js → ApiKeysView-Bqf6XYR1.js} +2 -1
- package/dist/{ApiKeysView-B66TdzhH.js.map → ApiKeysView-Bqf6XYR1.js.map} +1 -1
- package/dist/{BackupsView-DmSMzAbT.js → BackupsView-BhOHB6iP.js} +6 -20
- package/dist/BackupsView-BhOHB6iP.js.map +1 -0
- package/dist/{BranchesView-DP1a87E-.js → BranchesView-BHfwGTYc.js} +5 -3
- package/dist/BranchesView-BHfwGTYc.js.map +1 -0
- package/dist/{CronJobsView-TDErHTuK.js → CronJobsView-CMNK7eWO.js} +52 -59
- package/dist/CronJobsView-CMNK7eWO.js.map +1 -0
- package/dist/{JSEditor-B59pwg4c.js → JSEditor-DIO2Mw8q.js} +115 -96
- package/dist/JSEditor-DIO2Mw8q.js.map +1 -0
- package/dist/{LogsExplorer-BKM4yfP8.js → LogsExplorer-C-bwDFIl.js} +19 -23
- package/dist/LogsExplorer-C-bwDFIl.js.map +1 -0
- package/dist/{RLSEditor-BlLLV6xs.js → RLSEditor-jFD_d06f.js} +10 -8
- package/dist/RLSEditor-jFD_d06f.js.map +1 -0
- package/dist/{SQLEditor-1An7w0j6.js → SQLEditor-D5Ci4pyW.js} +372 -545
- package/dist/SQLEditor-D5Ci4pyW.js.map +1 -0
- package/dist/{SchemaVisualizer-DCLVfA5t.js → SchemaVisualizer-D-rAqwul.js} +479 -206
- package/dist/SchemaVisualizer-D-rAqwul.js.map +1 -0
- package/dist/{StorageView-CbsHuCBf.js → StorageView-CPkDKbsX.js} +498 -241
- package/dist/StorageView-CPkDKbsX.js.map +1 -0
- package/dist/assets/dagre-layout.worker-BFnhAzuR.js.map +1 -0
- package/dist/components/SQLEditor/SQLEditor.d.ts +11 -0
- package/dist/components/SchemaVisualizer/dagre-layout.d.ts +32 -0
- package/dist/components/SchemaVisualizer/dagre-layout.worker.d.ts +13 -0
- package/dist/components/SchemaVisualizer/layout-runner.d.ts +23 -0
- package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +21 -10
- package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +38 -1
- package/dist/components/StorageView/useVisibleFileMetadata.d.ts +46 -0
- package/dist/dagre-layout.worker-j9NbH9Gb.js +27 -0
- package/dist/dagre-layout.worker-j9NbH9Gb.js.map +1 -0
- package/dist/index.es.js +16 -14
- package/dist/index.es.js.map +1 -1
- package/dist/sql_utils-BZD31xE3.js +478 -0
- package/dist/sql_utils-BZD31xE3.js.map +1 -0
- package/dist/utils/sql_utils.d.ts +60 -0
- package/package.json +9 -9
- package/dist/ApiExplorer-CCtczj3F.js.map +0 -1
- package/dist/BackupsView-DmSMzAbT.js.map +0 -1
- package/dist/BranchesView-DP1a87E-.js.map +0 -1
- package/dist/CronJobsView-TDErHTuK.js.map +0 -1
- package/dist/JSEditor-B59pwg4c.js.map +0 -1
- package/dist/LogsExplorer-BKM4yfP8.js.map +0 -1
- package/dist/RLSEditor-BlLLV6xs.js.map +0 -1
- package/dist/SQLEditor-1An7w0j6.js.map +0 -1
- package/dist/SchemaVisualizer-DCLVfA5t.js.map +0 -1
- package/dist/StorageView-CbsHuCBf.js.map +0 -1
|
@@ -1,12 +1,101 @@
|
|
|
1
1
|
import { IconForView, useRebaseContext, useStudioCollectionRegistry, useTranslation } from "@rebasepro/app";
|
|
2
2
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { Chip, CircularProgress, IconButton, ResizablePanels, SearchBar, Tooltip, Typography, cls, defaultBorderMixin, useIsDarkMode } from "@rebasepro/ui";
|
|
3
|
+
import { Chip, CircularProgress, IconButton, ResizablePanels, SearchBar, ToggleButtonGroup, Tooltip, Typography, cls, defaultBorderMixin, useIsDarkMode } from "@rebasepro/ui";
|
|
4
4
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import { isPostgresCollectionConfig } from "@rebasepro/types";
|
|
6
6
|
import { getTableName, resolveCollectionRelations } from "@rebasepro/common";
|
|
7
|
-
import { Background, BackgroundVariant, BaseEdge, Controls, EdgeLabelRenderer, Handle, MarkerType, MiniMap, Position, ReactFlow, ReactFlowProvider, applyEdgeChanges, applyNodeChanges, getSmoothStepPath,
|
|
7
|
+
import { Background, BackgroundVariant, BaseEdge, Controls, EdgeLabelRenderer, Handle, MarkerType, MiniMap, Position, ReactFlow, ReactFlowProvider, applyEdgeChanges, applyNodeChanges, getSmoothStepPath, useReactFlow } from "@xyflow/react";
|
|
8
8
|
import "@xyflow/react/dist/style.css";
|
|
9
9
|
import dagre from "dagre";
|
|
10
|
+
//#region src/components/SchemaVisualizer/dagre-layout.ts
|
|
11
|
+
/**
|
|
12
|
+
* Place the graph with dagre, top to bottom.
|
|
13
|
+
*
|
|
14
|
+
* An ERD read as a column of tables fits the tall, narrow canvas the
|
|
15
|
+
* visualizer is given, and it is the only layout the visualizer offers.
|
|
16
|
+
* Deterministic: the same input always gives the same positions.
|
|
17
|
+
*
|
|
18
|
+
* The cost is in dagre's ordering phase and grows fast with cross-linked
|
|
19
|
+
* graphs — 5 s at 200 tables with two relations each, 90 s at 500 — which is
|
|
20
|
+
* why the visualizer runs this in a worker (`dagre-layout.worker.ts`).
|
|
21
|
+
*/
|
|
22
|
+
function runDagre(input) {
|
|
23
|
+
const g = new dagre.graphlib.Graph();
|
|
24
|
+
g.setGraph({
|
|
25
|
+
rankdir: "TB",
|
|
26
|
+
nodesep: 100,
|
|
27
|
+
ranksep: 180,
|
|
28
|
+
edgesep: 60,
|
|
29
|
+
marginx: 60,
|
|
30
|
+
marginy: 60
|
|
31
|
+
});
|
|
32
|
+
g.setDefaultEdgeLabel(() => ({}));
|
|
33
|
+
for (const node of input.nodes) g.setNode(node.id, {
|
|
34
|
+
width: node.width,
|
|
35
|
+
height: node.height
|
|
36
|
+
});
|
|
37
|
+
for (const edge of input.edges) g.setEdge(edge.source, edge.target);
|
|
38
|
+
dagre.layout(g);
|
|
39
|
+
const positions = {};
|
|
40
|
+
for (const node of input.nodes) {
|
|
41
|
+
const placed = g.node(node.id);
|
|
42
|
+
positions[node.id] = {
|
|
43
|
+
x: placed.x - node.width / 2,
|
|
44
|
+
y: placed.y - node.height / 2
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return positions;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/components/SchemaVisualizer/layout-runner.ts
|
|
51
|
+
/**
|
|
52
|
+
* Whether this environment can start the layout worker.
|
|
53
|
+
*
|
|
54
|
+
* No in tests (jsdom has no `Worker`) and on a server render; there the
|
|
55
|
+
* layout runs where it is asked for, as it always did.
|
|
56
|
+
*/
|
|
57
|
+
var canLayOutOffThread = () => typeof Worker !== "undefined" && typeof Blob !== "undefined" && typeof URL !== "undefined" && typeof URL.createObjectURL === "function";
|
|
58
|
+
/**
|
|
59
|
+
* Lay a graph out in a worker of its own, which ends with the layout.
|
|
60
|
+
*
|
|
61
|
+
* The worker is inlined into the bundle (`?worker&inline`) rather than emitted
|
|
62
|
+
* as a file beside it: `@rebasepro/studio` is a library, and a worker file
|
|
63
|
+
* would be found only by an app bundler that rewrites
|
|
64
|
+
* `new URL(…, import.meta.url)` inside dependencies, which Vite's dependency
|
|
65
|
+
* pre-bundling does not. Imported on first use, so a page that never lays a
|
|
66
|
+
* graph out never decodes it.
|
|
67
|
+
*
|
|
68
|
+
* A worker that cannot be started or loaded — a Content-Security-Policy
|
|
69
|
+
* without `blob:` in `worker-src`, say — is not a broken diagram: the layout
|
|
70
|
+
* runs on the main thread instead, as it did before there was a worker.
|
|
71
|
+
*/
|
|
72
|
+
async function layOutOffThread(input) {
|
|
73
|
+
const { default: LayoutWorker } = await import("./dagre-layout.worker-j9NbH9Gb.js");
|
|
74
|
+
let worker;
|
|
75
|
+
try {
|
|
76
|
+
worker = new LayoutWorker({ name: "rebase-schema-layout" });
|
|
77
|
+
} catch {
|
|
78
|
+
return runDagre(input);
|
|
79
|
+
}
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
worker.addEventListener("message", (event) => {
|
|
82
|
+
worker.terminate();
|
|
83
|
+
const reply = event.data;
|
|
84
|
+
if ("positions" in reply) resolve(reply.positions);
|
|
85
|
+
else reject(new Error(reply.error));
|
|
86
|
+
});
|
|
87
|
+
worker.addEventListener("error", (event) => {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
worker.terminate();
|
|
90
|
+
try {
|
|
91
|
+
resolve(runDagre(input));
|
|
92
|
+
} catch (error) {
|
|
93
|
+
reject(error);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
worker.postMessage(input);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
10
99
|
/** Header with a single line of text (junction tables or tableName === collectionName). */
|
|
11
100
|
var HEADER_HEIGHT_SINGLE = 33;
|
|
12
101
|
/** Header with two lines (name + subtitle when collectionName !== tableName). */
|
|
@@ -29,57 +118,82 @@ var estimateNodeHeight = (columnCount, headerHeight = HEADER_HEIGHT_DOUBLE) => h
|
|
|
29
118
|
*/
|
|
30
119
|
var getColumnRowY = (rowIndex, headerHeight = HEADER_HEIGHT_DOUBLE) => headerHeight + rowIndex * ROW_HEIGHT + ROW_HEIGHT / 2;
|
|
31
120
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* The graph always ranks top to bottom: an ERD read as a column of tables
|
|
36
|
-
* fits the tall, narrow canvas the visualizer is given, and it is the only
|
|
37
|
-
* layout the visualizer offers.
|
|
121
|
+
* What dagre will be given for these nodes and edges: each node's id and
|
|
122
|
+
* box, each edge's ends, in order. The signature is all of it, so a changed
|
|
123
|
+
* schema — a table, a column count, a relation — is a different key.
|
|
38
124
|
*/
|
|
39
|
-
var
|
|
40
|
-
const
|
|
41
|
-
g.setGraph({
|
|
42
|
-
rankdir: "TB",
|
|
43
|
-
nodesep: 100,
|
|
44
|
-
ranksep: 180,
|
|
45
|
-
edgesep: 60,
|
|
46
|
-
marginx: 60,
|
|
47
|
-
marginy: 60
|
|
48
|
-
});
|
|
49
|
-
g.setDefaultEdgeLabel(() => ({}));
|
|
50
|
-
const nodeHeights = /* @__PURE__ */ new Map();
|
|
51
|
-
nodes.forEach((node) => {
|
|
52
|
-
const data = node.data;
|
|
53
|
-
const h = estimateNodeHeight(data.columns?.length ?? 3, getHeaderHeight({
|
|
54
|
-
isJunction: Boolean(data.isJunction),
|
|
55
|
-
collectionName: data.collectionName ?? "",
|
|
56
|
-
tableName: data.tableName ?? ""
|
|
57
|
-
}));
|
|
58
|
-
nodeHeights.set(node.id, h);
|
|
59
|
-
g.setNode(node.id, {
|
|
60
|
-
width: 280,
|
|
61
|
-
height: h
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
edges.forEach((edge) => {
|
|
65
|
-
g.setEdge(edge.source, edge.target);
|
|
66
|
-
});
|
|
67
|
-
dagre.layout(g);
|
|
68
|
-
return {
|
|
125
|
+
var planLayout = (nodes, edges) => {
|
|
126
|
+
const input = {
|
|
69
127
|
nodes: nodes.map((node) => {
|
|
70
|
-
const
|
|
71
|
-
const
|
|
128
|
+
const data = node.data;
|
|
129
|
+
const headerH = getHeaderHeight({
|
|
130
|
+
isJunction: Boolean(data.isJunction),
|
|
131
|
+
collectionName: data.collectionName ?? "",
|
|
132
|
+
tableName: data.tableName ?? ""
|
|
133
|
+
});
|
|
72
134
|
return {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
y: nodeWithPosition.y - h / 2
|
|
77
|
-
}
|
|
135
|
+
id: node.id,
|
|
136
|
+
width: 280,
|
|
137
|
+
height: estimateNodeHeight(data.columns?.length ?? 3, headerH)
|
|
78
138
|
};
|
|
79
139
|
}),
|
|
80
|
-
edges
|
|
140
|
+
edges: edges.map((edge) => ({
|
|
141
|
+
source: edge.source,
|
|
142
|
+
target: edge.target
|
|
143
|
+
}))
|
|
144
|
+
};
|
|
145
|
+
return {
|
|
146
|
+
input,
|
|
147
|
+
signature: `${input.nodes.map((node) => `${node.id}:${node.height}`).join("|")}#${input.edges.map((edge) => `${edge.source}>${edge.target}`).join("|")}`
|
|
81
148
|
};
|
|
82
149
|
};
|
|
150
|
+
/**
|
|
151
|
+
* Positions of the graphs laid out in this page's life, by
|
|
152
|
+
* {@link LayoutPlan.signature}: the most recent few.
|
|
153
|
+
*
|
|
154
|
+
* Dagre takes 4.5–5 s at 200 cross-linked tables and 90 s at 500, so a graph
|
|
155
|
+
* is laid out once: not again when the live RLS answer arrives (a badge, not
|
|
156
|
+
* the graph), nor when the view is reopened. The layout is deterministic, so
|
|
157
|
+
* an entry never goes stale. Holds geometry only, nothing read from the
|
|
158
|
+
* database.
|
|
159
|
+
*/
|
|
160
|
+
var layoutCache = /* @__PURE__ */ new Map();
|
|
161
|
+
var LAYOUT_CACHE_SIZE = 16;
|
|
162
|
+
var rememberLayout = (signature, positions) => {
|
|
163
|
+
layoutCache.set(signature, positions);
|
|
164
|
+
if (layoutCache.size > LAYOUT_CACHE_SIZE) {
|
|
165
|
+
const oldest = layoutCache.keys().next().value;
|
|
166
|
+
if (oldest !== void 0) layoutCache.delete(oldest);
|
|
167
|
+
}
|
|
168
|
+
return positions;
|
|
169
|
+
};
|
|
170
|
+
/** The positions for this plan, if it has been laid out before. */
|
|
171
|
+
var cachedLayout = (plan) => layoutCache.get(plan.signature);
|
|
172
|
+
/** Lay the plan out here, on the calling thread. */
|
|
173
|
+
var layOutNow = (plan) => cachedLayout(plan) ?? rememberLayout(plan.signature, runDagre(plan.input));
|
|
174
|
+
/** Layouts running in a worker, by signature, so a second ask waits for the first. */
|
|
175
|
+
var layoutsInFlight = /* @__PURE__ */ new Map();
|
|
176
|
+
/**
|
|
177
|
+
* Lay the plan out in a worker; the main thread stays free while it runs.
|
|
178
|
+
* See {@link layOutOffThread}.
|
|
179
|
+
*/
|
|
180
|
+
var layOutInBackground = (plan) => {
|
|
181
|
+
const cached = cachedLayout(plan);
|
|
182
|
+
if (cached) return Promise.resolve(cached);
|
|
183
|
+
const running = layoutsInFlight.get(plan.signature);
|
|
184
|
+
if (running) return running;
|
|
185
|
+
const job = layOutOffThread(plan.input).then((positions) => rememberLayout(plan.signature, positions)).finally(() => layoutsInFlight.delete(plan.signature));
|
|
186
|
+
layoutsInFlight.set(plan.signature, job);
|
|
187
|
+
return job;
|
|
188
|
+
};
|
|
189
|
+
/** The nodes at their laid-out positions, as new objects. */
|
|
190
|
+
var placeNodes = (nodes, positions) => nodes.map((node) => ({
|
|
191
|
+
...node,
|
|
192
|
+
position: { ...positions[node.id] ?? {
|
|
193
|
+
x: 0,
|
|
194
|
+
y: 0
|
|
195
|
+
} }
|
|
196
|
+
}));
|
|
83
197
|
var getCardinalityLabel = (cardinality, direction) => {
|
|
84
198
|
if (cardinality === "many") return "M:N";
|
|
85
199
|
if (direction === "inverse") return "1:1 ←";
|
|
@@ -307,52 +421,151 @@ var buildGraph = (collections, liveRls) => {
|
|
|
307
421
|
});
|
|
308
422
|
}
|
|
309
423
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
424
|
+
return {
|
|
425
|
+
nodes,
|
|
426
|
+
edges
|
|
427
|
+
};
|
|
428
|
+
};
|
|
429
|
+
/**
|
|
430
|
+
* The graph at its laid-out positions, with each edge's handles on the sides
|
|
431
|
+
* that face each other: a source left of its target exits right and enters
|
|
432
|
+
* the target from the left, and the other way round.
|
|
433
|
+
*/
|
|
434
|
+
var placeGraph = (graph, positions) => {
|
|
435
|
+
return {
|
|
436
|
+
nodes: placeNodes(graph.nodes, positions),
|
|
437
|
+
edges: graph.edges.map((edge) => {
|
|
438
|
+
const srcPos = positions[edge.source];
|
|
439
|
+
const tgtPos = positions[edge.target];
|
|
440
|
+
const srcBaseHandle = edge.sourceHandle ?? "source-default";
|
|
441
|
+
const tgtBaseHandle = edge.targetHandle ?? "target-default";
|
|
442
|
+
if (!srcPos || !tgtPos) return {
|
|
443
|
+
...edge,
|
|
444
|
+
sourceHandle: `${srcBaseHandle}-right`,
|
|
445
|
+
targetHandle: `${tgtBaseHandle}-left`
|
|
446
|
+
};
|
|
319
447
|
const sourceIsLeft = srcPos.x + 280 / 2 <= tgtPos.x + 280 / 2;
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
448
|
+
return {
|
|
449
|
+
...edge,
|
|
450
|
+
sourceHandle: `${srcBaseHandle}-${sourceIsLeft ? "right" : "left"}`,
|
|
451
|
+
targetHandle: `${tgtBaseHandle}-${sourceIsLeft ? "left" : "right"}`
|
|
452
|
+
};
|
|
453
|
+
})
|
|
454
|
+
};
|
|
455
|
+
};
|
|
456
|
+
var isJunctionNode = (node) => Boolean(node.data.isJunction);
|
|
457
|
+
/**
|
|
458
|
+
* The tables within `depth` relations of `focus`, and every relation between
|
|
459
|
+
* them.
|
|
460
|
+
*
|
|
461
|
+
* A junction table is not a step of its own: a many-to-many is one relation,
|
|
462
|
+
* drawn through its junction. So distances are counted in half steps — an edge
|
|
463
|
+
* that touches a junction is one, a foreign key between two tables is two.
|
|
464
|
+
*/
|
|
465
|
+
var neighbourhoodOf = (graph, focus, depth) => {
|
|
466
|
+
const junctions = new Set(graph.nodes.filter(isJunctionNode).map((node) => node.id));
|
|
467
|
+
const adjacent = /* @__PURE__ */ new Map();
|
|
468
|
+
for (const edge of graph.edges) {
|
|
469
|
+
adjacent.set(edge.source, [...adjacent.get(edge.source) ?? [], edge.target]);
|
|
470
|
+
adjacent.set(edge.target, [...adjacent.get(edge.target) ?? [], edge.source]);
|
|
471
|
+
}
|
|
472
|
+
const limit = depth * 2;
|
|
473
|
+
const distance = /* @__PURE__ */ new Map([[focus, 0]]);
|
|
474
|
+
const byDistance = Array.from({ length: limit + 1 }, () => []);
|
|
475
|
+
byDistance[0].push(focus);
|
|
476
|
+
for (let reached = 0; reached <= limit; reached++) for (const id of byDistance[reached]) {
|
|
477
|
+
if (distance.get(id) !== reached) continue;
|
|
478
|
+
for (const next of adjacent.get(id) ?? []) {
|
|
479
|
+
const step = junctions.has(id) || junctions.has(next) ? 1 : 2;
|
|
480
|
+
const nextDistance = reached + step;
|
|
481
|
+
if (nextDistance > limit || (distance.get(next) ?? Infinity) <= nextDistance) continue;
|
|
482
|
+
distance.set(next, nextDistance);
|
|
483
|
+
byDistance[nextDistance].push(next);
|
|
325
484
|
}
|
|
326
485
|
}
|
|
327
|
-
return
|
|
486
|
+
return {
|
|
487
|
+
nodes: graph.nodes.filter((node) => distance.has(node.id)),
|
|
488
|
+
edges: graph.edges.filter((edge) => distance.has(edge.source) && distance.has(edge.target))
|
|
489
|
+
};
|
|
490
|
+
};
|
|
491
|
+
/** The table with the most relations: where a large schema opens. */
|
|
492
|
+
var mostConnectedTable = (graph) => {
|
|
493
|
+
const degree = /* @__PURE__ */ new Map();
|
|
494
|
+
for (const edge of graph.edges) {
|
|
495
|
+
degree.set(edge.source, (degree.get(edge.source) ?? 0) + 1);
|
|
496
|
+
degree.set(edge.target, (degree.get(edge.target) ?? 0) + 1);
|
|
497
|
+
}
|
|
498
|
+
let best;
|
|
499
|
+
for (const node of graph.nodes) {
|
|
500
|
+
if (isJunctionNode(node)) continue;
|
|
501
|
+
if (!best || (degree.get(node.id) ?? 0) > (degree.get(best.id) ?? 0)) best = node;
|
|
502
|
+
}
|
|
503
|
+
return best?.id;
|
|
504
|
+
};
|
|
505
|
+
var EMPTY_GRAPH = {
|
|
506
|
+
nodes: [],
|
|
507
|
+
edges: []
|
|
328
508
|
};
|
|
329
|
-
var
|
|
509
|
+
var NO_POSITIONS = {};
|
|
510
|
+
var useSchemaGraph = (collections, liveRls = null, focus = null, depth = 1) => {
|
|
330
511
|
const [version, setVersion] = useState(0);
|
|
331
512
|
const relayout = useCallback(() => setVersion((v) => v + 1), []);
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
513
|
+
const graph = useMemo(() => collections && collections.length > 0 ? buildGraph(collections, liveRls) : EMPTY_GRAPH, [collections, liveRls]);
|
|
514
|
+
const tables = graph.nodes.filter((node) => !isJunctionNode(node)).length;
|
|
515
|
+
const focused = tables > 300 ? (focus && graph.nodes.some((node) => node.id === focus) ? focus : mostConnectedTable(graph)) ?? null : null;
|
|
516
|
+
const drawn = useMemo(() => focused ? neighbourhoodOf(graph, focused, depth) : graph, [
|
|
517
|
+
graph,
|
|
518
|
+
focused,
|
|
519
|
+
depth
|
|
520
|
+
]);
|
|
521
|
+
const plan = useMemo(() => planLayout(drawn.nodes, drawn.edges), [drawn]);
|
|
522
|
+
/**
|
|
523
|
+
* Where the layout runs. Off the main thread wherever a worker can start;
|
|
524
|
+
* in tests and on a server, here, as the visualizer always did.
|
|
525
|
+
*/
|
|
526
|
+
const offThread = canLayOutOffThread();
|
|
527
|
+
const [laidOut, setLaidOut] = useState(null);
|
|
528
|
+
const [layoutError, setLayoutError] = useState(null);
|
|
529
|
+
const positions = drawn.nodes.length === 0 ? NO_POSITIONS : cachedLayout(plan) ?? (laidOut?.signature === plan.signature ? laidOut.positions : void 0) ?? (offThread ? void 0 : layOutNow(plan));
|
|
530
|
+
useEffect(() => {
|
|
531
|
+
if (!offThread || drawn.nodes.length === 0 || cachedLayout(plan)) return;
|
|
532
|
+
let current = true;
|
|
533
|
+
layOutInBackground(plan).then((result) => {
|
|
534
|
+
if (current) setLaidOut({
|
|
535
|
+
signature: plan.signature,
|
|
536
|
+
positions: result
|
|
537
|
+
});
|
|
538
|
+
}, (error) => {
|
|
539
|
+
if (current) setLayoutError(error);
|
|
540
|
+
});
|
|
541
|
+
return () => {
|
|
542
|
+
current = false;
|
|
344
543
|
};
|
|
345
544
|
}, [
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
545
|
+
plan,
|
|
546
|
+
offThread,
|
|
547
|
+
drawn
|
|
548
|
+
]);
|
|
549
|
+
const placed = useMemo(() => positions ? placeGraph(drawn, positions) : EMPTY_GRAPH, [
|
|
550
|
+
drawn,
|
|
551
|
+
positions,
|
|
552
|
+
version
|
|
349
553
|
]);
|
|
554
|
+
if (layoutError) throw layoutError;
|
|
350
555
|
return {
|
|
351
|
-
nodes,
|
|
352
|
-
edges,
|
|
556
|
+
nodes: placed.nodes,
|
|
557
|
+
edges: placed.edges,
|
|
558
|
+
allNodes: graph.nodes,
|
|
353
559
|
relayout,
|
|
354
|
-
tableCount,
|
|
355
|
-
relationCount
|
|
560
|
+
tableCount: graph.nodes.length,
|
|
561
|
+
relationCount: graph.edges.length,
|
|
562
|
+
layingOut: positions === void 0,
|
|
563
|
+
neighbourhood: focused ? {
|
|
564
|
+
focus: focused,
|
|
565
|
+
depth,
|
|
566
|
+
shown: drawn.nodes.length,
|
|
567
|
+
total: tables
|
|
568
|
+
} : null
|
|
356
569
|
};
|
|
357
570
|
};
|
|
358
571
|
//#endregion
|
|
@@ -699,43 +912,44 @@ function SchemaVisualizerCanvas({ collections }) {
|
|
|
699
912
|
const reactFlowInstance = useReactFlow();
|
|
700
913
|
const liveRls = useLiveRlsTables();
|
|
701
914
|
const isDark = useIsDarkMode();
|
|
702
|
-
const
|
|
915
|
+
const [focus, setFocus] = useState(null);
|
|
916
|
+
const [depth, setDepth] = useState(1);
|
|
917
|
+
const { nodes: layoutedNodes, edges: layoutedEdges, allNodes, relayout, tableCount, relationCount, layingOut, neighbourhood } = useSchemaGraph(collections, liveRls, focus, depth);
|
|
703
918
|
const [nodes, setNodes] = useState([]);
|
|
704
919
|
const [edges, setEdges] = useState([]);
|
|
705
920
|
const [selectedTable, setSelectedTable] = useState(null);
|
|
706
921
|
const [searchQuery, setSearchQuery] = useState("");
|
|
707
|
-
const initialFitDone = useRef(false);
|
|
708
922
|
useEffect(() => {
|
|
923
|
+
if (layingOut) return;
|
|
709
924
|
setNodes(layoutedNodes);
|
|
710
925
|
setEdges(layoutedEdges);
|
|
711
|
-
}, [
|
|
926
|
+
}, [
|
|
927
|
+
layoutedNodes,
|
|
928
|
+
layoutedEdges,
|
|
929
|
+
layingOut
|
|
930
|
+
]);
|
|
712
931
|
const onNodesChange = useCallback((changes) => setNodes((nds) => applyNodeChanges(changes, nds)), []);
|
|
713
932
|
const onEdgesChange = useCallback((changes) => setEdges((eds) => applyEdgeChanges(changes, eds)), []);
|
|
714
933
|
/**
|
|
715
|
-
* Fit the graph into view
|
|
934
|
+
* Fit the graph into view when it is first drawn, and again whenever a
|
|
935
|
+
* different neighbourhood replaces it.
|
|
716
936
|
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
*
|
|
723
|
-
* scatter of specks in a corner and stayed there until you hit "fit" by
|
|
724
|
-
* hand. `useNodesInitialized` is the event this was approximating.
|
|
937
|
+
* `fitView` needs every node's real width and height, which React Flow
|
|
938
|
+
* only knows once it has rendered and measured them; a fit made before
|
|
939
|
+
* that leaves the ERD a scatter of specks in a corner. React Flow's own
|
|
940
|
+
* `fitView` waits for the measurement, so it is asked for in the commit
|
|
941
|
+
* that hands it the new nodes — after its store has them, which a
|
|
942
|
+
* child's effect does before this one runs.
|
|
725
943
|
*/
|
|
726
|
-
const
|
|
944
|
+
const fitPending = useRef(true);
|
|
727
945
|
useEffect(() => {
|
|
728
|
-
if (!
|
|
729
|
-
|
|
946
|
+
if (!fitPending.current || nodes.length === 0) return;
|
|
947
|
+
fitPending.current = false;
|
|
730
948
|
reactFlowInstance.fitView({
|
|
731
949
|
padding: .15,
|
|
732
950
|
duration: 400
|
|
733
951
|
});
|
|
734
|
-
}, [
|
|
735
|
-
nodesInitialized,
|
|
736
|
-
nodes.length,
|
|
737
|
-
reactFlowInstance
|
|
738
|
-
]);
|
|
952
|
+
}, [nodes, reactFlowInstance]);
|
|
739
953
|
const handleFitView = useCallback(() => {
|
|
740
954
|
reactFlowInstance.fitView({
|
|
741
955
|
padding: .15,
|
|
@@ -750,12 +964,27 @@ function SchemaVisualizerCanvas({ collections }) {
|
|
|
750
964
|
}, []);
|
|
751
965
|
const handleTableSelect = useCallback((nodeId) => {
|
|
752
966
|
setSelectedTable(nodeId);
|
|
967
|
+
if (neighbourhood) {
|
|
968
|
+
if (neighbourhood.focus !== nodeId) {
|
|
969
|
+
fitPending.current = true;
|
|
970
|
+
setFocus(nodeId);
|
|
971
|
+
}
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
753
974
|
const node = nodes.find((n) => n.id === nodeId);
|
|
754
975
|
if (node) reactFlowInstance.setCenter(node.position.x + 140, node.position.y + 60, {
|
|
755
976
|
zoom: 1.2,
|
|
756
977
|
duration: 400
|
|
757
978
|
});
|
|
758
|
-
}, [
|
|
979
|
+
}, [
|
|
980
|
+
nodes,
|
|
981
|
+
reactFlowInstance,
|
|
982
|
+
neighbourhood
|
|
983
|
+
]);
|
|
984
|
+
const handleDepthChange = useCallback((value) => {
|
|
985
|
+
fitPending.current = true;
|
|
986
|
+
setDepth(value === "2" ? 2 : 1);
|
|
987
|
+
}, []);
|
|
759
988
|
const [sidebarSize, setSidebarSize] = useState(() => {
|
|
760
989
|
try {
|
|
761
990
|
const saved = localStorage.getItem("rebase_schema_viz_sidebar_size");
|
|
@@ -775,30 +1004,34 @@ function SchemaVisualizerCanvas({ collections }) {
|
|
|
775
1004
|
const q = searchQuery.toLowerCase();
|
|
776
1005
|
return postgresCollections.filter((c) => c.name.toLowerCase().includes(q) || getTableName(c).toLowerCase().includes(q) || c.slug?.toLowerCase().includes(q));
|
|
777
1006
|
}, [postgresCollections, searchQuery]);
|
|
778
|
-
const junctionNodes = useMemo(() =>
|
|
1007
|
+
const junctionNodes = useMemo(() => allNodes.filter((n) => n.data.isJunction), [allNodes]);
|
|
779
1008
|
/**
|
|
780
1009
|
* The RLS answer for one collection, from the same place the graph used.
|
|
781
1010
|
*
|
|
782
|
-
*
|
|
783
|
-
* that is what kept the sidebar dots, the node badges and the
|
|
784
|
-
* agreeing.
|
|
1011
|
+
* The graph's nodes already carry it, so look it up there rather than
|
|
1012
|
+
* re-deriving — that is what kept the sidebar dots, the node badges and the
|
|
1013
|
+
* total from agreeing. Every node, not only the drawn ones: a large schema
|
|
1014
|
+
* draws a neighbourhood, and the list shows every table.
|
|
785
1015
|
*/
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1016
|
+
const rlsByNodeId = useMemo(() => new Map(allNodes.map((n) => [n.id, Boolean(n.data.rlsEnabled)])), [allNodes]);
|
|
1017
|
+
const rlsEnabledFor = useCallback((collection) => rlsByNodeId.get(`table-${getTableName(collection)}`) ?? false, [rlsByNodeId]);
|
|
1018
|
+
const focusName = useMemo(() => {
|
|
1019
|
+
const node = neighbourhood ? allNodes.find((n) => n.id === neighbourhood.focus) : void 0;
|
|
1020
|
+
return node ? node.data.collectionName : "";
|
|
1021
|
+
}, [allNodes, neighbourhood]);
|
|
790
1022
|
const stats = useMemo(() => ({
|
|
791
1023
|
tables: tableCount,
|
|
792
1024
|
relations: relationCount,
|
|
793
1025
|
junctions: junctionNodes.length,
|
|
794
|
-
withRls:
|
|
1026
|
+
withRls: allNodes.filter((n) => n.data?.rlsEnabled).length
|
|
795
1027
|
}), [
|
|
796
1028
|
tableCount,
|
|
797
1029
|
relationCount,
|
|
798
1030
|
junctionNodes.length,
|
|
799
|
-
|
|
1031
|
+
allNodes
|
|
800
1032
|
]);
|
|
801
1033
|
return /* @__PURE__ */ jsx(ResizablePanels, {
|
|
1034
|
+
dividerAriaLabel: t("resize_sidebar"),
|
|
802
1035
|
orientation: "horizontal",
|
|
803
1036
|
panelSizePercent: sidebarSize,
|
|
804
1037
|
onPanelSizeChange: setSidebarSize,
|
|
@@ -1013,104 +1246,144 @@ function SchemaVisualizerCanvas({ collections }) {
|
|
|
1013
1246
|
})]
|
|
1014
1247
|
}), /* @__PURE__ */ jsxs("div", {
|
|
1015
1248
|
className: "flex-grow relative",
|
|
1016
|
-
children: [
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
className: "text-[10px] text-text-disabled dark:text-text-disabled-dark",
|
|
1067
|
-
children: "Owning"
|
|
1068
|
-
})]
|
|
1069
|
-
}),
|
|
1070
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1071
|
-
className: "flex items-center gap-1.5",
|
|
1072
|
-
children: [/* @__PURE__ */ jsx("div", { className: "w-6 h-0.5 bg-violet-500 rounded" }), /* @__PURE__ */ jsx(Typography, {
|
|
1073
|
-
variant: "caption",
|
|
1074
|
-
className: "text-[10px] text-text-disabled dark:text-text-disabled-dark",
|
|
1075
|
-
children: "M:N"
|
|
1076
|
-
})]
|
|
1077
|
-
}),
|
|
1078
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1079
|
-
className: "flex items-center gap-1.5",
|
|
1080
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
1081
|
-
className: "w-6 h-0.5 rounded",
|
|
1082
|
-
style: { backgroundImage: "repeating-linear-gradient(90deg, #94a3b8, #94a3b8 4px, transparent 4px, transparent 7px)" }
|
|
1083
|
-
}), /* @__PURE__ */ jsx(Typography, {
|
|
1084
|
-
variant: "caption",
|
|
1085
|
-
className: "text-[10px] text-text-disabled dark:text-text-disabled-dark",
|
|
1086
|
-
children: "Inverse"
|
|
1087
|
-
})]
|
|
1088
|
-
}),
|
|
1089
|
-
/* @__PURE__ */ jsx("div", { className: "h-3 w-px bg-surface-raised" }),
|
|
1090
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1091
|
-
className: "flex items-center gap-1",
|
|
1092
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
1093
|
-
className: "text-[9px]",
|
|
1094
|
-
children: "🔑"
|
|
1095
|
-
}), /* @__PURE__ */ jsx(Typography, {
|
|
1249
|
+
children: [
|
|
1250
|
+
/* @__PURE__ */ jsxs(ReactFlow, {
|
|
1251
|
+
nodes,
|
|
1252
|
+
edges,
|
|
1253
|
+
onNodesChange,
|
|
1254
|
+
onEdgesChange,
|
|
1255
|
+
onNodeClick: handleNodeClick,
|
|
1256
|
+
onPaneClick: handlePaneClick,
|
|
1257
|
+
nodeTypes,
|
|
1258
|
+
edgeTypes,
|
|
1259
|
+
fitView: true,
|
|
1260
|
+
fitViewOptions: { padding: .15 },
|
|
1261
|
+
minZoom: .1,
|
|
1262
|
+
maxZoom: 2,
|
|
1263
|
+
proOptions: { hideAttribution: true },
|
|
1264
|
+
colorMode: isDark ? "dark" : "light",
|
|
1265
|
+
className: "bg-surface-well",
|
|
1266
|
+
children: [
|
|
1267
|
+
/* @__PURE__ */ jsx(Background, {
|
|
1268
|
+
variant: BackgroundVariant.Dots,
|
|
1269
|
+
gap: 20,
|
|
1270
|
+
size: 1,
|
|
1271
|
+
className: "!bg-surface-well",
|
|
1272
|
+
color: isDark ? "#2a2a2a" : "#d9d9dc"
|
|
1273
|
+
}),
|
|
1274
|
+
/* @__PURE__ */ jsx(Controls, {
|
|
1275
|
+
showInteractive: false,
|
|
1276
|
+
className: "!mb-16 !bg-surface-card !border !border-hairline !shadow-sm !rounded-lg [--xy-controls-button-background-color:var(--color-surface-card)] [--xy-controls-button-background-color-hover:var(--color-surface-card-hover)] [--xy-controls-button-color:var(--color-text-secondary)] dark:[--xy-controls-button-color:var(--color-text-secondary-dark)] [--xy-controls-button-color-hover:var(--color-text-primary)] dark:[--xy-controls-button-color-hover:var(--color-text-primary-dark)] [--xy-controls-button-border-color:var(--color-hairline)]"
|
|
1277
|
+
}),
|
|
1278
|
+
/* @__PURE__ */ jsx(MiniMap, {
|
|
1279
|
+
nodeStrokeColor: (n) => {
|
|
1280
|
+
const d = n.data;
|
|
1281
|
+
if (d.isJunction) return "#a78bfa";
|
|
1282
|
+
if (d.rlsEnabled) return "#22c55e";
|
|
1283
|
+
return "#6366f1";
|
|
1284
|
+
},
|
|
1285
|
+
nodeColor: (n) => {
|
|
1286
|
+
if (n.data.isJunction) return isDark ? "#3b2f6b" : "#ede9fe";
|
|
1287
|
+
return isDark ? "#2b2d4a" : "#eef2ff";
|
|
1288
|
+
},
|
|
1289
|
+
maskColor: isDark ? "rgba(0,0,0,0.35)" : "rgba(0,0,0,0.08)",
|
|
1290
|
+
className: "!bg-surface-card !border !border-hairline !shadow-sm !rounded-lg"
|
|
1291
|
+
})
|
|
1292
|
+
]
|
|
1293
|
+
}),
|
|
1294
|
+
neighbourhood && /* @__PURE__ */ jsx("div", {
|
|
1295
|
+
className: "absolute top-4 left-4 right-4 flex justify-start pointer-events-none",
|
|
1296
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1297
|
+
className: "flex flex-wrap items-center gap-2 max-w-full px-3 py-2 bg-surface-card/90 backdrop-blur-sm rounded-lg border border-hairline shadow-sm pointer-events-auto",
|
|
1298
|
+
children: [/* @__PURE__ */ jsx(Typography, {
|
|
1096
1299
|
variant: "caption",
|
|
1097
|
-
className: "text-
|
|
1098
|
-
children: "
|
|
1300
|
+
className: "text-text-secondary dark:text-text-secondary-dark",
|
|
1301
|
+
children: t("studio_schema_neighbourhood", {
|
|
1302
|
+
shown: neighbourhood.shown,
|
|
1303
|
+
total: neighbourhood.total,
|
|
1304
|
+
table: focusName
|
|
1305
|
+
})
|
|
1306
|
+
}), /* @__PURE__ */ jsx(ToggleButtonGroup, {
|
|
1307
|
+
value: String(neighbourhood.depth),
|
|
1308
|
+
onValueChange: handleDepthChange,
|
|
1309
|
+
options: [{
|
|
1310
|
+
value: "1",
|
|
1311
|
+
label: t("studio_schema_neighbourhood_depth_1")
|
|
1312
|
+
}, {
|
|
1313
|
+
value: "2",
|
|
1314
|
+
label: t("studio_schema_neighbourhood_depth_2")
|
|
1315
|
+
}]
|
|
1099
1316
|
})]
|
|
1100
|
-
})
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1317
|
+
})
|
|
1318
|
+
}),
|
|
1319
|
+
layingOut && /* @__PURE__ */ jsx("div", {
|
|
1320
|
+
role: "status",
|
|
1321
|
+
className: "absolute inset-0 flex items-center justify-center pointer-events-none",
|
|
1322
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1323
|
+
className: "flex items-center gap-2 px-3 py-2 bg-surface-card/90 backdrop-blur-sm rounded-lg border border-hairline shadow-sm",
|
|
1324
|
+
children: [/* @__PURE__ */ jsx(CircularProgress, { size: "smallest" }), /* @__PURE__ */ jsx(Typography, {
|
|
1107
1325
|
variant: "caption",
|
|
1108
|
-
className: "text-
|
|
1109
|
-
children: "
|
|
1326
|
+
className: "text-text-secondary dark:text-text-secondary-dark",
|
|
1327
|
+
children: t("studio_schema_laying_out")
|
|
1110
1328
|
})]
|
|
1111
1329
|
})
|
|
1112
|
-
|
|
1113
|
-
|
|
1330
|
+
}),
|
|
1331
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1332
|
+
className: "absolute bottom-4 left-4 flex items-center gap-3 px-3 py-2 bg-surface-card/90 backdrop-blur-sm rounded-lg border border-hairline shadow-sm",
|
|
1333
|
+
children: [
|
|
1334
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1335
|
+
className: "flex items-center gap-1.5",
|
|
1336
|
+
children: [/* @__PURE__ */ jsx("div", { className: "w-6 h-0.5 bg-indigo-500 rounded" }), /* @__PURE__ */ jsx(Typography, {
|
|
1337
|
+
variant: "caption",
|
|
1338
|
+
className: "text-[10px] text-text-disabled dark:text-text-disabled-dark",
|
|
1339
|
+
children: "Owning"
|
|
1340
|
+
})]
|
|
1341
|
+
}),
|
|
1342
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1343
|
+
className: "flex items-center gap-1.5",
|
|
1344
|
+
children: [/* @__PURE__ */ jsx("div", { className: "w-6 h-0.5 bg-violet-500 rounded" }), /* @__PURE__ */ jsx(Typography, {
|
|
1345
|
+
variant: "caption",
|
|
1346
|
+
className: "text-[10px] text-text-disabled dark:text-text-disabled-dark",
|
|
1347
|
+
children: "M:N"
|
|
1348
|
+
})]
|
|
1349
|
+
}),
|
|
1350
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1351
|
+
className: "flex items-center gap-1.5",
|
|
1352
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1353
|
+
className: "w-6 h-0.5 rounded",
|
|
1354
|
+
style: { backgroundImage: "repeating-linear-gradient(90deg, #94a3b8, #94a3b8 4px, transparent 4px, transparent 7px)" }
|
|
1355
|
+
}), /* @__PURE__ */ jsx(Typography, {
|
|
1356
|
+
variant: "caption",
|
|
1357
|
+
className: "text-[10px] text-text-disabled dark:text-text-disabled-dark",
|
|
1358
|
+
children: "Inverse"
|
|
1359
|
+
})]
|
|
1360
|
+
}),
|
|
1361
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 w-px bg-surface-raised" }),
|
|
1362
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1363
|
+
className: "flex items-center gap-1",
|
|
1364
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1365
|
+
className: "text-[9px]",
|
|
1366
|
+
children: "🔑"
|
|
1367
|
+
}), /* @__PURE__ */ jsx(Typography, {
|
|
1368
|
+
variant: "caption",
|
|
1369
|
+
className: "text-[10px] text-text-disabled dark:text-text-disabled-dark",
|
|
1370
|
+
children: "PK"
|
|
1371
|
+
})]
|
|
1372
|
+
}),
|
|
1373
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1374
|
+
className: "flex items-center gap-1",
|
|
1375
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1376
|
+
className: "text-[9px]",
|
|
1377
|
+
children: "🔗"
|
|
1378
|
+
}), /* @__PURE__ */ jsx(Typography, {
|
|
1379
|
+
variant: "caption",
|
|
1380
|
+
className: "text-[10px] text-text-disabled dark:text-text-disabled-dark",
|
|
1381
|
+
children: "FK"
|
|
1382
|
+
})]
|
|
1383
|
+
})
|
|
1384
|
+
]
|
|
1385
|
+
})
|
|
1386
|
+
]
|
|
1114
1387
|
})]
|
|
1115
1388
|
})
|
|
1116
1389
|
});
|
|
@@ -1173,4 +1446,4 @@ var SchemaVisualizer = () => {
|
|
|
1173
1446
|
//#endregion
|
|
1174
1447
|
export { SchemaVisualizer };
|
|
1175
1448
|
|
|
1176
|
-
//# sourceMappingURL=SchemaVisualizer-
|
|
1449
|
+
//# sourceMappingURL=SchemaVisualizer-D-rAqwul.js.map
|