@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.
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,6 +1,6 @@
1
- import { IconForView, useRebaseContext, useStudioCollectionRegistry } from "@rebasepro/app";
1
+ import { IconForView, useRebaseContext, useStudioCollectionRegistry, useTranslation } from "@rebasepro/app";
2
2
  import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
3
- import { Button, Chip, CircularProgress, IconButton, ResizablePanels, SearchBar, Tooltip, Typography, cls, defaultBorderMixin } from "@rebasepro/ui";
3
+ import { Chip, CircularProgress, IconButton, ResizablePanels, SearchBar, Tooltip, Typography, cls, defaultBorderMixin } from "@rebasepro/ui";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { isPostgresCollectionConfig } from "@rebasepro/types";
6
6
  import { resolveCollectionRelations } from "@rebasepro/common";
@@ -31,11 +31,15 @@ var getColumnRowY = (rowIndex, headerHeight = HEADER_HEIGHT_DOUBLE) => headerHei
31
31
  /**
32
32
  * Compute node positions using the dagre graph layout engine.
33
33
  * Returns a new array of nodes with updated `position`.
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.
34
38
  */
35
- var getLayoutedElements = (nodes, edges, direction = "LR") => {
39
+ var getLayoutedElements = (nodes, edges) => {
36
40
  const g = new dagre.graphlib.Graph();
37
41
  g.setGraph({
38
- rankdir: direction,
42
+ rankdir: "TB",
39
43
  nodesep: 100,
40
44
  ranksep: 180,
41
45
  edgesep: 60,
@@ -67,10 +71,6 @@ var getLayoutedElements = (nodes, edges, direction = "LR") => {
67
71
  const h = nodeHeights.get(node.id) ?? estimateNodeHeight(3);
68
72
  return {
69
73
  ...node,
70
- data: {
71
- ...node.data,
72
- layoutDirection: direction
73
- },
74
74
  position: {
75
75
  x: nodeWithPosition.x - 280 / 2,
76
76
  y: nodeWithPosition.y - h / 2
@@ -143,7 +143,7 @@ var extractColumns = (collection) => {
143
143
  } catch {}
144
144
  return columns;
145
145
  };
146
- var buildGraph = (collections, direction, liveRls) => {
146
+ var buildGraph = (collections, liveRls) => {
147
147
  /**
148
148
  * Is RLS on for this table?
149
149
  *
@@ -317,7 +317,7 @@ var buildGraph = (collections, direction, liveRls) => {
317
317
  });
318
318
  }
319
319
  }
320
- const layoutResult = getLayoutedElements(nodes, edges, direction);
320
+ const layoutResult = getLayoutedElements(nodes, edges);
321
321
  const nodePositions = /* @__PURE__ */ new Map();
322
322
  for (const node of layoutResult.nodes) nodePositions.set(node.id, { x: node.position.x });
323
323
  for (const edge of layoutResult.edges) {
@@ -337,12 +337,8 @@ var buildGraph = (collections, direction, liveRls) => {
337
337
  return layoutResult;
338
338
  };
339
339
  var useSchemaGraph = (collections, liveRls = null) => {
340
- const [direction, setDirection] = useState("LR");
341
340
  const [version, setVersion] = useState(0);
342
341
  const relayout = useCallback(() => setVersion((v) => v + 1), []);
343
- useEffect(() => {
344
- setVersion((v) => v + 1);
345
- }, [direction]);
346
342
  const { nodes, edges, tableCount, relationCount } = useMemo(() => {
347
343
  if (!collections || collections.length === 0) return {
348
344
  nodes: [],
@@ -350,7 +346,7 @@ var useSchemaGraph = (collections, liveRls = null) => {
350
346
  tableCount: 0,
351
347
  relationCount: 0
352
348
  };
353
- const result = buildGraph(collections, direction, liveRls);
349
+ const result = buildGraph(collections, liveRls);
354
350
  return {
355
351
  ...result,
356
352
  tableCount: result.nodes.length,
@@ -358,17 +354,13 @@ var useSchemaGraph = (collections, liveRls = null) => {
358
354
  };
359
355
  }, [
360
356
  collections,
361
- direction,
362
357
  version,
363
358
  liveRls
364
359
  ]);
365
360
  return {
366
361
  nodes,
367
362
  edges,
368
- direction,
369
- setDirection,
370
363
  relayout,
371
- isLoading: !collections,
372
364
  tableCount,
373
365
  relationCount
374
366
  };
@@ -714,7 +706,7 @@ var nodeTypes = { tableNode: TableNode };
714
706
  var edgeTypes = { relationEdge: RelationEdge };
715
707
  function SchemaVisualizerCanvas({ collections }) {
716
708
  const reactFlowInstance = useReactFlow();
717
- const { nodes: layoutedNodes, edges: layoutedEdges, direction, setDirection, relayout, tableCount, relationCount } = useSchemaGraph(collections, useLiveRlsTables());
709
+ const { nodes: layoutedNodes, edges: layoutedEdges, relayout, tableCount, relationCount } = useSchemaGraph(collections, useLiveRlsTables());
718
710
  const [nodes, setNodes] = useState([]);
719
711
  const [edges, setEdges] = useState([]);
720
712
  const [selectedTable, setSelectedTable] = useState(null);
@@ -751,30 +743,6 @@ function SchemaVisualizerCanvas({ collections }) {
751
743
  nodes.length,
752
744
  reactFlowInstance
753
745
  ]);
754
- /**
755
- * Re-fit after a relayout.
756
- *
757
- * Switching LR↔TB transposes the graph — a tall column becomes a wide band
758
- * — so the viewport that framed the old shape frames the new one badly:
759
- * asking for a different layout used to leave most of it off-screen until
760
- * you pressed "fit" as a second step. Skipped for the very first layout,
761
- * which the initial fit above already handles.
762
- */
763
- const lastFittedDirection = useRef(direction);
764
- useEffect(() => {
765
- if (!nodesInitialized || nodes.length === 0) return;
766
- if (lastFittedDirection.current === direction) return;
767
- lastFittedDirection.current = direction;
768
- reactFlowInstance.fitView({
769
- padding: .15,
770
- duration: 400
771
- });
772
- }, [
773
- direction,
774
- nodesInitialized,
775
- nodes,
776
- reactFlowInstance
777
- ]);
778
746
  const handleFitView = useCallback(() => {
779
747
  reactFlowInstance.fitView({
780
748
  padding: .15,
@@ -1007,71 +975,43 @@ function SchemaVisualizerCanvas({ collections }) {
1007
975
  })
1008
976
  }), /* @__PURE__ */ jsxs("div", {
1009
977
  className: "flex shrink-0 items-center gap-1.5",
1010
- children: [
1011
- /* @__PURE__ */ jsxs("div", {
1012
- className: "flex items-center bg-surface-100 dark:bg-surface-950 rounded-md border border-surface-200/40 dark:border-surface-700/40",
1013
- children: [/* @__PURE__ */ jsx(Tooltip, {
1014
- title: "Left to right layout",
1015
- children: /* @__PURE__ */ jsx(Button, {
1016
- size: "small",
1017
- variant: direction === "LR" ? "filled" : "text",
1018
- color: direction === "LR" ? "primary" : "neutral",
1019
- onClick: () => setDirection("LR"),
1020
- className: "!rounded-r-none !px-2 !py-1 !text-[10px] !font-mono",
1021
- children: "LR"
1022
- })
1023
- }), /* @__PURE__ */ jsx(Tooltip, {
1024
- title: "Top to bottom layout",
1025
- children: /* @__PURE__ */ jsx(Button, {
1026
- size: "small",
1027
- variant: direction === "TB" ? "filled" : "text",
1028
- color: direction === "TB" ? "primary" : "neutral",
1029
- onClick: () => setDirection("TB"),
1030
- className: "!rounded-l-none !px-2 !py-1 !text-[10px] !font-mono",
1031
- children: "TB"
1032
- })
1033
- })]
1034
- }),
1035
- /* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-surface-200 dark:bg-surface-950 mx-0.5" }),
1036
- /* @__PURE__ */ jsx(Tooltip, {
1037
- title: "Fit to view",
1038
- children: /* @__PURE__ */ jsx(IconButton, {
1039
- size: "small",
1040
- onClick: handleFitView,
1041
- children: /* @__PURE__ */ jsx("svg", {
1042
- className: "w-4 h-4",
1043
- fill: "none",
1044
- stroke: "currentColor",
1045
- viewBox: "0 0 24 24",
1046
- children: /* @__PURE__ */ jsx("path", {
1047
- strokeLinecap: "round",
1048
- strokeLinejoin: "round",
1049
- strokeWidth: 2,
1050
- d: "M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
1051
- })
978
+ children: [/* @__PURE__ */ jsx(Tooltip, {
979
+ title: "Fit to view",
980
+ children: /* @__PURE__ */ jsx(IconButton, {
981
+ size: "small",
982
+ onClick: handleFitView,
983
+ children: /* @__PURE__ */ jsx("svg", {
984
+ className: "w-4 h-4",
985
+ fill: "none",
986
+ stroke: "currentColor",
987
+ viewBox: "0 0 24 24",
988
+ children: /* @__PURE__ */ jsx("path", {
989
+ strokeLinecap: "round",
990
+ strokeLinejoin: "round",
991
+ strokeWidth: 2,
992
+ d: "M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
1052
993
  })
1053
994
  })
1054
- }),
1055
- /* @__PURE__ */ jsx(Tooltip, {
1056
- title: "Re-layout",
1057
- children: /* @__PURE__ */ jsx(IconButton, {
1058
- size: "small",
1059
- onClick: relayout,
1060
- children: /* @__PURE__ */ jsx("svg", {
1061
- className: "w-4 h-4",
1062
- fill: "none",
1063
- stroke: "currentColor",
1064
- viewBox: "0 0 24 24",
1065
- children: /* @__PURE__ */ jsx("path", {
1066
- strokeLinecap: "round",
1067
- strokeLinejoin: "round",
1068
- strokeWidth: 2,
1069
- 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"
1070
- })
995
+ })
996
+ }), /* @__PURE__ */ jsx(Tooltip, {
997
+ title: "Re-layout",
998
+ children: /* @__PURE__ */ jsx(IconButton, {
999
+ size: "small",
1000
+ onClick: relayout,
1001
+ children: /* @__PURE__ */ jsx("svg", {
1002
+ className: "w-4 h-4",
1003
+ fill: "none",
1004
+ stroke: "currentColor",
1005
+ viewBox: "0 0 24 24",
1006
+ children: /* @__PURE__ */ jsx("path", {
1007
+ strokeLinecap: "round",
1008
+ strokeLinejoin: "round",
1009
+ strokeWidth: 2,
1010
+ 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"
1071
1011
  })
1072
1012
  })
1073
1013
  })
1074
- ]
1014
+ })]
1075
1015
  })]
1076
1016
  }), /* @__PURE__ */ jsxs("div", {
1077
1017
  className: "flex-grow relative",
@@ -1177,11 +1117,20 @@ function SchemaVisualizerCanvas({ collections }) {
1177
1117
  });
1178
1118
  }
1179
1119
  var SchemaVisualizer = () => {
1120
+ const { t } = useTranslation();
1180
1121
  const { collections: registryCollections } = useStudioCollectionRegistry();
1181
- const collections = useMemo(() => {
1182
- return registryCollections ?? [];
1183
- }, [registryCollections]);
1184
- if (!collections || collections.length === 0) return /* @__PURE__ */ jsx("div", {
1122
+ /**
1123
+ * `undefined` and `[]` are different answers, and this view used to render
1124
+ * the same spinner for both. A project that declares no collections — a
1125
+ * fresh scaffold, a backend-only project someone opened the console
1126
+ * against — sat on "Loading schema…" forever, because there was nothing
1127
+ * coming.
1128
+ *
1129
+ * `undefined` is the Studio bridge before the admin's collection registry
1130
+ * has registered itself: genuinely still loading. `[]` is that registry
1131
+ * saying there is nothing to draw.
1132
+ */
1133
+ if (registryCollections === void 0) return /* @__PURE__ */ jsx("div", {
1185
1134
  className: "flex items-center justify-center h-full w-full",
1186
1135
  children: /* @__PURE__ */ jsxs("div", {
1187
1136
  className: "text-center space-y-3",
@@ -1192,12 +1141,37 @@ var SchemaVisualizer = () => {
1192
1141
  })]
1193
1142
  })
1194
1143
  });
1144
+ if (registryCollections.length === 0) return /* @__PURE__ */ jsx("div", {
1145
+ className: "flex items-center justify-center h-full w-full p-6",
1146
+ children: /* @__PURE__ */ jsxs("div", {
1147
+ className: "max-w-md text-center space-y-2",
1148
+ children: [
1149
+ /* @__PURE__ */ jsx(Typography, {
1150
+ variant: "subtitle2",
1151
+ className: "block",
1152
+ children: t("studio_schema_no_collections")
1153
+ }),
1154
+ /* @__PURE__ */ jsx(Typography, {
1155
+ variant: "body2",
1156
+ color: "secondary",
1157
+ className: "block",
1158
+ children: t("studio_schema_no_collections_body")
1159
+ }),
1160
+ /* @__PURE__ */ jsx(Typography, {
1161
+ variant: "body2",
1162
+ color: "secondary",
1163
+ className: "block",
1164
+ children: t("studio_schema_introspect_hint")
1165
+ })
1166
+ ]
1167
+ })
1168
+ });
1195
1169
  return /* @__PURE__ */ jsx("div", {
1196
1170
  className: "flex h-full w-full bg-white dark:bg-surface-950 overflow-hidden text-text-primary dark:text-text-primary-dark",
1197
- children: /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(SchemaVisualizerCanvas, { collections }) })
1171
+ children: /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(SchemaVisualizerCanvas, { collections: registryCollections }) })
1198
1172
  });
1199
1173
  };
1200
1174
  //#endregion
1201
1175
  export { SchemaVisualizer };
1202
1176
 
1203
- //# sourceMappingURL=SchemaVisualizer-D6tMZgeC.js.map
1177
+ //# sourceMappingURL=SchemaVisualizer-DTDMYL6o.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SchemaVisualizer-DTDMYL6o.js","names":[],"sources":["../src/components/SchemaVisualizer/schema-visualizer.utils.ts","../src/components/SchemaVisualizer/useSchemaGraph.ts","../src/components/SchemaVisualizer/useLiveRls.ts","../src/components/SchemaVisualizer/TableNode.tsx","../src/components/SchemaVisualizer/RelationEdge.tsx","../src/components/SchemaVisualizer/SchemaVisualizer.tsx"],"sourcesContent":["import dagre from \"dagre\";\nimport type { Node, Edge } from \"@xyflow/react\";\n\n// ─── Layout Constants ─────────────────────────────────────────────────\nexport const NODE_WIDTH = 280;\n/** Header with a single line of text (junction tables or tableName === collectionName). */\nconst HEADER_HEIGHT_SINGLE = 33;\n/** Header with two lines (name + subtitle when collectionName !== tableName). */\nconst HEADER_HEIGHT_DOUBLE = 47;\nconst ROW_HEIGHT = 28; // height per column row\n\n/**\n * Compute the correct header height for a table node.\n */\nexport const getHeaderHeight = (opts: {\n isJunction: boolean;\n collectionName: string;\n tableName: string;\n}): number => {\n if (opts.isJunction) return HEADER_HEIGHT_SINGLE;\n return opts.collectionName !== opts.tableName\n ? HEADER_HEIGHT_DOUBLE\n : HEADER_HEIGHT_SINGLE;\n};\n\n/**\n * Estimate the pixel height of a table node based on column count.\n */\nconst estimateNodeHeight = (columnCount: number, headerHeight: number = HEADER_HEIGHT_DOUBLE): number =>\n headerHeight + Math.max(columnCount, 1) * ROW_HEIGHT + 4; // +4 for bottom padding\n\n/**\n * Get the vertical center Y of a specific column row (0-indexed)\n * relative to the top of the node.\n */\nexport const getColumnRowY = (rowIndex: number, headerHeight: number = HEADER_HEIGHT_DOUBLE): number =>\n headerHeight + rowIndex * ROW_HEIGHT + ROW_HEIGHT / 2;\n\n// ─── Auto-Layout via Dagre ────────────────────────────────────────────\n\n/**\n * Compute node positions using the dagre graph layout engine.\n * Returns a new array of nodes with updated `position`.\n *\n * The graph always ranks top to bottom: an ERD read as a column of tables\n * fits the tall, narrow canvas the visualizer is given, and it is the only\n * layout the visualizer offers.\n */\nexport const getLayoutedElements = (\n nodes: Node[],\n edges: Edge[]\n): { nodes: Node[]; edges: Edge[] } => {\n const g = new dagre.graphlib.Graph();\n g.setGraph({\n rankdir: \"TB\",\n nodesep: 100,\n ranksep: 180,\n edgesep: 60,\n marginx: 60,\n marginy: 60\n });\n g.setDefaultEdgeLabel(() => ({}));\n\n const nodeHeights = new Map<string, number>();\n\n nodes.forEach((node) => {\n const data = node.data as { columns?: unknown[]; isJunction?: boolean; collectionName?: string; tableName?: string };\n const columnCount = data.columns?.length ?? 3;\n const headerH = getHeaderHeight({\n isJunction: Boolean(data.isJunction),\n collectionName: data.collectionName ?? \"\",\n tableName: data.tableName ?? \"\"\n });\n const h = estimateNodeHeight(columnCount, headerH);\n nodeHeights.set(node.id, h);\n g.setNode(node.id, {\n width: NODE_WIDTH,\n height: h\n });\n });\n\n edges.forEach((edge) => {\n g.setEdge(edge.source, edge.target);\n });\n\n dagre.layout(g);\n\n const layoutedNodes = nodes.map((node) => {\n const nodeWithPosition = g.node(node.id);\n const h = nodeHeights.get(node.id) ?? estimateNodeHeight(3);\n return {\n ...node,\n position: {\n x: nodeWithPosition.x - NODE_WIDTH / 2,\n y: nodeWithPosition.y - h / 2\n }\n };\n });\n\n return { nodes: layoutedNodes,\nedges };\n};\n\n// ─── Column type → display label ──────────────────────────────────────\n\nconst TYPE_LABELS: Record<string, string> = {\n string: \"varchar\",\n number: \"integer\",\n boolean: \"boolean\",\n date: \"timestamp\",\n map: \"jsonb\",\n array: \"jsonb\",\n relation: \"FK\"\n};\n\nexport const getTypeLabel = (type: string): string =>\n TYPE_LABELS[type] ?? type;\n\n// ─── Edge styling by relation type ────────────────────────────────────\n\nexport interface RelationEdgeData {\n cardinality: \"one\" | \"many\";\n direction: \"owning\" | \"inverse\";\n relationName: string;\n hasJunction: boolean;\n hasJoinPath: boolean;\n label: string;\n}\n\nexport const getCardinalityLabel = (\n cardinality: \"one\" | \"many\",\n direction: \"owning\" | \"inverse\"\n): string => {\n if (cardinality === \"many\") return \"M:N\";\n if (direction === \"inverse\") return \"1:1 ←\";\n return \"N:1\";\n};\n","import { useMemo, useState, useCallback } from \"react\";\nimport type { Node, Edge } from \"@xyflow/react\";\nimport { MarkerType } from \"@xyflow/react\";\nimport { isPostgresCollectionConfig } from \"@rebasepro/types\";\nimport type { Relation, ResolvedRelation } from \"@rebasepro/types\";\nimport { resolveCollectionRelations } from \"@rebasepro/common\";\nimport { getLayoutedElements, getCardinalityLabel, getTypeLabel, NODE_WIDTH } from \"./schema-visualizer.utils\";\nimport type { RelationEdgeData } from \"./schema-visualizer.utils\";\nimport type { AdminCollection, AdminPostgresCollection } from \"@rebasepro/cms-types\";\n\n// ─── Column info extracted from a collection ──────────────────────────\n\nexport interface ColumnInfo {\n name: string;\n type: string;\n typeLabel: string;\n isPrimaryKey: boolean;\n isForeignKey: boolean;\n isRequired: boolean;\n isEnum: boolean;\n enumValues?: string[];\n relationName?: string;\n}\n\n// ─── Node data shape ──────────────────────────────────────────────────\n\nexport interface TableNodeData {\n tableName: string;\n collectionName: string;\n slug: string;\n columns: ColumnInfo[];\n isJunction: boolean;\n isUnmanaged: boolean;\n rlsEnabled: boolean;\n historyEnabled: boolean;\n icon?: string;\n [key: string]: unknown;\n}\n\n// ─── Extract columns from a AdminCollection ─────────────────────────\n\nconst extractColumns = (collection: AdminCollection): ColumnInfo[] => {\n const columns: ColumnInfo[] = [];\n const properties = collection.properties ?? {};\n\n for (const [propName, prop] of Object.entries(properties)) {\n if (prop.type === \"relation\") continue; // Relations are shown as edges, not columns\n\n const isPk =\n (\"isId\" in prop && Boolean(prop.isId)) ||\n (!Object.values(properties).some(\n (p) => \"isId\" in p && Boolean(p.isId)\n ) &&\n propName === \"id\");\n\n const isEnum = prop.type === \"string\" && \"enum\" in prop && Boolean(prop.enum);\n\n let enumValues: string[] | undefined;\n if (isEnum && \"enum\" in prop && prop.enum) {\n const enumProp = prop.enum;\n if (Array.isArray(enumProp)) {\n enumValues = enumProp.map((v: unknown) =>\n typeof v === \"string\" ? v : String((v as Record<string, unknown>)?.id ?? v)\n );\n } else if (typeof enumProp === \"object\" && enumProp !== null) {\n enumValues = Object.keys(enumProp);\n }\n }\n\n columns.push({\n name: propName,\n type: prop.type,\n typeLabel: getTypeLabel(prop.type),\n isPrimaryKey: isPk,\n isForeignKey: false,\n isRequired: Boolean(prop.validation?.required),\n isEnum,\n enumValues\n });\n }\n\n // Add FK columns from owning relations\n if (isPostgresCollectionConfig(collection)) {\n try {\n const resolvedRelations = resolveCollectionRelations(collection);\n for (const rel of Object.values(resolvedRelations)) {\n if (rel.kind === \"belongsTo\" && rel.localKey) {\n // Only add if not already present as a regular column\n if (!columns.some((c) => c.name === rel.localKey)) {\n columns.push({\n name: rel.localKey,\n type: \"number\",\n typeLabel: \"FK\",\n isPrimaryKey: false,\n isForeignKey: true,\n isRequired: false,\n isEnum: false,\n relationName: rel.relationName\n });\n } else {\n // Mark existing column as FK\n const existing = columns.find((c) => c.name === rel.localKey);\n if (existing) {\n existing.isForeignKey = true;\n existing.relationName = rel.relationName;\n }\n }\n }\n }\n } catch {\n // Ignore resolution errors\n }\n }\n\n return columns;\n};\n\n// ─── Build graph from collections ─────────────────────────────────────\n\nconst buildGraph = (\n collections: AdminCollection[],\n liveRls: Set<string> | null\n): { nodes: Node[]; edges: Edge[] } => {\n /**\n * Is RLS on for this table?\n *\n * The database is the authority whenever we could reach it. Declaring rules\n * in code is not the same as having them applied, and the hosted console\n * never sees the declarations at all — see `useLiveRlsTables`. Only when\n * there is no SQL capability do we fall back to what the config says.\n */\n const isRlsEnabled = (schema: string, tableName: string, declaresRules: boolean): boolean =>\n liveRls ? liveRls.has(`${schema}.${tableName}`) : declaresRules;\n\n const nodes: Node[] = [];\n const edges: Edge[] = [];\n const tableToNodeId = new Map<string, string>();\n // Track columns per node so we can resolve handle IDs for edges\n const nodeColumns = new Map<string, ColumnInfo[]>();\n\n // Helper: find the PK column name for a node (falls back to \"id\")\n const getPkHandle = (nodeId: string): string => {\n const cols = nodeColumns.get(nodeId);\n const pk = cols?.find((c) => c.isPrimaryKey);\n return pk ? `target-${pk.name}` : \"target-default\";\n };\n\n // Helper: find the FK column name for a node pointing to a relation\n const getFkHandle = (nodeId: string, localKey: string | undefined): string => {\n if (!localKey) return \"source-default\";\n const cols = nodeColumns.get(nodeId);\n const fk = cols?.find((c) => c.name === localKey);\n return fk ? `source-${fk.name}` : \"source-default\";\n };\n\n // 1. Create nodes for each collection\n for (const collection of collections) {\n if (!isPostgresCollectionConfig(collection)) continue;\n\n const tableName = collection.table ?? collection.slug;\n const nodeId = `table-${tableName}`;\n tableToNodeId.set(tableName, nodeId);\n\n const columns = extractColumns(collection);\n nodeColumns.set(nodeId, columns);\n\n const nodeData: TableNodeData = {\n tableName,\n collectionName: collection.name,\n slug: collection.slug,\n columns,\n isJunction: false,\n isUnmanaged: false,\n rlsEnabled: isRlsEnabled(\n collection.schema ?? \"public\",\n tableName,\n Boolean(collection.securityRules && collection.securityRules.length > 0)\n ),\n historyEnabled: Boolean(collection.history),\n icon: typeof collection.icon === \"string\" ? collection.icon : undefined\n };\n\n nodes.push({\n id: nodeId,\n type: \"tableNode\",\n position: { x: 0,\ny: 0 },\n data: nodeData\n });\n }\n\n // 2. Create junction table nodes and edges\n const processedJunctions = new Set<string>();\n\n for (const collection of collections) {\n if (!isPostgresCollectionConfig(collection)) continue;\n\n const tableName = collection.table ?? collection.slug;\n const sourceNodeId = tableToNodeId.get(tableName);\n if (!sourceNodeId) continue;\n\n let resolvedRelations: Record<string, ResolvedRelation>;\n try {\n resolvedRelations = resolveCollectionRelations(collection);\n } catch {\n continue;\n }\n\n for (const [relationKey, rel] of Object.entries(resolvedRelations)) {\n let targetCollection: AdminCollection;\n try {\n targetCollection = rel.target();\n } catch {\n continue;\n }\n\n if (!isPostgresCollectionConfig(targetCollection)) continue;\n\n const targetTable = targetCollection.table ?? targetCollection.slug;\n const targetNodeId = tableToNodeId.get(targetTable);\n if (!targetNodeId) continue;\n\n // Draw the side that owns the storage: a foreign key on this table,\n // or the junction. `hasOne`/`hasMany` are the mirror image of a\n // `belongsTo` that is drawn from the other end, and `via` has no\n // single edge to draw.\n if (rel.kind !== \"belongsTo\" && rel.kind !== \"manyToMany\") continue;\n\n const direction = \"owning\" as const;\n const edgeData: RelationEdgeData = {\n cardinality: rel.cardinality,\n direction,\n relationName: rel.relationName ?? relationKey,\n hasJunction: rel.kind === \"manyToMany\",\n hasJoinPath: false,\n label: getCardinalityLabel(rel.cardinality, direction)\n };\n\n if (rel.kind === \"manyToMany\" && !processedJunctions.has(rel.through.table)) {\n // Many-to-many: create junction node + two edges\n processedJunctions.add(rel.through.table);\n const junctionNodeId = `junction-${rel.through.table}`;\n\n const junctionColumns: ColumnInfo[] = [\n {\n name: rel.through.sourceColumn,\n type: \"number\",\n typeLabel: \"FK\",\n isPrimaryKey: true,\n isForeignKey: true,\n isRequired: true,\n isEnum: false\n },\n {\n name: rel.through.targetColumn,\n type: \"number\",\n typeLabel: \"FK\",\n isPrimaryKey: true,\n isForeignKey: true,\n isRequired: true,\n isEnum: false\n }\n ];\n nodeColumns.set(junctionNodeId, junctionColumns);\n\n nodes.push({\n id: junctionNodeId,\n type: \"tableNode\",\n position: { x: 0,\ny: 0 },\n data: {\n tableName: rel.through.table,\n collectionName: rel.through.table,\n slug: rel.through.table,\n columns: junctionColumns,\n isJunction: true,\n isUnmanaged: false,\n // Junction tables get derived policies rather than\n // declared ones, so the config can never say — but the\n // database can, and usually says yes.\n rlsEnabled: isRlsEnabled(\"public\", rel.through.table, false),\n historyEnabled: false\n } satisfies TableNodeData\n });\n\n // Source table → junction (junction references source PK)\n const sourcePk = nodeColumns.get(sourceNodeId)?.find((c) => c.isPrimaryKey);\n edges.push({\n id: `edge-${sourceNodeId}-${junctionNodeId}`,\n source: sourceNodeId,\n target: junctionNodeId,\n sourceHandle: sourcePk ? `source-${sourcePk.name}` : \"source-default\",\n targetHandle: `target-${rel.through.sourceColumn}`,\n type: \"relationEdge\",\n data: { ...edgeData,\nlabel: \"1:N\" } as Record<string, unknown>,\n markerEnd: { type: MarkerType.ArrowClosed,\nwidth: 16,\nheight: 16 }\n });\n\n // Junction → target table\n edges.push({\n id: `edge-${junctionNodeId}-${targetNodeId}`,\n source: junctionNodeId,\n target: targetNodeId,\n sourceHandle: `source-${rel.through.targetColumn}`,\n targetHandle: getPkHandle(targetNodeId),\n type: \"relationEdge\",\n data: { ...edgeData,\nlabel: \"N:1\" } as Record<string, unknown>,\n markerEnd: { type: MarkerType.ArrowClosed,\nwidth: 16,\nheight: 16 }\n });\n } else if (rel.kind === \"belongsTo\") {\n // A foreign key on this table.\n edges.push({\n id: `edge-${sourceNodeId}-${targetNodeId}-${relationKey}`,\n source: sourceNodeId,\n target: targetNodeId,\n sourceHandle: getFkHandle(sourceNodeId, rel.localKey),\n targetHandle: getPkHandle(targetNodeId),\n type: \"relationEdge\",\n data: { ...edgeData } as Record<string, unknown>,\n markerEnd: { type: MarkerType.ArrowClosed,\nwidth: 16,\nheight: 16 }\n });\n }\n }\n }\n // 3. Apply dagre layout first to get node positions\n const layoutResult = getLayoutedElements(nodes, edges);\n\n // 4. Build a position lookup from the laid-out nodes\n const nodePositions = new Map<string, { x: number }>();\n for (const node of layoutResult.nodes) {\n nodePositions.set(node.id, { x: node.position.x });\n }\n\n // 5. Resolve handle sides based on relative node positions.\n // If source is left of target → source exits Right, target enters Left.\n // If source is right of target → source exits Left, target enters Right.\n for (const edge of layoutResult.edges) {\n const srcPos = nodePositions.get(edge.source);\n const tgtPos = nodePositions.get(edge.target);\n\n const srcBaseHandle = (edge.sourceHandle ?? \"source-default\") as string;\n const tgtBaseHandle = (edge.targetHandle ?? \"target-default\") as string;\n\n if (srcPos && tgtPos) {\n const srcCenterX = srcPos.x + NODE_WIDTH / 2;\n const tgtCenterX = tgtPos.x + NODE_WIDTH / 2;\n const sourceIsLeft = srcCenterX <= tgtCenterX;\n\n edge.sourceHandle = `${srcBaseHandle}-${sourceIsLeft ? \"right\" : \"left\"}`;\n edge.targetHandle = `${tgtBaseHandle}-${sourceIsLeft ? \"left\" : \"right\"}`;\n } else {\n edge.sourceHandle = `${srcBaseHandle}-right`;\n edge.targetHandle = `${tgtBaseHandle}-left`;\n }\n }\n\n return layoutResult;\n};\n\n// ─── Hook ─────────────────────────────────────────────────────────────\n\nexport interface UseSchemaGraphResult {\n nodes: Node[];\n edges: Edge[];\n relayout: () => void;\n tableCount: number;\n relationCount: number;\n}\n\nexport const useSchemaGraph = (\n collections: AdminCollection[] | undefined,\n /**\n * `schema.table` for every table the database reports RLS on, or `null`\n * when there is no SQL capability to ask. See {@link useLiveRlsTables}.\n */\n liveRls: Set<string> | null = null\n): UseSchemaGraphResult => {\n const [version, setVersion] = useState(0);\n\n const relayout = useCallback(() => setVersion((v) => v + 1), []);\n\n const { nodes, edges, tableCount, relationCount } = useMemo(() => {\n if (!collections || collections.length === 0) {\n return { nodes: [],\nedges: [],\ntableCount: 0,\nrelationCount: 0 };\n }\n const result = buildGraph(collections, liveRls);\n return {\n ...result,\n tableCount: result.nodes.length,\n relationCount: result.edges.length\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [collections, version, liveRls]);\n\n return {\n nodes,\n edges,\n relayout,\n tableCount,\n relationCount\n };\n};\n","/**\n * Which tables actually have row-level security switched on, according to the\n * database.\n *\n * ## Why this is not read from the collection config\n *\n * The graph used to derive \"RLS protected\" from `collection.securityRules` —\n * i.e. from what the *codebase* declares. Two problems with that:\n *\n * - **It is not the truth.** Rules in code are only live once a migration has\n * applied them, and `ALTER TABLE … DISABLE ROW LEVEL SECURITY` in a psql\n * session is invisible to it. A security indicator that reports intent\n * rather than state is worse than none: it is green precisely when you most\n * need it to be red.\n * - **The hosted console has no codebase.** There, collections arrive from the\n * tenant's `/api/meta/contract`, which deliberately strips `securityRules`\n * (they carry the raw SQL of every policy guarding the project). So the\n * console showed \"RLS protected: 0\" and no badge on any table, for every\n * project, however well secured.\n *\n * `pg_tables.rowsecurity` answers the question directly and is available\n * wherever `databaseAdmin` is — which is both hosts.\n *\n * When there is no SQL capability at all the hook reports `null`, and callers\n * fall back to the config-derived flag rather than claiming everything is\n * unprotected.\n */\nimport { useEffect, useState } from \"react\";\nimport { useRebaseContext } from \"@rebasepro/app\";\n\n/** `schema.table` for every table with RLS enabled, or `null` if unknown. */\nexport type LiveRlsTables = Set<string> | null;\n\nconst RLS_TABLES_SQL = `\n SELECT schemaname, tablename, rowsecurity\n FROM pg_tables\n WHERE schemaname NOT IN ('information_schema', 'pg_catalog');\n`;\n\n/** Postgres column names arrive lower-cased over the wire, but not on every driver. */\nfunction pick<T>(row: Record<string, unknown>, name: string): T | undefined {\n return (row[name] ?? row[name.toUpperCase()]) as T | undefined;\n}\n\nfunction extractRows(result: unknown): Record<string, unknown>[] {\n if (result && typeof result === \"object\" && \"rows\" in result) {\n const rows = (result as { rows: unknown }).rows;\n if (Array.isArray(rows)) return rows as Record<string, unknown>[];\n }\n if (Array.isArray(result)) return result as Record<string, unknown>[];\n return [];\n}\n\nexport function useLiveRlsTables(): LiveRlsTables {\n const { databaseAdmin } = useRebaseContext();\n const executeSql = databaseAdmin?.executeSql;\n const [tables, setTables] = useState<LiveRlsTables>(null);\n\n useEffect(() => {\n if (!executeSql) {\n setTables(null);\n return;\n }\n let cancelled = false;\n executeSql(RLS_TABLES_SQL)\n .then((result: unknown) => {\n if (cancelled) return;\n const enabled = new Set<string>();\n for (const row of extractRows(result)) {\n if (!pick<boolean>(row, \"rowsecurity\")) continue;\n const schema = pick<string>(row, \"schemaname\") ?? \"public\";\n const table = pick<string>(row, \"tablename\");\n if (table) enabled.add(`${schema}.${table}`);\n }\n setTables(enabled);\n })\n .catch(() => {\n // No answer is not the same as \"nothing is protected\" — leave it\n // unknown so the caller keeps its config-derived fallback.\n if (!cancelled) setTables(null);\n });\n return () => { cancelled = true; };\n }, [executeSql]);\n\n return tables;\n}\n","import React, { memo, useMemo } from \"react\";\nimport { Handle, Position } from \"@xyflow/react\";\nimport type { NodeProps } from \"@xyflow/react\";\nimport { Typography, Chip, Tooltip, cls } from \"@rebasepro/ui\";\nimport { IconForView } from \"@rebasepro/app\";\nimport type { TableNodeData, ColumnInfo } from \"./useSchemaGraph\";\nimport { getColumnRowY, getHeaderHeight } from \"./schema-visualizer.utils\";\n\n/**\n * Custom React Flow node that renders a database table as a card.\n *\n * Every PK and FK column renders handles on BOTH Left and Right sides.\n * The edge builder picks which side to use based on relative node position.\n */\nconst TableNodeInner = ({ data, selected }: NodeProps) => {\n const {\n tableName,\n collectionName,\n columns,\n isJunction,\n rlsEnabled,\n historyEnabled,\n icon\n } = data as TableNodeData;\n\n // Build handles: every PK/FK column gets Left + Right handles\n const handles = useMemo(() => {\n const result: { id: string; type: \"source\" | \"target\"; position: Position; top: number }[] = [];\n const cols = columns as ColumnInfo[];\n const headerH = getHeaderHeight({\n isJunction: Boolean(isJunction),\n collectionName: collectionName as string,\n tableName: tableName as string\n });\n const midY = cols.length > 0 ? getColumnRowY(Math.floor(cols.length / 2), headerH) : 30;\n\n cols.forEach((col, idx) => {\n const y = getColumnRowY(idx, headerH);\n\n if (col.isForeignKey && !col.isPrimaryKey) {\n // FK: source handles on both sides\n result.push({ id: `source-${col.name}-right`,\ntype: \"source\",\nposition: Position.Right,\ntop: y });\n result.push({ id: `source-${col.name}-left`,\ntype: \"source\",\nposition: Position.Left,\ntop: y });\n }\n if (col.isPrimaryKey) {\n // PK: target + source handles on both sides\n result.push({ id: `target-${col.name}-right`,\ntype: \"target\",\nposition: Position.Right,\ntop: y });\n result.push({ id: `target-${col.name}-left`,\ntype: \"target\",\nposition: Position.Left,\ntop: y });\n result.push({ id: `source-${col.name}-right`,\ntype: \"source\",\nposition: Position.Right,\ntop: y });\n result.push({ id: `source-${col.name}-left`,\ntype: \"source\",\nposition: Position.Left,\ntop: y });\n }\n });\n\n // Default handles on both sides\n result.push({ id: \"target-default-right\",\ntype: \"target\",\nposition: Position.Right,\ntop: midY });\n result.push({ id: \"target-default-left\",\ntype: \"target\",\nposition: Position.Left,\ntop: midY });\n result.push({ id: \"source-default-right\",\ntype: \"source\",\nposition: Position.Right,\ntop: midY });\n result.push({ id: \"source-default-left\",\ntype: \"source\",\nposition: Position.Left,\ntop: midY });\n\n return result;\n }, [columns, isJunction, collectionName, tableName]);\n\n return (\n <div\n className={cls(\n \"relative rounded-lg border bg-white dark:bg-surface-900 shadow-sm transition-all duration-200 min-w-[240px] max-w-[320px]\",\n selected\n ? \"border-primary ring-2 ring-primary/20 shadow-md\"\n : \"border-surface-200/40 dark:border-surface-700/40 hover:shadow-md hover:border-surface-300 dark:hover:border-surface-600\",\n isJunction && \"border-dashed\"\n )}\n >\n {/* ── Header ── */}\n <div\n className={cls(\n \"flex items-center gap-2 px-3 py-2 border-b rounded-t-lg\",\n isJunction\n ? \"bg-surface-50 dark:bg-surface-950/50 border-surface-200/30 dark:border-surface-700/30\"\n : \"bg-surface-50 dark:bg-surface-950 border-surface-200/40 dark:border-surface-700/40\"\n )}\n >\n {icon && !isJunction && (\n <div className=\"text-primary shrink-0\">\n <IconForView\n collectionOrView={{ slug: tableName,\nname: collectionName,\nicon }}\n size=\"smallest\"\n />\n </div>\n )}\n {isJunction && (\n <svg\n className=\"w-3.5 h-3.5 text-text-disabled dark:text-text-disabled-dark shrink-0\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4\"\n />\n </svg>\n )}\n <div className=\"flex flex-col min-w-0\">\n <Typography\n variant=\"caption\"\n className={cls(\n \"font-semibold truncate text-[12px]\",\n isJunction\n ? \"text-text-secondary dark:text-text-secondary-dark\"\n : \"text-text-primary dark:text-text-primary-dark\"\n )}\n >\n {tableName}\n </Typography>\n {collectionName !== tableName && !isJunction && (\n <Typography\n variant=\"caption\"\n className=\"text-[10px] text-text-disabled dark:text-text-disabled-dark truncate\"\n >\n {collectionName}\n </Typography>\n )}\n </div>\n\n {/* Badges */}\n <div className=\"ml-auto flex items-center gap-1 shrink-0\">\n {rlsEnabled && (\n <Tooltip title=\"RLS enabled\">\n <div className=\"w-1.5 h-1.5 rounded-full bg-green-500\"/>\n </Tooltip>\n )}\n {historyEnabled && (\n <Tooltip title=\"History enabled\">\n <div className=\"w-1.5 h-1.5 rounded-full bg-blue-400\"/>\n </Tooltip>\n )}\n </div>\n </div>\n\n {/* ── Columns ── */}\n <div className=\"divide-y divide-surface-100 dark:divide-surface-950/60\">\n {(columns as ColumnInfo[]).map((col: ColumnInfo) => (\n <div\n key={col.name}\n className={cls(\n \"flex items-center gap-2 px-3 py-1.5 text-xs transition-colors\",\n col.isPrimaryKey && \"bg-amber-50/50 dark:bg-amber-950/10\",\n col.isForeignKey &&\n !col.isPrimaryKey &&\n \"bg-blue-50/40 dark:bg-blue-950/10\"\n )}\n >\n <span className=\"w-3 shrink-0 text-center\">\n {col.isPrimaryKey && (\n <Tooltip title=\"Primary Key\">\n <span className=\"text-amber-500 text-[10px] font-semibold\">🔑</span>\n </Tooltip>\n )}\n {col.isForeignKey && !col.isPrimaryKey && (\n <Tooltip title={`FK → ${col.relationName ?? \"?\"}`}>\n <span className=\"text-blue-400 text-[10px] font-semibold\">🔗</span>\n </Tooltip>\n )}\n </span>\n\n <Typography\n variant=\"caption\"\n className={cls(\n \"font-mono text-[11px] truncate flex-1 min-w-0\",\n col.isPrimaryKey\n ? \"font-semibold text-amber-700 dark:text-amber-400\"\n : col.isForeignKey\n ? \"text-blue-600 dark:text-blue-400\"\n : \"text-text-primary dark:text-text-primary-dark\"\n )}\n >\n {col.name}\n </Typography>\n\n {col.isEnum ? (\n <Chip\n size=\"smallest\"\n className=\"bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400 border-violet-200 dark:border-violet-800 text-[9px] py-0\"\n >\n enum\n </Chip>\n ) : (\n <Typography\n variant=\"caption\"\n className=\"text-[10px] text-text-disabled dark:text-text-disabled-dark font-mono shrink-0\"\n >\n {col.typeLabel}\n </Typography>\n )}\n\n {col.isRequired && !col.isPrimaryKey && (\n <Tooltip title=\"Required\">\n <span className=\"text-red-400 text-[9px]\">•</span>\n </Tooltip>\n )}\n </div>\n ))}\n </div>\n\n {/* ── Handles at node root ── */}\n {handles.map((h) => (\n <Handle\n key={h.id}\n id={h.id}\n type={h.type}\n position={h.position}\n style={{ top: h.top }}\n className={cls(\n \"!w-2 !h-2 !border-2 !border-white dark:!border-surface-900\",\n h.type === \"source\" ? \"!bg-blue-400\" : \"!bg-amber-400\"\n )}\n />\n ))}\n </div>\n );\n};\n\nexport const TableNode = memo(TableNodeInner);\n","import React, { memo } from \"react\";\nimport { BaseEdge, getSmoothStepPath, EdgeLabelRenderer } from \"@xyflow/react\";\nimport type { EdgeProps } from \"@xyflow/react\";\nimport { cls } from \"@rebasepro/ui\";\nimport type { RelationEdgeData } from \"./schema-visualizer.utils\";\n\n/**\n * Custom React Flow edge that renders relations with style variations\n * based on cardinality and direction.\n *\n * - Owning one-to-one: solid, primary color\n * - Many-to-many (junction): solid, violet\n * - Inverse: dashed, muted\n * - Join path: dotted, muted\n */\nconst RelationEdgeInner = ({\n id,\n sourceX,\n sourceY,\n targetX,\n targetY,\n sourcePosition,\n targetPosition,\n data,\n selected\n}: EdgeProps) => {\n const edgeData = data as RelationEdgeData | undefined;\n\n const [edgePath, labelX, labelY] = getSmoothStepPath({\n sourceX,\n sourceY,\n sourcePosition,\n targetX,\n targetY,\n targetPosition,\n borderRadius: 8\n });\n\n const isInverse = edgeData?.direction === \"inverse\";\n const isJoinPath = edgeData?.hasJoinPath;\n const isJunction = edgeData?.hasJunction;\n\n // Determine stroke style\n let strokeColor = \"var(--rf-edge-stroke, #94a3b8)\";\n let strokeDasharray: string | undefined;\n let strokeWidth = 1.5;\n\n if (selected) {\n strokeColor = \"var(--rf-edge-stroke-selected, #6366f1)\";\n strokeWidth = 2.5;\n } else if (isJunction) {\n strokeColor = \"#8b5cf6\"; // violet\n } else if (isInverse) {\n strokeDasharray = \"6 3\";\n strokeColor = \"#94a3b8\";\n } else if (isJoinPath) {\n strokeDasharray = \"3 3\";\n strokeColor = \"#94a3b8\";\n } else {\n strokeColor = \"#6366f1\"; // primary/indigo\n }\n\n return (\n <>\n <BaseEdge\n id={id}\n path={edgePath}\n style={{\n stroke: strokeColor,\n strokeWidth,\n strokeDasharray\n }}\n />\n {edgeData?.label && (\n <EdgeLabelRenderer>\n <div\n style={{\n position: \"absolute\",\n transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,\n pointerEvents: \"all\"\n }}\n className={cls(\n \"px-1.5 py-0.5 rounded text-[9px] font-mono font-semibold leading-none\",\n \"bg-white dark:bg-surface-900 border\",\n selected\n ? \"border-primary text-primary\"\n : isJunction\n ? \"border-violet-200 dark:border-violet-800 text-violet-600 dark:text-violet-400\"\n : isInverse || isJoinPath\n ? \"border-surface-200 dark:border-surface-700 text-text-disabled dark:text-text-disabled-dark\"\n : \"border-primary/30 dark:border-primary/30 text-primary\"\n )}\n >\n {edgeData.label}\n </div>\n </EdgeLabelRenderer>\n )}\n </>\n );\n};\n\nexport const RelationEdge = memo(RelationEdgeInner);\n","import React, { useState, useCallback, useMemo, useEffect, useRef } from \"react\";\nimport {\n ReactFlow,\n Controls,\n MiniMap,\n Background,\n BackgroundVariant,\n useReactFlow,\n useNodesInitialized,\n ReactFlowProvider,\n applyNodeChanges,\n applyEdgeChanges\n} from \"@xyflow/react\";\nimport type { Node, Edge, NodeChange, EdgeChange } from \"@xyflow/react\";\nimport \"@xyflow/react/dist/style.css\";\nimport {\n SearchBar,\n TextField,\n Tooltip,\n Alert,\n Typography,\n cls,\n defaultBorderMixin,\n CircularProgress,\n ResizablePanels,\n IconButton\n} from \"@rebasepro/ui\";\nimport {\n useStudioCollectionRegistry,\n IconForView,\n useTranslation\n} from \"@rebasepro/app\";\n\nimport { isPostgresCollectionConfig } from \"@rebasepro/types\";\nimport { useSchemaGraph } from \"./useSchemaGraph\";\nimport { useLiveRlsTables } from \"./useLiveRls\";\nimport type { TableNodeData } from \"./useSchemaGraph\";\nimport { TableNode } from \"./TableNode\";\nimport { RelationEdge } from \"./RelationEdge\";\nimport type { AdminCollection } from \"@rebasepro/cms-types\";\n\n// ─── Custom node / edge type registrations ────────────────────────────\n\nconst nodeTypes = {\n tableNode: TableNode\n};\n\nconst edgeTypes = {\n relationEdge: RelationEdge\n};\n\n// ─── Inner component (needs ReactFlowProvider) ─────────────────────────\n\nfunction SchemaVisualizerCanvas({\n collections\n}: {\n collections: AdminCollection[];\n}) {\n const reactFlowInstance = useReactFlow();\n const liveRls = useLiveRlsTables();\n const {\n nodes: layoutedNodes,\n edges: layoutedEdges,\n relayout,\n tableCount,\n relationCount\n } = useSchemaGraph(collections, liveRls);\n\n const [nodes, setNodes] = useState<Node[]>([]);\n const [edges, setEdges] = useState<Edge[]>([]);\n const [selectedTable, setSelectedTable] = useState<string | null>(null);\n const [searchQuery, setSearchQuery] = useState(\"\");\n const initialFitDone = useRef(false);\n\n // Sync layouted data into state\n useEffect(() => {\n setNodes(layoutedNodes);\n setEdges(layoutedEdges);\n }, [layoutedNodes, layoutedEdges]);\n\n const onNodesChange = useCallback(\n (changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)),\n []\n );\n const onEdgesChange = useCallback(\n (changes: EdgeChange[]) => setEdges((eds) => applyEdgeChanges(changes, eds)),\n []\n );\n\n /**\n * Fit the graph into view, once, as soon as it can be measured.\n *\n * This used to fire on a 200ms timer. `fitView` needs every node's real\n * width and height, which React Flow only knows after it has laid them out\n * and measured them — with a dozen tables, a cold Monaco-sized bundle and\n * a slow first paint, 200ms is often too early. The fit then ran against\n * zero-sized nodes and produced a viewport nothing lines up with, and\n * `initialFitDone` made sure it was never retried: the ERD opened as a\n * scatter of specks in a corner and stayed there until you hit \"fit\" by\n * hand. `useNodesInitialized` is the event this was approximating.\n */\n const nodesInitialized = useNodesInitialized();\n useEffect(() => {\n if (!nodesInitialized || nodes.length === 0 || initialFitDone.current) return;\n initialFitDone.current = true;\n reactFlowInstance.fitView({ padding: 0.15,\nduration: 400 });\n }, [nodesInitialized, nodes.length, reactFlowInstance]);\n\n const handleFitView = useCallback(() => {\n reactFlowInstance.fitView({ padding: 0.15,\nduration: 400 });\n }, [reactFlowInstance]);\n\n const handleNodeClick = useCallback((_: React.MouseEvent, node: Node) => {\n setSelectedTable(node.id);\n }, []);\n\n const handlePaneClick = useCallback(() => {\n setSelectedTable(null);\n }, []);\n\n // Focus on a table from the sidebar\n const handleTableSelect = useCallback(\n (nodeId: string) => {\n setSelectedTable(nodeId);\n const node = nodes.find((n) => n.id === nodeId);\n if (node) {\n reactFlowInstance.setCenter(\n node.position.x + 140,\n node.position.y + 60,\n { zoom: 1.2,\nduration: 400 }\n );\n }\n },\n [nodes, reactFlowInstance]\n );\n\n // Sidebar panel size\n const [sidebarSize, setSidebarSize] = useState(() => {\n try {\n const saved = localStorage.getItem(\"rebase_schema_viz_sidebar_size\");\n return saved !== null ? parseFloat(saved) : 20;\n } catch {\n return 20;\n }\n });\n\n useEffect(() => {\n try {\n localStorage.setItem(\n \"rebase_schema_viz_sidebar_size\",\n sidebarSize.toString()\n );\n } catch {\n // Ignore\n }\n }, [sidebarSize]);\n\n // Group collections for sidebar\n const postgresCollections = useMemo(\n () => collections.filter(isPostgresCollectionConfig),\n [collections]\n );\n\n const filteredCollections = useMemo(() => {\n if (!searchQuery.trim()) return postgresCollections;\n const q = searchQuery.toLowerCase();\n return postgresCollections.filter(\n (c) =>\n c.name.toLowerCase().includes(q) ||\n c.table?.toLowerCase().includes(q) ||\n c.slug?.toLowerCase().includes(q)\n );\n }, [postgresCollections, searchQuery]);\n\n // Junction table nodes\n const junctionNodes = useMemo(\n () =>\n nodes.filter(\n (n) => (n.data as TableNodeData).isJunction\n ),\n [nodes]\n );\n\n /**\n * The RLS answer for one collection, from the same place the graph used.\n *\n * `nodes` already carries it, so look it up there rather than re-deriving —\n * that is what kept the sidebar dots, the node badges and the total from\n * agreeing.\n */\n const rlsEnabledFor = useCallback(\n (collection: AdminCollection): boolean => {\n const table = (isPostgresCollectionConfig(collection) ? collection.table : undefined) ?? collection.slug;\n const node = nodes.find((n) => n.id === `table-${table}`);\n return Boolean((node?.data as TableNodeData | undefined)?.rlsEnabled);\n },\n [nodes]\n );\n\n // Stats\n const stats = useMemo(\n () => ({\n tables: tableCount,\n relations: relationCount,\n junctions: junctionNodes.length,\n // Counted off the nodes, which already carry the database's answer\n // where there was one — the same number the per-table badges show.\n // Counting `securityRules` here instead made the total disagree\n // with the badges, and read 0 in the hosted console, where the\n // contract endpoint strips those rules.\n withRls: nodes.filter((n) => (n.data as TableNodeData | undefined)?.rlsEnabled).length\n }),\n [\n tableCount,\n relationCount,\n junctionNodes.length,\n nodes\n ]\n );\n\n return (\n <ResizablePanels\n orientation=\"horizontal\"\n panelSizePercent={sidebarSize}\n onPanelSizeChange={setSidebarSize}\n minPanelSizePx={220}\n firstPanel={\n <div\n className={cls(\n \"flex flex-col h-full w-full bg-white dark:bg-surface-950 border-r\",\n defaultBorderMixin\n )}\n >\n {/* Sidebar header */}\n <div\n className={cls(\n \"flex items-center justify-between px-3 py-2 border-b bg-surface-50 dark:bg-surface-900 min-h-[48px]\",\n defaultBorderMixin\n )}\n >\n <Typography\n variant=\"caption\"\n className=\"font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark\"\n >\n Tables\n </Typography>\n <Typography\n variant=\"caption\"\n className=\"text-[10px] text-text-disabled dark:text-text-disabled-dark font-mono\"\n >\n {stats.tables}\n </Typography>\n </div>\n\n {/* Search */}\n <div className=\"px-2 py-1.5 border-b border-surface-200/40 dark:border-surface-700/40\">\n <SearchBar\n size=\"smallest\"\n placeholder=\"Filter tables…\"\n onTextSearch={(val) => setSearchQuery(val ?? \"\")}\n innerClassName=\"text-xs\"\n />\n </div>\n\n {/* Table list */}\n <div className=\"flex-grow overflow-y-auto no-scrollbar p-1\">\n {/* Regular collections */}\n <div className=\"space-y-0.5\">\n {filteredCollections.map((collection) => {\n const table = collection.table ?? collection.slug;\n const nodeId = `table-${table}`;\n const isSelected = selectedTable === nodeId;\n return (\n <div\n key={nodeId}\n onClick={() =>\n handleTableSelect(nodeId)\n }\n className={cls(\n \"flex items-center p-1.5 cursor-pointer rounded transition-colors group\",\n isSelected\n ? \"bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-light\"\n : \"hover:bg-surface-100 dark:hover:bg-surface-950 text-text-secondary dark:text-text-secondary-dark\"\n )}\n >\n <div className=\"shrink-0 mr-1.5 text-text-disabled dark:text-text-disabled-dark\">\n <IconForView\n collectionOrView={{\n slug: collection.slug,\n name: collection.name,\n icon:\n typeof collection.icon ===\n \"string\"\n ? collection.icon\n : undefined\n }}\n size=\"smallest\"\n />\n </div>\n <div className=\"flex flex-col min-w-0 flex-1\">\n <Typography\n variant=\"caption\"\n className=\"text-xs truncate font-medium\"\n >\n {collection.name}\n </Typography>\n {table !== collection.name && (\n <Typography\n variant=\"caption\"\n className=\"text-[10px] text-text-disabled dark:text-text-disabled-dark font-mono truncate\"\n >\n {table}\n </Typography>\n )}\n </div>\n <div className=\"flex items-center gap-1 shrink-0 ml-1\">\n {/* Same source as the node badge and the\n \"RLS protected\" total: the database when\n it could be asked, the config otherwise.\n Reading `securityRules` directly here made\n the three disagree. */}\n {rlsEnabledFor(collection) && (\n <Tooltip title=\"RLS enabled\">\n <div className=\"w-1.5 h-1.5 rounded-full bg-green-500\"/>\n </Tooltip>\n )}\n {collection.history && (\n <Tooltip title=\"History enabled\">\n <div className=\"w-1.5 h-1.5 rounded-full bg-blue-400\"/>\n </Tooltip>\n )}\n </div>\n </div>\n );\n })}\n </div>\n\n {/* Junction tables */}\n {junctionNodes.length > 0 && (\n <div className=\"mt-3\">\n <Typography\n variant=\"caption\"\n className=\"px-1.5 text-[10px] uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark font-medium\"\n >\n Junction Tables\n </Typography>\n <div className=\"mt-1 space-y-0.5\">\n {junctionNodes.map((node) => {\n const d =\n node.data as TableNodeData;\n const isSelected =\n selectedTable === node.id;\n return (\n <div\n key={node.id}\n onClick={() =>\n handleTableSelect(\n node.id\n )\n }\n className={cls(\n \"flex items-center p-1.5 cursor-pointer rounded transition-colors\",\n isSelected\n ? \"bg-primary/10 text-primary dark:bg-primary/20\"\n : \"hover:bg-surface-100 dark:hover:bg-surface-950 text-text-disabled dark:text-text-disabled-dark\"\n )}\n >\n <svg\n className=\"w-3.5 h-3.5 mr-1.5 shrink-0\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4\"\n />\n </svg>\n <Typography\n variant=\"caption\"\n className=\"text-xs font-mono truncate\"\n >\n {d.tableName}\n </Typography>\n </div>\n );\n })}\n </div>\n </div>\n )}\n </div>\n\n {/* Stats footer */}\n <div\n className={cls(\n \"px-3 py-2 border-t bg-surface-50 dark:bg-surface-900 space-y-1\",\n defaultBorderMixin\n )}\n >\n <div className=\"flex items-center justify-between\">\n <Typography\n variant=\"caption\"\n className=\"text-[10px] text-text-disabled dark:text-text-disabled-dark\"\n >\n Tables\n </Typography>\n <Typography\n variant=\"caption\"\n className=\"text-[10px] font-mono font-medium\"\n >\n {stats.tables}\n </Typography>\n </div>\n <div className=\"flex items-center justify-between\">\n <Typography\n variant=\"caption\"\n className=\"text-[10px] text-text-disabled dark:text-text-disabled-dark\"\n >\n Relations\n </Typography>\n <Typography\n variant=\"caption\"\n className=\"text-[10px] font-mono font-medium\"\n >\n {stats.relations}\n </Typography>\n </div>\n <div className=\"flex items-center justify-between\">\n <Typography\n variant=\"caption\"\n className=\"text-[10px] text-text-disabled dark:text-text-disabled-dark\"\n >\n RLS protected\n </Typography>\n <div className=\"flex items-center gap-1\">\n <div className=\"w-1.5 h-1.5 rounded-full bg-green-500\"/>\n <Typography\n variant=\"caption\"\n className=\"text-[10px] font-mono font-medium\"\n >\n {stats.withRls}\n </Typography>\n </div>\n </div>\n </div>\n </div>\n }\n secondPanel={\n <div className=\"flex-grow flex flex-col min-w-0 h-full w-full bg-white dark:bg-surface-950\">\n {/* Toolbar */}\n <div\n className={cls(\n \"flex items-center justify-between pr-2 border-b bg-white dark:bg-surface-950 min-h-[46px]\",\n defaultBorderMixin\n )}\n >\n <div className=\"flex items-center gap-2 px-4\">\n <Typography\n variant=\"subtitle2\"\n className=\"font-mono text-text-secondary dark:text-text-secondary-dark\"\n >\n Schema Visualizer\n </Typography>\n </div>\n <div className=\"flex shrink-0 items-center gap-1.5\">\n {/* Fit view */}\n <Tooltip title=\"Fit to view\">\n <IconButton\n size=\"small\"\n onClick={handleFitView}\n >\n <svg\n className=\"w-4 h-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4\"\n />\n </svg>\n </IconButton>\n </Tooltip>\n\n {/* Re-layout */}\n <Tooltip title=\"Re-layout\">\n <IconButton\n size=\"small\"\n onClick={relayout}\n >\n <svg\n className=\"w-4 h-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n 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\"\n />\n </svg>\n </IconButton>\n </Tooltip>\n </div>\n </div>\n\n {/* Canvas */}\n <div className=\"flex-grow relative\">\n <ReactFlow\n nodes={nodes}\n edges={edges}\n onNodesChange={onNodesChange}\n onEdgesChange={onEdgesChange}\n onNodeClick={handleNodeClick}\n onPaneClick={handlePaneClick}\n nodeTypes={nodeTypes}\n edgeTypes={edgeTypes}\n fitView\n fitViewOptions={{ padding: 0.15 }}\n minZoom={0.1}\n maxZoom={2}\n proOptions={{ hideAttribution: true }}\n className=\"bg-surface-50 dark:bg-surface-950\"\n >\n <Background\n variant={BackgroundVariant.Dots}\n gap={20}\n size={1}\n className=\"!bg-surface-50 dark:!bg-surface-950\"\n color=\"var(--rf-bg-dot, #d4d4d8)\"\n />\n <Controls\n showInteractive={false}\n // mb clears the legend overlay pinned at bottom-left\n 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)]\"\n />\n <MiniMap\n nodeStrokeColor={(n) => {\n const d = n.data as TableNodeData;\n if (d.isJunction) return \"#a78bfa\";\n if (d.rlsEnabled) return \"#22c55e\";\n return \"#6366f1\";\n }}\n nodeColor={(n) => {\n const d = n.data as TableNodeData;\n if (d.isJunction) return \"#ede9fe\";\n return \"#eef2ff\";\n }}\n maskColor=\"rgba(0,0,0,0.08)\"\n className=\"!bg-white dark:!bg-surface-900 !border !border-surface-200/40 dark:!border-surface-700/40 !shadow-sm !rounded-lg\"\n />\n </ReactFlow>\n\n {/* Legend overlay */}\n <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\">\n <div className=\"flex items-center gap-1.5\">\n <div className=\"w-6 h-0.5 bg-indigo-500 rounded\"/>\n <Typography\n variant=\"caption\"\n className=\"text-[10px] text-text-disabled dark:text-text-disabled-dark\"\n >\n Owning\n </Typography>\n </div>\n <div className=\"flex items-center gap-1.5\">\n <div className=\"w-6 h-0.5 bg-violet-500 rounded\"/>\n <Typography\n variant=\"caption\"\n className=\"text-[10px] text-text-disabled dark:text-text-disabled-dark\"\n >\n M:N\n </Typography>\n </div>\n <div className=\"flex items-center gap-1.5\">\n <div\n className=\"w-6 h-0.5 rounded\"\n style={{\n backgroundImage:\n \"repeating-linear-gradient(90deg, #94a3b8, #94a3b8 4px, transparent 4px, transparent 7px)\"\n }}\n />\n <Typography\n variant=\"caption\"\n className=\"text-[10px] text-text-disabled dark:text-text-disabled-dark\"\n >\n Inverse\n </Typography>\n </div>\n <div className=\"h-3 w-px bg-surface-200 dark:bg-surface-700\"/>\n <div className=\"flex items-center gap-1\">\n <span className=\"text-[9px]\">🔑</span>\n <Typography\n variant=\"caption\"\n className=\"text-[10px] text-text-disabled dark:text-text-disabled-dark\"\n >\n PK\n </Typography>\n </div>\n <div className=\"flex items-center gap-1\">\n <span className=\"text-[9px]\">🔗</span>\n <Typography\n variant=\"caption\"\n className=\"text-[10px] text-text-disabled dark:text-text-disabled-dark\"\n >\n FK\n </Typography>\n </div>\n </div>\n </div>\n </div>\n }\n />\n );\n}\n\n// ─── Outer wrapper (provides ReactFlowProvider) ────────────────────────\n\nexport const SchemaVisualizer = () => {\n const { t } = useTranslation();\n const { collections: registryCollections } =\n useStudioCollectionRegistry() as {\n collections?: AdminCollection[];\n };\n\n /**\n * `undefined` and `[]` are different answers, and this view used to render\n * the same spinner for both. A project that declares no collections — a\n * fresh scaffold, a backend-only project someone opened the console\n * against — sat on \"Loading schema…\" forever, because there was nothing\n * coming.\n *\n * `undefined` is the Studio bridge before the admin's collection registry\n * has registered itself: genuinely still loading. `[]` is that registry\n * saying there is nothing to draw.\n */\n if (registryCollections === undefined) {\n return (\n <div className=\"flex items-center justify-center h-full w-full\">\n <div className=\"text-center space-y-3\">\n <CircularProgress size=\"small\"/>\n <Typography\n variant=\"body2\"\n color=\"secondary\"\n >\n Loading schema…\n </Typography>\n </div>\n </div>\n );\n }\n\n if (registryCollections.length === 0) {\n return (\n <div className=\"flex items-center justify-center h-full w-full p-6\">\n <div className=\"max-w-md text-center space-y-2\">\n <Typography variant=\"subtitle2\" className=\"block\">\n {t(\"studio_schema_no_collections\")}\n </Typography>\n <Typography variant=\"body2\" color=\"secondary\" className=\"block\">\n {t(\"studio_schema_no_collections_body\")}\n </Typography>\n <Typography variant=\"body2\" color=\"secondary\" className=\"block\">\n {t(\"studio_schema_introspect_hint\")}\n </Typography>\n </div>\n </div>\n );\n }\n\n const collections = registryCollections;\n\n return (\n <div className=\"flex h-full w-full bg-white dark:bg-surface-950 overflow-hidden text-text-primary dark:text-text-primary-dark\">\n <ReactFlowProvider>\n <SchemaVisualizerCanvas collections={collections}/>\n </ReactFlowProvider>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { t as classifyLoadFailure } from "./load-failure-C6OMJSGC.js";
1
2
  import { ErrorView, useApiBase, useApiConfig, useSnackbarController, useStorageSource, useStorageSources } from "@rebasepro/app";
2
3
  import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
4
  import { ArrowLeftIcon, Button, CheckIcon, Checkbox, Chip, CircularProgress, CopyIcon, Dialog, DialogActions, DialogContent, DialogTitle, DownloadIcon, FileTextIcon, FileUpload, FolderIcon, FolderPlusIcon, IconButton, ImageIcon, LayoutGridIcon, ListIcon, LoadingButton, Music2Icon, PlusIcon, RefreshCwIcon, Select, SelectItem, TextField, Tooltip, Trash2Icon, Typography, UploadCloudIcon, VideoIcon, XIcon, cls, defaultBorderMixin, iconSize } from "@rebasepro/ui";
@@ -5,34 +6,6 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
6
  import { useSearchParams } from "react-router";
6
7
  import { DEFAULT_STORAGE_SOURCE_KEY } from "@rebasepro/types";
7
8
  import { useDropzone } from "react-dropzone";
8
- //#region src/components/StorageView/storage-failure.ts
9
- /** HTTP status carried by whatever the SDK threw, if it carries one. */
10
- function statusOf(err) {
11
- if (typeof err !== "object" || err === null) return null;
12
- for (const key of [
13
- "status",
14
- "statusCode",
15
- "code"
16
- ]) {
17
- const v = err[key];
18
- if (typeof v === "number" && v >= 100 && v < 600) return v;
19
- }
20
- return null;
21
- }
22
- function classifyStorageFailure(err) {
23
- const detail = err instanceof Error ? err.message : String(err);
24
- const status = statusOf(err);
25
- return status === 403 || status === 401 || /\bnot authori[sz]ed\b|\bforbidden\b|\bpermission denied\b/i.test(detail) ? {
26
- kind: "denied",
27
- detail,
28
- retryable: false
29
- } : {
30
- kind: "unavailable",
31
- detail,
32
- retryable: true
33
- };
34
- }
35
- //#endregion
36
9
  //#region src/components/StorageView/StorageView.tsx
37
10
  /**
38
11
  * The query parameter holding the folder being browsed.
@@ -456,9 +429,9 @@ var StorageView = () => {
456
429
  const [selectedSourceKey, setSelectedSourceKey] = useState(DEFAULT_STORAGE_SOURCE_KEY);
457
430
  const storageSource = storageSources.sources[selectedSourceKey] ?? defaultStorageSource;
458
431
  const [searchParams, setSearchParams] = useSearchParams();
459
- const currentPath = searchParams.get(STORAGE_PATH_PARAM) || searchParams.get("path") || "";
432
+ const currentPath = searchParams.get(STORAGE_PATH_PARAM) ?? searchParams.get("path") ?? "";
460
433
  const [loading, setLoading] = useState(true);
461
- /** Why the listing failed, classified — see `storage-failure.ts`. */
434
+ /** Why the listing failed, classified — see `load-failure.ts`. */
462
435
  const [failure, setFailure] = useState(null);
463
436
  const [folders, setFolders] = useState([]);
464
437
  const [files, setFiles] = useState([]);
@@ -513,7 +486,7 @@ var StorageView = () => {
513
486
  setFiles(fileItems);
514
487
  } catch (e) {
515
488
  console.error("Storage list error:", e);
516
- setFailure(classifyStorageFailure(e));
489
+ setFailure(classifyLoadFailure(e));
517
490
  } finally {
518
491
  setLoading(false);
519
492
  }
@@ -1490,4 +1463,4 @@ var StorageView = () => {
1490
1463
  //#endregion
1491
1464
  export { StorageView };
1492
1465
 
1493
- //# sourceMappingURL=StorageView-BGDeC5GI.js.map
1466
+ //# sourceMappingURL=StorageView-Ed9Q_VlB.js.map