@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,257 +0,0 @@
1
- import React, { memo, useMemo } from "react";
2
- import { Handle, Position } from "@xyflow/react";
3
- import type { NodeProps } from "@xyflow/react";
4
- import { Typography, Chip, Tooltip, cls } from "@rebasepro/ui";
5
- import { IconForView } from "@rebasepro/app";
6
- import type { TableNodeData, ColumnInfo } from "./useSchemaGraph";
7
- import { getColumnRowY, getHeaderHeight } from "./schema-visualizer.utils";
8
-
9
- /**
10
- * Custom React Flow node that renders a database table as a card.
11
- *
12
- * Every PK and FK column renders handles on BOTH Left and Right sides.
13
- * The edge builder picks which side to use based on relative node position.
14
- */
15
- const TableNodeInner = ({ data, selected }: NodeProps) => {
16
- const {
17
- tableName,
18
- collectionName,
19
- columns,
20
- isJunction,
21
- rlsEnabled,
22
- historyEnabled,
23
- icon
24
- } = data as TableNodeData;
25
-
26
- // Build handles: every PK/FK column gets Left + Right handles
27
- const handles = useMemo(() => {
28
- const result: { id: string; type: "source" | "target"; position: Position; top: number }[] = [];
29
- const cols = columns as ColumnInfo[];
30
- const headerH = getHeaderHeight({
31
- isJunction: Boolean(isJunction),
32
- collectionName: collectionName as string,
33
- tableName: tableName as string
34
- });
35
- const midY = cols.length > 0 ? getColumnRowY(Math.floor(cols.length / 2), headerH) : 30;
36
-
37
- cols.forEach((col, idx) => {
38
- const y = getColumnRowY(idx, headerH);
39
-
40
- if (col.isForeignKey && !col.isPrimaryKey) {
41
- // FK: source handles on both sides
42
- result.push({ id: `source-${col.name}-right`,
43
- type: "source",
44
- position: Position.Right,
45
- top: y });
46
- result.push({ id: `source-${col.name}-left`,
47
- type: "source",
48
- position: Position.Left,
49
- top: y });
50
- }
51
- if (col.isPrimaryKey) {
52
- // PK: target + source handles on both sides
53
- result.push({ id: `target-${col.name}-right`,
54
- type: "target",
55
- position: Position.Right,
56
- top: y });
57
- result.push({ id: `target-${col.name}-left`,
58
- type: "target",
59
- position: Position.Left,
60
- top: y });
61
- result.push({ id: `source-${col.name}-right`,
62
- type: "source",
63
- position: Position.Right,
64
- top: y });
65
- result.push({ id: `source-${col.name}-left`,
66
- type: "source",
67
- position: Position.Left,
68
- top: y });
69
- }
70
- });
71
-
72
- // Default handles on both sides
73
- result.push({ id: "target-default-right",
74
- type: "target",
75
- position: Position.Right,
76
- top: midY });
77
- result.push({ id: "target-default-left",
78
- type: "target",
79
- position: Position.Left,
80
- top: midY });
81
- result.push({ id: "source-default-right",
82
- type: "source",
83
- position: Position.Right,
84
- top: midY });
85
- result.push({ id: "source-default-left",
86
- type: "source",
87
- position: Position.Left,
88
- top: midY });
89
-
90
- return result;
91
- }, [columns, isJunction, collectionName, tableName]);
92
-
93
- return (
94
- <div
95
- className={cls(
96
- "relative rounded-lg border bg-white dark:bg-surface-900 shadow-sm transition-all duration-200 min-w-[240px] max-w-[320px]",
97
- selected
98
- ? "border-primary ring-2 ring-primary/20 shadow-md"
99
- : "border-surface-200/40 dark:border-surface-700/40 hover:shadow-md hover:border-surface-300 dark:hover:border-surface-600",
100
- isJunction && "border-dashed"
101
- )}
102
- >
103
- {/* ── Header ── */}
104
- <div
105
- className={cls(
106
- "flex items-center gap-2 px-3 py-2 border-b rounded-t-lg",
107
- isJunction
108
- ? "bg-surface-50 dark:bg-surface-950/50 border-surface-200/30 dark:border-surface-700/30"
109
- : "bg-surface-50 dark:bg-surface-950 border-surface-200/40 dark:border-surface-700/40"
110
- )}
111
- >
112
- {icon && !isJunction && (
113
- <div className="text-primary shrink-0">
114
- <IconForView
115
- collectionOrView={{ slug: tableName,
116
- name: collectionName,
117
- icon }}
118
- size="smallest"
119
- />
120
- </div>
121
- )}
122
- {isJunction && (
123
- <svg
124
- className="w-3.5 h-3.5 text-text-disabled dark:text-text-disabled-dark shrink-0"
125
- fill="none"
126
- stroke="currentColor"
127
- viewBox="0 0 24 24"
128
- >
129
- <path
130
- strokeLinecap="round"
131
- strokeLinejoin="round"
132
- strokeWidth={2}
133
- d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"
134
- />
135
- </svg>
136
- )}
137
- <div className="flex flex-col min-w-0">
138
- <Typography
139
- variant="caption"
140
- className={cls(
141
- "font-semibold truncate text-[12px]",
142
- isJunction
143
- ? "text-text-secondary dark:text-text-secondary-dark"
144
- : "text-text-primary dark:text-text-primary-dark"
145
- )}
146
- >
147
- {tableName}
148
- </Typography>
149
- {collectionName !== tableName && !isJunction && (
150
- <Typography
151
- variant="caption"
152
- className="text-[10px] text-text-disabled dark:text-text-disabled-dark truncate"
153
- >
154
- {collectionName}
155
- </Typography>
156
- )}
157
- </div>
158
-
159
- {/* Badges */}
160
- <div className="ml-auto flex items-center gap-1 shrink-0">
161
- {rlsEnabled && (
162
- <Tooltip title="RLS enabled">
163
- <div className="w-1.5 h-1.5 rounded-full bg-green-500"/>
164
- </Tooltip>
165
- )}
166
- {historyEnabled && (
167
- <Tooltip title="History enabled">
168
- <div className="w-1.5 h-1.5 rounded-full bg-blue-400"/>
169
- </Tooltip>
170
- )}
171
- </div>
172
- </div>
173
-
174
- {/* ── Columns ── */}
175
- <div className="divide-y divide-surface-100 dark:divide-surface-950/60">
176
- {(columns as ColumnInfo[]).map((col: ColumnInfo) => (
177
- <div
178
- key={col.name}
179
- className={cls(
180
- "flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
181
- col.isPrimaryKey && "bg-amber-50/50 dark:bg-amber-950/10",
182
- col.isForeignKey &&
183
- !col.isPrimaryKey &&
184
- "bg-blue-50/40 dark:bg-blue-950/10"
185
- )}
186
- >
187
- <span className="w-3 shrink-0 text-center">
188
- {col.isPrimaryKey && (
189
- <Tooltip title="Primary Key">
190
- <span className="text-amber-500 text-[10px] font-semibold">🔑</span>
191
- </Tooltip>
192
- )}
193
- {col.isForeignKey && !col.isPrimaryKey && (
194
- <Tooltip title={`FK → ${col.relationName ?? "?"}`}>
195
- <span className="text-blue-400 text-[10px] font-semibold">🔗</span>
196
- </Tooltip>
197
- )}
198
- </span>
199
-
200
- <Typography
201
- variant="caption"
202
- className={cls(
203
- "font-mono text-[11px] truncate flex-1 min-w-0",
204
- col.isPrimaryKey
205
- ? "font-semibold text-amber-700 dark:text-amber-400"
206
- : col.isForeignKey
207
- ? "text-blue-600 dark:text-blue-400"
208
- : "text-text-primary dark:text-text-primary-dark"
209
- )}
210
- >
211
- {col.name}
212
- </Typography>
213
-
214
- {col.isEnum ? (
215
- <Chip
216
- size="smallest"
217
- 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"
218
- >
219
- enum
220
- </Chip>
221
- ) : (
222
- <Typography
223
- variant="caption"
224
- className="text-[10px] text-text-disabled dark:text-text-disabled-dark font-mono shrink-0"
225
- >
226
- {col.typeLabel}
227
- </Typography>
228
- )}
229
-
230
- {col.isRequired && !col.isPrimaryKey && (
231
- <Tooltip title="Required">
232
- <span className="text-red-400 text-[9px]">•</span>
233
- </Tooltip>
234
- )}
235
- </div>
236
- ))}
237
- </div>
238
-
239
- {/* ── Handles at node root ── */}
240
- {handles.map((h) => (
241
- <Handle
242
- key={h.id}
243
- id={h.id}
244
- type={h.type}
245
- position={h.position}
246
- style={{ top: h.top }}
247
- className={cls(
248
- "!w-2 !h-2 !border-2 !border-white dark:!border-surface-900",
249
- h.type === "source" ? "!bg-blue-400" : "!bg-amber-400"
250
- )}
251
- />
252
- ))}
253
- </div>
254
- );
255
- };
256
-
257
- export const TableNode = memo(TableNodeInner);
@@ -1,140 +0,0 @@
1
- import dagre from "dagre";
2
- import type { Node, Edge } from "@xyflow/react";
3
-
4
- // ─── Layout Constants ─────────────────────────────────────────────────
5
- export const NODE_WIDTH = 280;
6
- /** Header with a single line of text (junction tables or tableName === collectionName). */
7
- const HEADER_HEIGHT_SINGLE = 33;
8
- /** Header with two lines (name + subtitle when collectionName !== tableName). */
9
- const HEADER_HEIGHT_DOUBLE = 47;
10
- const ROW_HEIGHT = 28; // height per column row
11
-
12
- /**
13
- * Compute the correct header height for a table node.
14
- */
15
- export const getHeaderHeight = (opts: {
16
- isJunction: boolean;
17
- collectionName: string;
18
- tableName: string;
19
- }): number => {
20
- if (opts.isJunction) return HEADER_HEIGHT_SINGLE;
21
- return opts.collectionName !== opts.tableName
22
- ? HEADER_HEIGHT_DOUBLE
23
- : HEADER_HEIGHT_SINGLE;
24
- };
25
-
26
- /**
27
- * Estimate the pixel height of a table node based on column count.
28
- */
29
- const estimateNodeHeight = (columnCount: number, headerHeight: number = HEADER_HEIGHT_DOUBLE): number =>
30
- headerHeight + Math.max(columnCount, 1) * ROW_HEIGHT + 4; // +4 for bottom padding
31
-
32
- /**
33
- * Get the vertical center Y of a specific column row (0-indexed)
34
- * relative to the top of the node.
35
- */
36
- export const getColumnRowY = (rowIndex: number, headerHeight: number = HEADER_HEIGHT_DOUBLE): number =>
37
- headerHeight + rowIndex * ROW_HEIGHT + ROW_HEIGHT / 2;
38
-
39
- // ─── Auto-Layout via Dagre ────────────────────────────────────────────
40
-
41
- export type LayoutDirection = "TB" | "LR";
42
-
43
- /**
44
- * Compute node positions using the dagre graph layout engine.
45
- * Returns a new array of nodes with updated `position`.
46
- */
47
- export const getLayoutedElements = (
48
- nodes: Node[],
49
- edges: Edge[],
50
- direction: LayoutDirection = "LR"
51
- ): { nodes: Node[]; edges: Edge[] } => {
52
- const g = new dagre.graphlib.Graph();
53
- g.setGraph({
54
- rankdir: direction,
55
- nodesep: 100,
56
- ranksep: 180,
57
- edgesep: 60,
58
- marginx: 60,
59
- marginy: 60
60
- });
61
- g.setDefaultEdgeLabel(() => ({}));
62
-
63
- const nodeHeights = new Map<string, number>();
64
-
65
- nodes.forEach((node) => {
66
- const data = node.data as { columns?: unknown[]; isJunction?: boolean; collectionName?: string; tableName?: string };
67
- const columnCount = data.columns?.length ?? 3;
68
- const headerH = getHeaderHeight({
69
- isJunction: Boolean(data.isJunction),
70
- collectionName: data.collectionName ?? "",
71
- tableName: data.tableName ?? ""
72
- });
73
- const h = estimateNodeHeight(columnCount, headerH);
74
- nodeHeights.set(node.id, h);
75
- g.setNode(node.id, {
76
- width: NODE_WIDTH,
77
- height: h
78
- });
79
- });
80
-
81
- edges.forEach((edge) => {
82
- g.setEdge(edge.source, edge.target);
83
- });
84
-
85
- dagre.layout(g);
86
-
87
- const layoutedNodes = nodes.map((node) => {
88
- const nodeWithPosition = g.node(node.id);
89
- const h = nodeHeights.get(node.id) ?? estimateNodeHeight(3);
90
- return {
91
- ...node,
92
- data: {
93
- ...node.data,
94
- layoutDirection: direction
95
- },
96
- position: {
97
- x: nodeWithPosition.x - NODE_WIDTH / 2,
98
- y: nodeWithPosition.y - h / 2
99
- }
100
- };
101
- });
102
-
103
- return { nodes: layoutedNodes,
104
- edges };
105
- };
106
-
107
- // ─── Column type → display label ──────────────────────────────────────
108
-
109
- const TYPE_LABELS: Record<string, string> = {
110
- string: "varchar",
111
- number: "integer",
112
- boolean: "boolean",
113
- date: "timestamp",
114
- map: "jsonb",
115
- array: "jsonb",
116
- relation: "FK"
117
- };
118
-
119
- export const getTypeLabel = (type: string): string =>
120
- TYPE_LABELS[type] ?? type;
121
-
122
- // ─── Edge styling by relation type ────────────────────────────────────
123
-
124
- export interface RelationEdgeData {
125
- cardinality: "one" | "many";
126
- direction: "owning" | "inverse";
127
- relationName: string;
128
- hasJunction: boolean;
129
- hasJoinPath: boolean;
130
- label: string;
131
- }
132
-
133
- export const getCardinalityLabel = (
134
- cardinality: "one" | "many",
135
- direction: "owning" | "inverse"
136
- ): string => {
137
- if (cardinality === "many") return "M:N";
138
- if (direction === "inverse") return "1:1 ←";
139
- return "N:1";
140
- };
@@ -1,86 +0,0 @@
1
- /**
2
- * Which tables actually have row-level security switched on, according to the
3
- * database.
4
- *
5
- * ## Why this is not read from the collection config
6
- *
7
- * The graph used to derive "RLS protected" from `collection.securityRules` —
8
- * i.e. from what the *codebase* declares. Two problems with that:
9
- *
10
- * - **It is not the truth.** Rules in code are only live once a migration has
11
- * applied them, and `ALTER TABLE … DISABLE ROW LEVEL SECURITY` in a psql
12
- * session is invisible to it. A security indicator that reports intent
13
- * rather than state is worse than none: it is green precisely when you most
14
- * need it to be red.
15
- * - **The hosted console has no codebase.** There, collections arrive from the
16
- * tenant's `/api/meta/contract`, which deliberately strips `securityRules`
17
- * (they carry the raw SQL of every policy guarding the project). So the
18
- * console showed "RLS protected: 0" and no badge on any table, for every
19
- * project, however well secured.
20
- *
21
- * `pg_tables.rowsecurity` answers the question directly and is available
22
- * wherever `databaseAdmin` is — which is both hosts.
23
- *
24
- * When there is no SQL capability at all the hook reports `null`, and callers
25
- * fall back to the config-derived flag rather than claiming everything is
26
- * unprotected.
27
- */
28
- import { useEffect, useState } from "react";
29
- import { useRebaseContext } from "@rebasepro/app";
30
-
31
- /** `schema.table` for every table with RLS enabled, or `null` if unknown. */
32
- export type LiveRlsTables = Set<string> | null;
33
-
34
- const RLS_TABLES_SQL = `
35
- SELECT schemaname, tablename, rowsecurity
36
- FROM pg_tables
37
- WHERE schemaname NOT IN ('information_schema', 'pg_catalog');
38
- `;
39
-
40
- /** Postgres column names arrive lower-cased over the wire, but not on every driver. */
41
- function pick<T>(row: Record<string, unknown>, name: string): T | undefined {
42
- return (row[name] ?? row[name.toUpperCase()]) as T | undefined;
43
- }
44
-
45
- function extractRows(result: unknown): Record<string, unknown>[] {
46
- if (result && typeof result === "object" && "rows" in result) {
47
- const rows = (result as { rows: unknown }).rows;
48
- if (Array.isArray(rows)) return rows as Record<string, unknown>[];
49
- }
50
- if (Array.isArray(result)) return result as Record<string, unknown>[];
51
- return [];
52
- }
53
-
54
- export function useLiveRlsTables(): LiveRlsTables {
55
- const { databaseAdmin } = useRebaseContext();
56
- const executeSql = databaseAdmin?.executeSql;
57
- const [tables, setTables] = useState<LiveRlsTables>(null);
58
-
59
- useEffect(() => {
60
- if (!executeSql) {
61
- setTables(null);
62
- return;
63
- }
64
- let cancelled = false;
65
- executeSql(RLS_TABLES_SQL)
66
- .then((result: unknown) => {
67
- if (cancelled) return;
68
- const enabled = new Set<string>();
69
- for (const row of extractRows(result)) {
70
- if (!pick<boolean>(row, "rowsecurity")) continue;
71
- const schema = pick<string>(row, "schemaname") ?? "public";
72
- const table = pick<string>(row, "tablename");
73
- if (table) enabled.add(`${schema}.${table}`);
74
- }
75
- setTables(enabled);
76
- })
77
- .catch(() => {
78
- // No answer is not the same as "nothing is protected" — leave it
79
- // unknown so the caller keeps its config-derived fallback.
80
- if (!cancelled) setTables(null);
81
- });
82
- return () => { cancelled = true; };
83
- }, [executeSql]);
84
-
85
- return tables;
86
- }