@rebasepro/studio 0.17.3 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -1
- package/README.md +12 -4
- package/dist/{ApiKeysView-DiCurTEU.js → ApiKeysView-EIvegrUk.js} +54 -39
- package/dist/ApiKeysView-EIvegrUk.js.map +1 -0
- package/dist/{BackupsView-DNS6LdVg.js → BackupsView-DVkKzaPR.js} +29 -31
- package/dist/BackupsView-DVkKzaPR.js.map +1 -0
- package/dist/{BranchesView-CM3B-ER3.js → BranchesView-Df8hQ8wE.js} +85 -24
- package/dist/BranchesView-Df8hQ8wE.js.map +1 -0
- package/dist/{CronJobsView-BHtJZeJZ.js → CronJobsView-CLOGjb-L.js} +32 -27
- package/dist/CronJobsView-CLOGjb-L.js.map +1 -0
- package/dist/{RLSEditor-CBX4uD66.js → RLSEditor-Sa1D34ny.js} +724 -615
- package/dist/RLSEditor-Sa1D34ny.js.map +1 -0
- package/dist/{SQLEditor-ByecwX8B.js → SQLEditor-BfNA8xM3.js} +9 -4
- package/dist/{SQLEditor-ByecwX8B.js.map → SQLEditor-BfNA8xM3.js.map} +1 -1
- package/dist/{SchemaVisualizer-D6tMZgeC.js → SchemaVisualizer-DTDMYL6o.js} +84 -110
- package/dist/SchemaVisualizer-DTDMYL6o.js.map +1 -0
- package/dist/{StorageView-BGDeC5GI.js → StorageView-Ed9Q_VlB.js} +5 -32
- package/dist/StorageView-Ed9Q_VlB.js.map +1 -0
- package/dist/components/RLSEditor/saveRules.d.ts +38 -0
- package/dist/components/RebaseStudio.d.ts +1 -1
- package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +5 -2
- package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +0 -4
- package/dist/components/load-failure-view.d.ts +17 -0
- package/dist/components/load-failure.d.ts +47 -0
- package/dist/index.es.js +56 -36
- package/dist/index.es.js.map +1 -1
- package/dist/load-failure-C6OMJSGC.js +31 -0
- package/dist/load-failure-C6OMJSGC.js.map +1 -0
- package/dist/load-failure-view-CBTB_h-h.js +48 -0
- package/dist/load-failure-view-CBTB_h-h.js.map +1 -0
- package/package.json +36 -21
- package/dist/ApiKeysView-DiCurTEU.js.map +0 -1
- package/dist/BackupsView-DNS6LdVg.js.map +0 -1
- package/dist/BranchesView-CM3B-ER3.js.map +0 -1
- package/dist/CronJobsView-BHtJZeJZ.js.map +0 -1
- package/dist/RLSEditor-CBX4uD66.js.map +0 -1
- package/dist/SchemaVisualizer-D6tMZgeC.js.map +0 -1
- package/dist/StorageView-BGDeC5GI.js.map +0 -1
- package/dist/components/StorageView/storage-failure.d.ts +0 -34
- package/src/components/ApiExplorer/ApiExplorer.tsx +0 -299
- package/src/components/ApiExplorer/EndpointDetail.tsx +0 -279
- package/src/components/ApiExplorer/TryItPanel.tsx +0 -499
- package/src/components/ApiExplorer/parseSpec.ts +0 -104
- package/src/components/ApiExplorer/types.ts +0 -84
- package/src/components/ApiKeys/ApiKeysView.tsx +0 -479
- package/src/components/ApiKeys/CreateApiKeyDialog.tsx +0 -711
- package/src/components/ApiKeys/permissions.ts +0 -145
- package/src/components/AuthSimulationSelector.tsx +0 -72
- package/src/components/Backups/BackupsView.tsx +0 -185
- package/src/components/Branches/BranchesView.tsx +0 -386
- package/src/components/CronJobs/CronJobsView.tsx +0 -413
- package/src/components/JSEditor/JSEditor.tsx +0 -1100
- package/src/components/JSEditor/JSEditorSidebar.tsx +0 -350
- package/src/components/JSEditor/JSMonacoEditor.tsx +0 -410
- package/src/components/LogsExplorer/LogsExplorer.tsx +0 -273
- package/src/components/LogsExplorer/useLogTail.ts +0 -356
- package/src/components/RLSEditor/PolicyEditor.tsx +0 -399
- package/src/components/RLSEditor/RLSEditor.tsx +0 -1128
- package/src/components/RLSEditor/index.ts +0 -1
- package/src/components/RLSEditor/policy-presets.ts +0 -176
- package/src/components/RLSEditor/saveSecurityRules.ts +0 -48
- package/src/components/RebaseStudio.tsx +0 -174
- package/src/components/SQLEditor/ExplainVisualizer.tsx +0 -128
- package/src/components/SQLEditor/MonacoEditor.tsx +0 -203
- package/src/components/SQLEditor/SQLEditor.tsx +0 -1518
- package/src/components/SQLEditor/SQLEditorSidebar.tsx +0 -173
- package/src/components/SQLEditor/SchemaBrowser.tsx +0 -168
- package/src/components/SQLEditor/sql_editor_types.ts +0 -18
- package/src/components/SchemaVisualizer/RelationEdge.tsx +0 -102
- package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +0 -710
- package/src/components/SchemaVisualizer/TableNode.tsx +0 -257
- package/src/components/SchemaVisualizer/schema-visualizer.utils.ts +0 -140
- package/src/components/SchemaVisualizer/useLiveRls.ts +0 -86
- package/src/components/SchemaVisualizer/useSchemaGraph.ts +0 -426
- package/src/components/StorageView/StorageView.tsx +0 -1588
- package/src/components/StorageView/storage-failure.ts +0 -63
- package/src/components/StudioHomePage.tsx +0 -430
- package/src/index.ts +0 -31
- package/src/utils/parseSpec.test.ts +0 -309
- package/src/utils/sql_utils.test.ts +0 -265
- package/src/utils/sql_utils.ts +0 -291
|
@@ -1 +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
|
-
};
|