@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 +0,0 @@
1
- export * from "./RLSEditor";
@@ -1,176 +0,0 @@
1
- /**
2
- * What the RLS editor offers before the user types anything: the roles a `TO`
3
- * list may name, and the ready-made policies.
4
- *
5
- * Separated from the component because these are the parts that have to agree
6
- * with the *server*, not with the UI — and the ways they can silently disagree
7
- * are the reason this file has tests.
8
- */
9
- import { policy as policyExpr } from "@rebasepro/types";
10
- import { policyToPostgres, REBASE_USER_ROLE } from "@rebasepro/common";
11
-
12
- export type PolicyCommand = "ALL" | "SELECT" | "INSERT" | "UPDATE" | "DELETE";
13
-
14
- export const COMMAND_OPTIONS: PolicyCommand[] = ["ALL", "SELECT", "INSERT", "UPDATE", "DELETE"];
15
-
16
- /**
17
- * The roles a policy's `TO` list can name when nothing better is known.
18
- *
19
- * Not `authenticated` / `anon` / `admin`, which is what this list used to be.
20
- * The first two are Supabase's role names and Rebase never creates them, so
21
- * `CREATE POLICY ... TO authenticated` — which is literally what the editor
22
- * builds and executes — fails with `role "authenticated" does not exist`. The
23
- * driver's `validatePolicyPgRoles` rejects the same three names in
24
- * `SecurityRule.pgRoles` and says why: they are another platform's convention,
25
- * and application roles belong in a condition, not in the `TO` list.
26
- *
27
- * `admin` was the more dangerous entry, because it is a plausible *application*
28
- * role. Had one existed as a database role too, the policy would have been
29
- * created successfully and then matched nothing at all: requests run as
30
- * {@link REBASE_USER_ROLE} after a `SET LOCAL ROLE`, and a `TO` list naming a
31
- * role the request never assumes filters every row without erroring.
32
- *
33
- * So: `public` (what the framework's own generator emits) and the role requests
34
- * actually arrive as. "Signed-in users" and "admins" are not roles here — they
35
- * are conditions over `rebase.uid()` and `rebase.roles()`, which is what the
36
- * presets below now compile to.
37
- */
38
- export const FALLBACK_ROLE_OPTIONS = ["public", REBASE_USER_ROLE];
39
-
40
- /**
41
- * The `TO` list the editor offers, given what the database reported and what
42
- * the policy being edited already targets.
43
- *
44
- * Three sources, and each is there for a reason the others do not cover:
45
- *
46
- * - {@link FALLBACK_ROLE_OPTIONS} always, because `public` is a **keyword, not
47
- * a row in `pg_roles`** — `fetchAvailableRoles` cannot return it, so seeding
48
- * from the live fetch alone would drop the one role every generated policy
49
- * targets.
50
- * - the live roles, so an operator's own `app_read` is reachable without
51
- * hand-editing SQL.
52
- * - the edited policy's own roles, because a `MultiSelect` silently drops a
53
- * value with no matching item. That value is the `TO` list of a policy
54
- * already enforcing something, so opening an unrelated policy for a one-word
55
- * rename would have quietly rewritten who it applies to.
56
- */
57
- export function roleOptionsFor(
58
- fetched: readonly string[] | undefined,
59
- policyRoles: readonly string[] | undefined
60
- ): string[] {
61
- return [...new Set([...FALLBACK_ROLE_OPTIONS, ...(fetched ?? []), ...(policyRoles ?? [])])];
62
- }
63
-
64
- /**
65
- * Preset conditions, compiled by the framework's own policy compiler rather
66
- * than written out here.
67
- *
68
- * `policyToPostgres` is the function `db push` generates policies with, so a
69
- * preset cannot drift from what the framework emits — and the one that matters
70
- * has drifted before: `authenticated()` used to compile to a bare
71
- * `IS NOT NULL`, which is true for anonymous visitors, and any copy of that
72
- * string sitting in a preset would still be handing out the old grant today.
73
- */
74
- export const SIGNED_IN_SQL = policyToPostgres(policyExpr.authenticated());
75
- export const IS_ADMIN_SQL = policyToPostgres(policyExpr.rolesOverlap(["admin"]));
76
- export const OWNS_ROW_SQL = policyToPostgres(
77
- policyExpr.compare(policyExpr.authUid(), "eq", policyExpr.field("uid"))
78
- );
79
-
80
- export interface PolicyPreset {
81
- id: string;
82
- label: string;
83
- description: string;
84
- policyname: string;
85
- cmd: PolicyCommand;
86
- permissive: "PERMISSIVE" | "RESTRICTIVE";
87
- roles: string[];
88
- qual: string;
89
- with_check: string;
90
- }
91
-
92
- /**
93
- * Every preset targets `TO public`, which is what the framework's generator
94
- * emits and what actually reaches a request running as `rebase_user`. The part
95
- * that used to be expressed as a role — "authenticated", "admin" — is a
96
- * condition now, so it is enforced where Postgres will actually evaluate it.
97
- */
98
- export const POLICY_PRESETS: PolicyPreset[] = [
99
- {
100
- id: "public_read",
101
- label: "Enable read access to everyone",
102
- description: "Anyone can read data, regardless of authentication status.",
103
- policyname: "Enable read access for all users",
104
- cmd: "SELECT",
105
- permissive: "PERMISSIVE",
106
- roles: ["public"],
107
- qual: "true",
108
- with_check: ""
109
- },
110
- {
111
- id: "auth_read",
112
- label: "Enable read access for signed-in users only",
113
- description: "Only signed-in users are allowed to read data. Anonymous requests carry a sentinel uid, so the condition excludes them explicitly.",
114
- policyname: "Enable read access for signed-in users",
115
- cmd: "SELECT",
116
- permissive: "PERMISSIVE",
117
- roles: ["public"],
118
- qual: SIGNED_IN_SQL,
119
- with_check: ""
120
- },
121
- {
122
- id: "auth_insert",
123
- label: "Enable insert for signed-in users only",
124
- description: "Only signed-in users are allowed to insert new data.",
125
- policyname: "Enable insert for signed-in users only",
126
- cmd: "INSERT",
127
- permissive: "PERMISSIVE",
128
- roles: ["public"],
129
- qual: "",
130
- with_check: SIGNED_IN_SQL
131
- },
132
- {
133
- id: "admin_all",
134
- label: "Admins can do anything",
135
- description: "Restricted to users holding the `admin` application role, matched inside the policy via rebase.roles().",
136
- policyname: "Admins have full access",
137
- cmd: "ALL",
138
- permissive: "PERMISSIVE",
139
- roles: ["public"],
140
- qual: IS_ADMIN_SQL,
141
- with_check: IS_ADMIN_SQL
142
- },
143
- {
144
- id: "user_select_own",
145
- label: "Users can read their own rows",
146
- description: "Users can only read rows whose uid column matches their rebase.uid()",
147
- policyname: "Users can select their own data",
148
- cmd: "SELECT",
149
- permissive: "PERMISSIVE",
150
- roles: ["public"],
151
- qual: OWNS_ROW_SQL,
152
- with_check: ""
153
- },
154
- {
155
- id: "user_update_own",
156
- label: "Users can update their own rows",
157
- description: "Users can only update rows whose uid column matches their rebase.uid()",
158
- policyname: "Users can update their own data",
159
- cmd: "UPDATE",
160
- permissive: "PERMISSIVE",
161
- roles: ["public"],
162
- qual: OWNS_ROW_SQL,
163
- with_check: OWNS_ROW_SQL
164
- },
165
- {
166
- id: "user_delete_own",
167
- label: "Users can delete their own rows",
168
- description: "Users can only delete rows whose uid column matches their rebase.uid()",
169
- policyname: "Users can delete their own data",
170
- cmd: "DELETE",
171
- permissive: "PERMISSIVE",
172
- roles: ["public"],
173
- qual: OWNS_ROW_SQL,
174
- with_check: ""
175
- }
176
- ];
@@ -1,48 +0,0 @@
1
- /**
2
- * Write a collection's security rules to its source file.
3
- *
4
- * The schema-editor routes sit behind an admin gate that accepts a bearer token
5
- * and nothing else — there is no cookie fallback — so a request without an
6
- * `Authorization` header is a 401 every time. The RLS editor's two write paths
7
- * ("Create policy" on a mapped table, and "Import to codebase") both sent none,
8
- * and both reported the failure as a constant "Failed to save policy", which is
9
- * also what a 501 and a parse error looked like.
10
- *
11
- * It lives in its own module rather than inline in the component so that the
12
- * header can be asserted without rendering the whole RLS editor.
13
- */
14
- export async function saveSecurityRulesToCodebase(options: {
15
- apiBase: string;
16
- collectionId: string;
17
- securityRules: unknown[];
18
- getAuthToken?: () => Promise<string | null | undefined>;
19
- }): Promise<void> {
20
- const { apiBase, collectionId, securityRules, getAuthToken } = options;
21
-
22
- const headers: Record<string, string> = { "Content-Type": "application/json" };
23
- const token = getAuthToken ? await getAuthToken() : null;
24
- if (token) headers["Authorization"] = `Bearer ${token}`;
25
-
26
- const response = await fetch(`${apiBase}/schema-editor/collection/save`, {
27
- method: "POST",
28
- headers,
29
- // Only the rules change here. `partial` says so: read as a
30
- // whole-collection save, a payload of one key deletes everything it does
31
- // not mention.
32
- body: JSON.stringify({ collectionId,
33
- collectionData: { securityRules },
34
- partial: true })
35
- });
36
-
37
- if (response.ok) return;
38
-
39
- const body = await response.text().catch(() => "");
40
- let message: string | undefined;
41
- try {
42
- const parsed = JSON.parse(body) as { error?: { message?: string }, message?: string };
43
- message = parsed.error?.message ?? parsed.message;
44
- } catch {
45
- message = body.trim() || undefined;
46
- }
47
- throw new Error(message ?? `Failed to save policy (HTTP ${response.status}).`);
48
- }
@@ -1,174 +0,0 @@
1
- import React, { Suspense, useLayoutEffect, useMemo } from "react";
2
- import { useRebaseRegistryDispatch } from "@rebasepro/app";
3
- import type { RebaseStudioConfig, AppView } from "@rebasepro/cms-types";
4
- import { CircularProgressCenter, lazyChunk } from "@rebasepro/ui";
5
-
6
- // Lazy-loaded studio tools — each fetched only when its route is visited.
7
- // This keeps Monaco, @xyflow/react, dagre, pgsql-ast-parser etc. out of the initial bundle.
8
- const SQLEditor = lazyChunk(() => import("./SQLEditor/SQLEditor").then(m => ({ default: m.SQLEditor })));
9
- const JSEditor = lazyChunk(() => import("./JSEditor/JSEditor").then(m => ({ default: m.JSEditor })));
10
- const RLSEditor = lazyChunk(() => import("./RLSEditor/RLSEditor").then(m => ({ default: m.RLSEditor })));
11
- const StorageView = lazyChunk(() => import("./StorageView/StorageView").then(m => ({ default: m.StorageView })));
12
- const CronJobsView = lazyChunk(() => import("./CronJobs/CronJobsView").then(m => ({ default: m.CronJobsView })));
13
- const SchemaVisualizer = lazyChunk(() => import("./SchemaVisualizer/SchemaVisualizer").then(m => ({ default: m.SchemaVisualizer })));
14
- const BranchesView = lazyChunk(() => import("./Branches/BranchesView").then(m => ({ default: m.BranchesView })));
15
- const BackupsView = lazyChunk(() => import("./Backups/BackupsView").then(m => ({ default: m.BackupsView })));
16
- const ApiExplorer = lazyChunk(() => import("./ApiExplorer/ApiExplorer").then(m => ({ default: m.ApiExplorer })));
17
- const LogsExplorer = lazyChunk(() => import("./LogsExplorer/LogsExplorer").then(m => ({ default: m.LogsExplorer })));
18
- const ApiKeysView = lazyChunk(() => import("./ApiKeys/ApiKeysView").then(m => ({ default: m.ApiKeysView })));
19
-
20
- import { StudioHomePage } from "./StudioHomePage";
21
-
22
- /**
23
- * Declarative component to configure the Studio in Rebase.
24
- * Renders nothing — purely registers config into the RebaseRegistry.
25
- *
26
- * The "schema" tool (collection editor view) is a Studio tool, but it is not
27
- * registered here: it ships from the panel package because it needs the project's
28
- * collection source to write back to. When `<RebaseCMS collectionEditor={...}>`
29
- * is used, the schema view is automatically injected into Studio — no manual
30
- * wiring needed. Where it is mounted from is an implementation detail; it is a
31
- * schema-editing tool and belongs beside SQL and RLS.
32
- */
33
- const DEFAULT_HOME_PAGE = <StudioHomePage/>;
34
-
35
- export function RebaseStudio({ tools, homePage }: RebaseStudioConfig) {
36
- const dispatch = useRebaseRegistryDispatch();
37
-
38
- const resolvedHomePage = homePage ?? DEFAULT_HOME_PAGE;
39
-
40
- /**
41
- * The tool list reduced to a value, so that re-rendering the host does not
42
- * republish the registry.
43
- *
44
- * `tools` is an array, and callers write it inline — `tools={[...TOOLS]}`
45
- * in the hosted console. A fresh array every render meant a fresh
46
- * `devViews` every render, which meant the effect below unregistered and
47
- * re-registered on every render. Because each registration builds *new*
48
- * `<Suspense><SQLEditor/></Suspense>` elements, whatever view was on screen
49
- * was torn down and remounted each time: a half-typed SQL query, a storage
50
- * folder you had navigated into, an open policy editor — all discarded, and
51
- * every mount reissued the view's initial requests.
52
- */
53
- const toolsKey = tools ? tools.join(",") : "";
54
-
55
- const devViews: AppView[] = useMemo(() => {
56
- const views: AppView[] = [];
57
- const activeTools = tools ?? ["sql", "js", "rls", "storage", "cron", "schema-visualizer", "branches", "backups", "api", "logs", "api-keys"];
58
- const suspense = (el: React.ReactNode) => <Suspense fallback={<CircularProgressCenter/>}>{el}</Suspense>;
59
-
60
- if (activeTools.includes("sql")) {
61
- views.push({ slug: "sql",
62
- name: "SQL Console",
63
- group: "Database",
64
- icon: "terminal",
65
- description: "Execute SQL queries",
66
- view: suspense(<SQLEditor/>) });
67
- }
68
- if (activeTools.includes("js")) {
69
- views.push({ slug: "js",
70
- name: "JS Console",
71
- group: "Compute",
72
- icon: "code",
73
- description: "Execute JavaScript",
74
- view: suspense(<JSEditor/>) });
75
- }
76
- if (activeTools.includes("rls")) {
77
- views.push({ slug: "rls",
78
- name: "RLS Policies",
79
- group: "Database",
80
- icon: "ShieldCheck",
81
- description: "Row Level Security",
82
- view: suspense(<RLSEditor/>) });
83
- }
84
- if (activeTools.includes("storage")) {
85
- views.push({ slug: "storage",
86
- name: "Storage",
87
- group: "Storage",
88
- icon: "HardDrive",
89
- description: "Manage storage files",
90
- view: suspense(<StorageView/>) });
91
- }
92
- if (activeTools.includes("cron")) {
93
- views.push({ slug: "cron",
94
- name: "Cron Jobs",
95
- group: "Compute",
96
- icon: "Clock",
97
- description: "Manage scheduled tasks",
98
- view: suspense(<CronJobsView/>) });
99
- }
100
- if (activeTools.includes("schema-visualizer")) {
101
- views.push({ slug: "schema-visualizer",
102
- name: "Schema Visualizer",
103
- group: "Database",
104
- icon: "Network",
105
- description: "Interactive database ERD",
106
- view: suspense(<SchemaVisualizer/>) });
107
- }
108
- if (activeTools.includes("branches")) {
109
- views.push({ slug: "branches",
110
- name: "Branches",
111
- group: "Database",
112
- icon: "GitBranch",
113
- description: "Create and manage database branches",
114
- view: suspense(<BranchesView/>) });
115
- }
116
- if (activeTools.includes("backups")) {
117
- views.push({ slug: "backups",
118
- name: "Backups",
119
- group: "Database",
120
- icon: "Database",
121
- description: "Download database backups",
122
- view: suspense(<BackupsView/>) });
123
- }
124
- if (activeTools.includes("api")) {
125
- views.push({ slug: "api",
126
- name: "API Explorer",
127
- group: "API",
128
- icon: "BookOpen",
129
- description: "Interactive API documentation and testing",
130
- view: suspense(<ApiExplorer/>) });
131
- }
132
- if (activeTools.includes("logs")) {
133
- views.push({ slug: "logs",
134
- name: "Logs Explorer",
135
- group: "Database",
136
- icon: "Activity",
137
- description: "Real-time system and query logs",
138
- view: suspense(<LogsExplorer/>) });
139
- }
140
- if (activeTools.includes("api-keys")) {
141
- views.push({ slug: "api-keys",
142
- name: "API Keys",
143
- group: "Access Control",
144
- icon: "KeyRound",
145
- description: "Create and manage scoped API keys",
146
- view: suspense(<ApiKeysView/>) });
147
- }
148
- // Note: "schema" tool is auto-injected by RebaseShell when collectionEditor is enabled.
149
- // It is NOT registered here anymore.
150
- return views;
151
- // Keyed by the tool list's *contents*; see `toolsKey`. `tools` itself is
152
- // read inside, and is exactly determined by the key.
153
- // eslint-disable-next-line react-hooks/exhaustive-deps
154
- }, [toolsKey]);
155
-
156
- // Use a ref for homePage so it never destabilizes the effect.
157
- // homePage is a React element — its identity doesn't matter for registration.
158
- const homePageRef = React.useRef(resolvedHomePage);
159
- homePageRef.current = resolvedHomePage;
160
-
161
- // Same reasoning for `tools`: it is registered as config, but a new array
162
- // with the same contents must not count as a change.
163
- const toolsRef = React.useRef(tools);
164
- toolsRef.current = tools;
165
-
166
- useLayoutEffect(() => {
167
- dispatch.registerStudio({ tools: toolsRef.current,
168
- homePage: homePageRef.current,
169
- devViews });
170
- return () => dispatch.unregisterStudio();
171
- }, [dispatch, devViews]);
172
-
173
- return null;
174
- }
@@ -1,128 +0,0 @@
1
- import React, { useState } from "react";
2
- import { Typography, cls, defaultBorderMixin, Collapse } from "@rebasepro/ui";
3
-
4
- export interface ExplainPlanNode {
5
- "Node Type": string;
6
- "Relation Name"?: string;
7
- "Alias"?: string;
8
- "Startup Cost": number;
9
- "Total Cost": number;
10
- "Plan Rows": number;
11
- "Plan Width": number;
12
- "Actual Startup Time"?: number;
13
- "Actual Total Time"?: number;
14
- "Actual Rows"?: number;
15
- "Actual Loops"?: number;
16
- "Filter"?: string;
17
- "Index Cond"?: string;
18
- "Hash Cond"?: string;
19
- "Plans"?: ExplainPlanNode[];
20
- [key: string]: unknown;
21
- }
22
-
23
- export interface ExplainVisualizerProps {
24
- plan: ExplainPlanNode;
25
- isRoot?: boolean;
26
- }
27
-
28
- export const ExplainVisualizer: React.FC<ExplainVisualizerProps> = ({ plan, isRoot = true }) => {
29
- const [expanded, setExpanded] = useState(true);
30
-
31
- const hasChildren = plan.Plans && plan.Plans.length > 0;
32
-
33
- // Determine color based on cost
34
- const cost = plan["Total Cost"];
35
- let costColor = "text-green-500 dark:text-green-400";
36
- if (cost > 1000) costColor = "text-red-500 dark:text-red-400";
37
- else if (cost > 100) costColor = "text-orange-500 dark:text-orange-400";
38
-
39
- return (
40
- <div className={cls("flex flex-col", isRoot ? "p-4" : "pl-6 mt-2 relative")}>
41
- {/* Tree branch line */}
42
- {!isRoot && (
43
- <div className="absolute left-2.5 top-0 bottom-0 w-px bg-surface-200 dark:bg-surface-950 -z-10"></div>
44
- )}
45
- {!isRoot && (
46
- <div className="absolute left-2.5 top-5 w-3 h-px bg-surface-200 dark:bg-surface-950 -z-10"></div>
47
- )}
48
-
49
- <div className={cls("border rounded-md bg-white dark:bg-surface-900 text-text-primary dark:text-text-primary-dark shadow-xs relative z-10 w-[420px] max-w-full", defaultBorderMixin)}>
50
- <div
51
- className={cls("px-4 py-3 flex justify-between items-center cursor-pointer select-none")}
52
- onClick={() => setExpanded(!expanded)}
53
- >
54
- <div className="flex items-center space-x-2">
55
- {hasChildren ? (
56
- <svg className={cls("w-4 h-4 text-text-secondary transition-transform", !expanded ? "-rotate-90" : "")} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7"/></svg>
57
- ) : (
58
- <div className="w-4 h-4"></div>
59
- )}
60
-
61
- <Typography variant="body1" className="flex items-center">
62
- {plan["Node Type"]}
63
- </Typography>
64
-
65
- {plan["Relation Name"] && (
66
- <span className="font-mono text-[11px] text-text-secondary dark:text-text-secondary-dark px-2 py-0.5 rounded bg-surface-200 dark:bg-surface-950 ml-2">
67
- on {plan["Relation Name"]} {plan.Alias && plan.Alias !== plan["Relation Name"] ? `(${plan.Alias})` : ""}
68
- </span>
69
- )}
70
- </div>
71
-
72
- <div className="flex space-x-6 items-center">
73
- <div className="flex flex-col text-right">
74
- <span className="text-[10px] uppercase text-text-disabled dark:text-text-disabled-dark font-semibold tracking-wide leading-tight mb-0.5">Cost</span>
75
- <span className={cls("font-mono font-medium text-[13px] leading-none", costColor)}>{cost.toFixed(2)}</span>
76
- </div>
77
- <div className="flex flex-col text-right">
78
- <span className="text-[10px] uppercase text-text-disabled dark:text-text-disabled-dark font-semibold tracking-wide leading-tight mb-0.5">Rows</span>
79
- <span className="font-mono text-[13px] text-text-secondary dark:text-text-secondary-dark leading-none">{plan["Plan Rows"]}</span>
80
- </div>
81
- </div>
82
- </div>
83
-
84
- <Collapse in={expanded}>
85
- <div className="px-4 py-3 border-t border-surface-200 dark:border-surface-950 text-[13px] flex flex-col gap-2">
86
- <div className="flex items-center space-x-6">
87
- <div className="flex items-center space-x-2">
88
- <span className="text-text-disabled dark:text-text-disabled-dark">Startup Cost:</span>
89
- <span className="font-mono font-medium">{plan["Startup Cost"].toFixed(2)}</span>
90
- </div>
91
- <div className="flex items-center space-x-2">
92
- <span className="text-text-disabled dark:text-text-disabled-dark">Width:</span>
93
- <span className="font-mono font-medium">{plan["Plan Width"]} bytes</span>
94
- </div>
95
- </div>
96
-
97
- {plan.Filter && (
98
- <div className="mt-1">
99
- <span className="text-text-disabled dark:text-text-disabled-dark block mb-1">Filter:</span>
100
- <code className="block w-full p-2 bg-surface-50 dark:bg-surface-950 border dark:border-surface-950 rounded font-mono text-[12px] truncate">{plan.Filter}</code>
101
- </div>
102
- )}
103
- {plan["Index Cond"] && (
104
- <div className="mt-1">
105
- <span className="text-text-disabled dark:text-text-disabled-dark block mb-1">Index Cond:</span>
106
- <code className="block w-full p-2 bg-surface-50 dark:bg-surface-950 border dark:border-surface-950 rounded font-mono text-[12px] truncate">{plan["Index Cond"]}</code>
107
- </div>
108
- )}
109
- {plan["Hash Cond"] && (
110
- <div className="mt-1">
111
- <span className="text-text-disabled dark:text-text-disabled-dark block mb-1">Hash Cond:</span>
112
- <code className="block w-full p-2 bg-surface-50 dark:bg-surface-950 border dark:border-surface-950 rounded font-mono text-[12px] truncate">{plan["Hash Cond"]}</code>
113
- </div>
114
- )}
115
- </div>
116
- </Collapse>
117
- </div>
118
-
119
- {expanded && hasChildren && (
120
- <div className="flex flex-col space-y-2 mt-[-4px]">
121
- {plan.Plans!.map((childPlan, idx) => (
122
- <ExplainVisualizer key={idx} plan={childPlan} isRoot={false}/>
123
- ))}
124
- </div>
125
- )}
126
- </div>
127
- );
128
- };