@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.
- 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,426 +0,0 @@
|
|
|
1
|
-
import { useMemo, useState, useCallback, useEffect } from "react";
|
|
2
|
-
import type { Node, Edge } from "@xyflow/react";
|
|
3
|
-
import { MarkerType } from "@xyflow/react";
|
|
4
|
-
import { isPostgresCollectionConfig } from "@rebasepro/types";
|
|
5
|
-
import type { Relation, ResolvedRelation } from "@rebasepro/types";
|
|
6
|
-
import { resolveCollectionRelations } from "@rebasepro/common";
|
|
7
|
-
import { getLayoutedElements, getCardinalityLabel, getTypeLabel, NODE_WIDTH } from "./schema-visualizer.utils";
|
|
8
|
-
import type { LayoutDirection, RelationEdgeData } from "./schema-visualizer.utils";
|
|
9
|
-
import type { AdminCollection, AdminPostgresCollection } from "@rebasepro/cms-types";
|
|
10
|
-
|
|
11
|
-
// ─── Column info extracted from a collection ──────────────────────────
|
|
12
|
-
|
|
13
|
-
export interface ColumnInfo {
|
|
14
|
-
name: string;
|
|
15
|
-
type: string;
|
|
16
|
-
typeLabel: string;
|
|
17
|
-
isPrimaryKey: boolean;
|
|
18
|
-
isForeignKey: boolean;
|
|
19
|
-
isRequired: boolean;
|
|
20
|
-
isEnum: boolean;
|
|
21
|
-
enumValues?: string[];
|
|
22
|
-
relationName?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ─── Node data shape ──────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
export interface TableNodeData {
|
|
28
|
-
tableName: string;
|
|
29
|
-
collectionName: string;
|
|
30
|
-
slug: string;
|
|
31
|
-
columns: ColumnInfo[];
|
|
32
|
-
isJunction: boolean;
|
|
33
|
-
isUnmanaged: boolean;
|
|
34
|
-
rlsEnabled: boolean;
|
|
35
|
-
historyEnabled: boolean;
|
|
36
|
-
icon?: string;
|
|
37
|
-
[key: string]: unknown;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ─── Extract columns from a AdminCollection ─────────────────────────
|
|
41
|
-
|
|
42
|
-
const extractColumns = (collection: AdminCollection): ColumnInfo[] => {
|
|
43
|
-
const columns: ColumnInfo[] = [];
|
|
44
|
-
const properties = collection.properties ?? {};
|
|
45
|
-
|
|
46
|
-
for (const [propName, prop] of Object.entries(properties)) {
|
|
47
|
-
if (prop.type === "relation") continue; // Relations are shown as edges, not columns
|
|
48
|
-
|
|
49
|
-
const isPk =
|
|
50
|
-
("isId" in prop && Boolean(prop.isId)) ||
|
|
51
|
-
(!Object.values(properties).some(
|
|
52
|
-
(p) => "isId" in p && Boolean(p.isId)
|
|
53
|
-
) &&
|
|
54
|
-
propName === "id");
|
|
55
|
-
|
|
56
|
-
const isEnum = prop.type === "string" && "enum" in prop && Boolean(prop.enum);
|
|
57
|
-
|
|
58
|
-
let enumValues: string[] | undefined;
|
|
59
|
-
if (isEnum && "enum" in prop && prop.enum) {
|
|
60
|
-
const enumProp = prop.enum;
|
|
61
|
-
if (Array.isArray(enumProp)) {
|
|
62
|
-
enumValues = enumProp.map((v: unknown) =>
|
|
63
|
-
typeof v === "string" ? v : String((v as Record<string, unknown>)?.id ?? v)
|
|
64
|
-
);
|
|
65
|
-
} else if (typeof enumProp === "object" && enumProp !== null) {
|
|
66
|
-
enumValues = Object.keys(enumProp);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
columns.push({
|
|
71
|
-
name: propName,
|
|
72
|
-
type: prop.type,
|
|
73
|
-
typeLabel: getTypeLabel(prop.type),
|
|
74
|
-
isPrimaryKey: isPk,
|
|
75
|
-
isForeignKey: false,
|
|
76
|
-
isRequired: Boolean(prop.validation?.required),
|
|
77
|
-
isEnum,
|
|
78
|
-
enumValues
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Add FK columns from owning relations
|
|
83
|
-
if (isPostgresCollectionConfig(collection)) {
|
|
84
|
-
try {
|
|
85
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
86
|
-
for (const rel of Object.values(resolvedRelations)) {
|
|
87
|
-
if (rel.kind === "belongsTo" && rel.localKey) {
|
|
88
|
-
// Only add if not already present as a regular column
|
|
89
|
-
if (!columns.some((c) => c.name === rel.localKey)) {
|
|
90
|
-
columns.push({
|
|
91
|
-
name: rel.localKey,
|
|
92
|
-
type: "number",
|
|
93
|
-
typeLabel: "FK",
|
|
94
|
-
isPrimaryKey: false,
|
|
95
|
-
isForeignKey: true,
|
|
96
|
-
isRequired: false,
|
|
97
|
-
isEnum: false,
|
|
98
|
-
relationName: rel.relationName
|
|
99
|
-
});
|
|
100
|
-
} else {
|
|
101
|
-
// Mark existing column as FK
|
|
102
|
-
const existing = columns.find((c) => c.name === rel.localKey);
|
|
103
|
-
if (existing) {
|
|
104
|
-
existing.isForeignKey = true;
|
|
105
|
-
existing.relationName = rel.relationName;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
} catch {
|
|
111
|
-
// Ignore resolution errors
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return columns;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
// ─── Build graph from collections ─────────────────────────────────────
|
|
119
|
-
|
|
120
|
-
const buildGraph = (
|
|
121
|
-
collections: AdminCollection[],
|
|
122
|
-
direction: LayoutDirection,
|
|
123
|
-
liveRls: Set<string> | null
|
|
124
|
-
): { nodes: Node[]; edges: Edge[] } => {
|
|
125
|
-
/**
|
|
126
|
-
* Is RLS on for this table?
|
|
127
|
-
*
|
|
128
|
-
* The database is the authority whenever we could reach it. Declaring rules
|
|
129
|
-
* in code is not the same as having them applied, and the hosted console
|
|
130
|
-
* never sees the declarations at all — see `useLiveRlsTables`. Only when
|
|
131
|
-
* there is no SQL capability do we fall back to what the config says.
|
|
132
|
-
*/
|
|
133
|
-
const isRlsEnabled = (schema: string, tableName: string, declaresRules: boolean): boolean =>
|
|
134
|
-
liveRls ? liveRls.has(`${schema}.${tableName}`) : declaresRules;
|
|
135
|
-
|
|
136
|
-
const nodes: Node[] = [];
|
|
137
|
-
const edges: Edge[] = [];
|
|
138
|
-
const tableToNodeId = new Map<string, string>();
|
|
139
|
-
// Track columns per node so we can resolve handle IDs for edges
|
|
140
|
-
const nodeColumns = new Map<string, ColumnInfo[]>();
|
|
141
|
-
|
|
142
|
-
// Helper: find the PK column name for a node (falls back to "id")
|
|
143
|
-
const getPkHandle = (nodeId: string): string => {
|
|
144
|
-
const cols = nodeColumns.get(nodeId);
|
|
145
|
-
const pk = cols?.find((c) => c.isPrimaryKey);
|
|
146
|
-
return pk ? `target-${pk.name}` : "target-default";
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
// Helper: find the FK column name for a node pointing to a relation
|
|
150
|
-
const getFkHandle = (nodeId: string, localKey: string | undefined): string => {
|
|
151
|
-
if (!localKey) return "source-default";
|
|
152
|
-
const cols = nodeColumns.get(nodeId);
|
|
153
|
-
const fk = cols?.find((c) => c.name === localKey);
|
|
154
|
-
return fk ? `source-${fk.name}` : "source-default";
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
// 1. Create nodes for each collection
|
|
158
|
-
for (const collection of collections) {
|
|
159
|
-
if (!isPostgresCollectionConfig(collection)) continue;
|
|
160
|
-
|
|
161
|
-
const tableName = collection.table ?? collection.slug;
|
|
162
|
-
const nodeId = `table-${tableName}`;
|
|
163
|
-
tableToNodeId.set(tableName, nodeId);
|
|
164
|
-
|
|
165
|
-
const columns = extractColumns(collection);
|
|
166
|
-
nodeColumns.set(nodeId, columns);
|
|
167
|
-
|
|
168
|
-
const nodeData: TableNodeData = {
|
|
169
|
-
tableName,
|
|
170
|
-
collectionName: collection.name,
|
|
171
|
-
slug: collection.slug,
|
|
172
|
-
columns,
|
|
173
|
-
isJunction: false,
|
|
174
|
-
isUnmanaged: false,
|
|
175
|
-
rlsEnabled: isRlsEnabled(
|
|
176
|
-
collection.schema ?? "public",
|
|
177
|
-
tableName,
|
|
178
|
-
Boolean(collection.securityRules && collection.securityRules.length > 0)
|
|
179
|
-
),
|
|
180
|
-
historyEnabled: Boolean(collection.history),
|
|
181
|
-
icon: typeof collection.icon === "string" ? collection.icon : undefined
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
nodes.push({
|
|
185
|
-
id: nodeId,
|
|
186
|
-
type: "tableNode",
|
|
187
|
-
position: { x: 0,
|
|
188
|
-
y: 0 },
|
|
189
|
-
data: nodeData
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// 2. Create junction table nodes and edges
|
|
194
|
-
const processedJunctions = new Set<string>();
|
|
195
|
-
|
|
196
|
-
for (const collection of collections) {
|
|
197
|
-
if (!isPostgresCollectionConfig(collection)) continue;
|
|
198
|
-
|
|
199
|
-
const tableName = collection.table ?? collection.slug;
|
|
200
|
-
const sourceNodeId = tableToNodeId.get(tableName);
|
|
201
|
-
if (!sourceNodeId) continue;
|
|
202
|
-
|
|
203
|
-
let resolvedRelations: Record<string, ResolvedRelation>;
|
|
204
|
-
try {
|
|
205
|
-
resolvedRelations = resolveCollectionRelations(collection);
|
|
206
|
-
} catch {
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
for (const [relationKey, rel] of Object.entries(resolvedRelations)) {
|
|
211
|
-
let targetCollection: AdminCollection;
|
|
212
|
-
try {
|
|
213
|
-
targetCollection = rel.target();
|
|
214
|
-
} catch {
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (!isPostgresCollectionConfig(targetCollection)) continue;
|
|
219
|
-
|
|
220
|
-
const targetTable = targetCollection.table ?? targetCollection.slug;
|
|
221
|
-
const targetNodeId = tableToNodeId.get(targetTable);
|
|
222
|
-
if (!targetNodeId) continue;
|
|
223
|
-
|
|
224
|
-
// Draw the side that owns the storage: a foreign key on this table,
|
|
225
|
-
// or the junction. `hasOne`/`hasMany` are the mirror image of a
|
|
226
|
-
// `belongsTo` that is drawn from the other end, and `via` has no
|
|
227
|
-
// single edge to draw.
|
|
228
|
-
if (rel.kind !== "belongsTo" && rel.kind !== "manyToMany") continue;
|
|
229
|
-
|
|
230
|
-
const direction = "owning" as const;
|
|
231
|
-
const edgeData: RelationEdgeData = {
|
|
232
|
-
cardinality: rel.cardinality,
|
|
233
|
-
direction,
|
|
234
|
-
relationName: rel.relationName ?? relationKey,
|
|
235
|
-
hasJunction: rel.kind === "manyToMany",
|
|
236
|
-
hasJoinPath: false,
|
|
237
|
-
label: getCardinalityLabel(rel.cardinality, direction)
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
if (rel.kind === "manyToMany" && !processedJunctions.has(rel.through.table)) {
|
|
241
|
-
// Many-to-many: create junction node + two edges
|
|
242
|
-
processedJunctions.add(rel.through.table);
|
|
243
|
-
const junctionNodeId = `junction-${rel.through.table}`;
|
|
244
|
-
|
|
245
|
-
const junctionColumns: ColumnInfo[] = [
|
|
246
|
-
{
|
|
247
|
-
name: rel.through.sourceColumn,
|
|
248
|
-
type: "number",
|
|
249
|
-
typeLabel: "FK",
|
|
250
|
-
isPrimaryKey: true,
|
|
251
|
-
isForeignKey: true,
|
|
252
|
-
isRequired: true,
|
|
253
|
-
isEnum: false
|
|
254
|
-
},
|
|
255
|
-
{
|
|
256
|
-
name: rel.through.targetColumn,
|
|
257
|
-
type: "number",
|
|
258
|
-
typeLabel: "FK",
|
|
259
|
-
isPrimaryKey: true,
|
|
260
|
-
isForeignKey: true,
|
|
261
|
-
isRequired: true,
|
|
262
|
-
isEnum: false
|
|
263
|
-
}
|
|
264
|
-
];
|
|
265
|
-
nodeColumns.set(junctionNodeId, junctionColumns);
|
|
266
|
-
|
|
267
|
-
nodes.push({
|
|
268
|
-
id: junctionNodeId,
|
|
269
|
-
type: "tableNode",
|
|
270
|
-
position: { x: 0,
|
|
271
|
-
y: 0 },
|
|
272
|
-
data: {
|
|
273
|
-
tableName: rel.through.table,
|
|
274
|
-
collectionName: rel.through.table,
|
|
275
|
-
slug: rel.through.table,
|
|
276
|
-
columns: junctionColumns,
|
|
277
|
-
isJunction: true,
|
|
278
|
-
isUnmanaged: false,
|
|
279
|
-
// Junction tables get derived policies rather than
|
|
280
|
-
// declared ones, so the config can never say — but the
|
|
281
|
-
// database can, and usually says yes.
|
|
282
|
-
rlsEnabled: isRlsEnabled("public", rel.through.table, false),
|
|
283
|
-
historyEnabled: false
|
|
284
|
-
} satisfies TableNodeData
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
// Source table → junction (junction references source PK)
|
|
288
|
-
const sourcePk = nodeColumns.get(sourceNodeId)?.find((c) => c.isPrimaryKey);
|
|
289
|
-
edges.push({
|
|
290
|
-
id: `edge-${sourceNodeId}-${junctionNodeId}`,
|
|
291
|
-
source: sourceNodeId,
|
|
292
|
-
target: junctionNodeId,
|
|
293
|
-
sourceHandle: sourcePk ? `source-${sourcePk.name}` : "source-default",
|
|
294
|
-
targetHandle: `target-${rel.through.sourceColumn}`,
|
|
295
|
-
type: "relationEdge",
|
|
296
|
-
data: { ...edgeData,
|
|
297
|
-
label: "1:N" } as Record<string, unknown>,
|
|
298
|
-
markerEnd: { type: MarkerType.ArrowClosed,
|
|
299
|
-
width: 16,
|
|
300
|
-
height: 16 }
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// Junction → target table
|
|
304
|
-
edges.push({
|
|
305
|
-
id: `edge-${junctionNodeId}-${targetNodeId}`,
|
|
306
|
-
source: junctionNodeId,
|
|
307
|
-
target: targetNodeId,
|
|
308
|
-
sourceHandle: `source-${rel.through.targetColumn}`,
|
|
309
|
-
targetHandle: getPkHandle(targetNodeId),
|
|
310
|
-
type: "relationEdge",
|
|
311
|
-
data: { ...edgeData,
|
|
312
|
-
label: "N:1" } as Record<string, unknown>,
|
|
313
|
-
markerEnd: { type: MarkerType.ArrowClosed,
|
|
314
|
-
width: 16,
|
|
315
|
-
height: 16 }
|
|
316
|
-
});
|
|
317
|
-
} else if (rel.kind === "belongsTo") {
|
|
318
|
-
// A foreign key on this table.
|
|
319
|
-
edges.push({
|
|
320
|
-
id: `edge-${sourceNodeId}-${targetNodeId}-${relationKey}`,
|
|
321
|
-
source: sourceNodeId,
|
|
322
|
-
target: targetNodeId,
|
|
323
|
-
sourceHandle: getFkHandle(sourceNodeId, rel.localKey),
|
|
324
|
-
targetHandle: getPkHandle(targetNodeId),
|
|
325
|
-
type: "relationEdge",
|
|
326
|
-
data: { ...edgeData } as Record<string, unknown>,
|
|
327
|
-
markerEnd: { type: MarkerType.ArrowClosed,
|
|
328
|
-
width: 16,
|
|
329
|
-
height: 16 }
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
// 3. Apply dagre layout first to get node positions
|
|
335
|
-
const layoutResult = getLayoutedElements(nodes, edges, direction);
|
|
336
|
-
|
|
337
|
-
// 4. Build a position lookup from the laid-out nodes
|
|
338
|
-
const nodePositions = new Map<string, { x: number }>();
|
|
339
|
-
for (const node of layoutResult.nodes) {
|
|
340
|
-
nodePositions.set(node.id, { x: node.position.x });
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// 5. Resolve handle sides based on relative node positions.
|
|
344
|
-
// If source is left of target → source exits Right, target enters Left.
|
|
345
|
-
// If source is right of target → source exits Left, target enters Right.
|
|
346
|
-
for (const edge of layoutResult.edges) {
|
|
347
|
-
const srcPos = nodePositions.get(edge.source);
|
|
348
|
-
const tgtPos = nodePositions.get(edge.target);
|
|
349
|
-
|
|
350
|
-
const srcBaseHandle = (edge.sourceHandle ?? "source-default") as string;
|
|
351
|
-
const tgtBaseHandle = (edge.targetHandle ?? "target-default") as string;
|
|
352
|
-
|
|
353
|
-
if (srcPos && tgtPos) {
|
|
354
|
-
const srcCenterX = srcPos.x + NODE_WIDTH / 2;
|
|
355
|
-
const tgtCenterX = tgtPos.x + NODE_WIDTH / 2;
|
|
356
|
-
const sourceIsLeft = srcCenterX <= tgtCenterX;
|
|
357
|
-
|
|
358
|
-
edge.sourceHandle = `${srcBaseHandle}-${sourceIsLeft ? "right" : "left"}`;
|
|
359
|
-
edge.targetHandle = `${tgtBaseHandle}-${sourceIsLeft ? "left" : "right"}`;
|
|
360
|
-
} else {
|
|
361
|
-
edge.sourceHandle = `${srcBaseHandle}-right`;
|
|
362
|
-
edge.targetHandle = `${tgtBaseHandle}-left`;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return layoutResult;
|
|
367
|
-
};
|
|
368
|
-
|
|
369
|
-
// ─── Hook ─────────────────────────────────────────────────────────────
|
|
370
|
-
|
|
371
|
-
export interface UseSchemaGraphResult {
|
|
372
|
-
nodes: Node[];
|
|
373
|
-
edges: Edge[];
|
|
374
|
-
direction: LayoutDirection;
|
|
375
|
-
setDirection: (dir: LayoutDirection) => void;
|
|
376
|
-
relayout: () => void;
|
|
377
|
-
isLoading: boolean;
|
|
378
|
-
tableCount: number;
|
|
379
|
-
relationCount: number;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
export const useSchemaGraph = (
|
|
383
|
-
collections: AdminCollection[] | undefined,
|
|
384
|
-
/**
|
|
385
|
-
* `schema.table` for every table the database reports RLS on, or `null`
|
|
386
|
-
* when there is no SQL capability to ask. See {@link useLiveRlsTables}.
|
|
387
|
-
*/
|
|
388
|
-
liveRls: Set<string> | null = null
|
|
389
|
-
): UseSchemaGraphResult => {
|
|
390
|
-
const [direction, setDirection] = useState<LayoutDirection>("LR");
|
|
391
|
-
const [version, setVersion] = useState(0);
|
|
392
|
-
|
|
393
|
-
const relayout = useCallback(() => setVersion((v) => v + 1), []);
|
|
394
|
-
|
|
395
|
-
// Re-layout on direction change
|
|
396
|
-
useEffect(() => {
|
|
397
|
-
setVersion((v) => v + 1);
|
|
398
|
-
}, [direction]);
|
|
399
|
-
|
|
400
|
-
const { nodes, edges, tableCount, relationCount } = useMemo(() => {
|
|
401
|
-
if (!collections || collections.length === 0) {
|
|
402
|
-
return { nodes: [],
|
|
403
|
-
edges: [],
|
|
404
|
-
tableCount: 0,
|
|
405
|
-
relationCount: 0 };
|
|
406
|
-
}
|
|
407
|
-
const result = buildGraph(collections, direction, liveRls);
|
|
408
|
-
return {
|
|
409
|
-
...result,
|
|
410
|
-
tableCount: result.nodes.length,
|
|
411
|
-
relationCount: result.edges.length
|
|
412
|
-
};
|
|
413
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
414
|
-
}, [collections, direction, version, liveRls]);
|
|
415
|
-
|
|
416
|
-
return {
|
|
417
|
-
nodes,
|
|
418
|
-
edges,
|
|
419
|
-
direction,
|
|
420
|
-
setDirection,
|
|
421
|
-
relayout,
|
|
422
|
-
isLoading: !collections,
|
|
423
|
-
tableCount,
|
|
424
|
-
relationCount
|
|
425
|
-
};
|
|
426
|
-
};
|