@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.
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,145 +0,0 @@
1
- /**
2
- * The vocabulary of an API key's permission entries.
3
- *
4
- * A permission entry is `{ collection, operations }`, but `collection` is not
5
- * only a collection: the same field addresses three namespaces, and the guard
6
- * that enforces them lives in `@rebasepro/server`
7
- * (`auth/api-keys/api-key-permission-guard.ts`):
8
- *
9
- * - `"*"` — every collection, every custom function, and storage
10
- * - `"storage"` — the storage routes
11
- * - `"functions"` — every custom function, including the function index
12
- * - `"functions/<name>"`— one named function
13
- * - anything else — the collection with that slug
14
- *
15
- * The UI used to label the field "Collection slug or *", which made two of
16
- * those namespaces undiscoverable and made `"*"` read as "all collections"
17
- * when it also hands over storage and every function. This module is the one
18
- * place that knows the mapping, so the picker, the row description, the grant
19
- * summary and the detail panel cannot drift from each other.
20
- *
21
- * @module
22
- */
23
-
24
- import type { ApiKeyPermission } from "@rebasepro/types";
25
-
26
- /** Grants every collection, every function, and storage. */
27
- export const RESOURCE_EVERYTHING = "*";
28
- /** Grants the storage routes. */
29
- export const RESOURCE_STORAGE = "storage";
30
- /** Grants every custom function. */
31
- export const RESOURCE_ALL_FUNCTIONS = "functions";
32
- /** Prefix addressing a single custom function. */
33
- export const FUNCTION_PREFIX = "functions/";
34
-
35
- export type ResourceKind =
36
- | "everything"
37
- | "storage"
38
- | "all-functions"
39
- | "function"
40
- | "collection";
41
-
42
- export interface ParsedResource {
43
- kind: ResourceKind;
44
- /** The function name for `"function"`, the slug for `"collection"`. */
45
- name: string;
46
- }
47
-
48
- /** Classify a raw `collection` field into the namespace it addresses. */
49
- export function parseResource(collection: string): ParsedResource {
50
- const value = collection.trim();
51
- if (value === RESOURCE_EVERYTHING) return { kind: "everything", name: "" };
52
- if (value === RESOURCE_STORAGE) return { kind: "storage", name: "" };
53
- if (value === RESOURCE_ALL_FUNCTIONS) return { kind: "all-functions", name: "" };
54
- if (value.startsWith(FUNCTION_PREFIX)) {
55
- return { kind: "function", name: value.slice(FUNCTION_PREFIX.length) };
56
- }
57
- return { kind: "collection", name: value };
58
- }
59
-
60
- /**
61
- * Short label for a resource — what a picker or a chip shows.
62
- *
63
- * Deliberately not the raw value: `"*"` alone is the thing nobody could read.
64
- */
65
- export function resourceLabel(collection: string): string {
66
- const { kind, name } = parseResource(collection);
67
- switch (kind) {
68
- case "everything": return "Everything";
69
- case "storage": return "Storage";
70
- case "all-functions": return "All functions";
71
- case "function": return name ? `${name}()` : "Function";
72
- case "collection": return name || "—";
73
- }
74
- }
75
-
76
- /**
77
- * The resource as a sentence fragment, for "this key can read <fragment>".
78
- *
79
- * `"everything"` spells out all three namespaces, because that is exactly the
80
- * fact the old `*` input hid.
81
- */
82
- export function resourcePhrase(collection: string): string {
83
- const { kind, name } = parseResource(collection);
84
- switch (kind) {
85
- case "everything": return "every collection, every custom function and storage";
86
- case "storage": return "storage";
87
- case "all-functions": return "every custom function";
88
- case "function": return name ? `the ${name} function` : "one function";
89
- case "collection": return name ? `the ${name} collection` : "an unnamed collection";
90
- }
91
- }
92
-
93
- /** How each operation reads for a given namespace, so the sentence stays true. */
94
- function operationPhrase(kind: ResourceKind, operation: string): string {
95
- if (kind === "all-functions" || kind === "function") {
96
- // Functions are invoked, not written to; the HTTP method still picks
97
- // the operation, so a POST-only function needs `write`.
98
- return operation === "read" ? "call (GET)"
99
- : operation === "write" ? "call (POST, PUT, PATCH)"
100
- : "call (DELETE)";
101
- }
102
- if (kind === "storage") {
103
- return operation === "read" ? "download from"
104
- : operation === "write" ? "upload to"
105
- : "delete from";
106
- }
107
- return operation;
108
- }
109
-
110
- /** Join with an Oxford-less "and", the way the rest of the panel reads. */
111
- function joinPhrases(parts: string[]): string {
112
- if (parts.length <= 1) return parts[0] ?? "";
113
- return `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
114
- }
115
-
116
- /**
117
- * One plain sentence per permission entry: what the key will actually be able
118
- * to do. An entry with no operations selected grants nothing and says so,
119
- * rather than being silently dropped at submit time.
120
- */
121
- export function grantSentence(perm: ApiKeyPermission): string {
122
- const { kind } = parseResource(perm.collection);
123
- const target = resourcePhrase(perm.collection);
124
- if (perm.operations.length === 0) return `No access to ${target}`;
125
- const verbs = joinPhrases(perm.operations.map(op => operationPhrase(kind, op)));
126
- const verb = verbs.charAt(0).toUpperCase() + verbs.slice(1);
127
- return `${verb} ${target}`;
128
- }
129
-
130
- /**
131
- * Dense one-line summary for list rows and the created-key confirmation.
132
- *
133
- * The wildcard wins over everything else in the array, because the guard
134
- * returns on the first match — a key holding `*` is a full-access key no
135
- * matter what else is listed beside it.
136
- */
137
- export function permissionSummary(perms: ApiKeyPermission[]): string {
138
- if (perms.length === 0) return "No permissions";
139
- const wildcard = perms.find(p => p.collection === RESOURCE_EVERYTHING);
140
- if (wildcard) return `Everything (${wildcard.operations.join(", ")})`;
141
- if (perms.length === 1) {
142
- return `${resourceLabel(perms[0].collection)} (${perms[0].operations.join(", ")})`;
143
- }
144
- return `${perms.length} resources`;
145
- }
@@ -1,72 +0,0 @@
1
- import React from "react";
2
- import { Button, cls, iconSize, KeyRoundIcon, Typography } from "@rebasepro/ui";
3
- import { UserSelectPopover, SelectableUser } from "@rebasepro/app";
4
-
5
- export interface AuthSimulationSelectorProps {
6
- authMode: "jwt" | "none";
7
- setAuthMode: (mode: "jwt" | "none") => void;
8
- selectedUser: SelectableUser | null;
9
- setSelectedUser: (user: SelectableUser | null) => void;
10
- users: SelectableUser[];
11
- loading?: boolean;
12
- currentUser: SelectableUser | null;
13
- }
14
-
15
- export function AuthSimulationSelector({
16
- authMode,
17
- setAuthMode,
18
- selectedUser,
19
- setSelectedUser,
20
- users,
21
- loading,
22
- currentUser
23
- }: AuthSimulationSelectorProps) {
24
- return (
25
- <div className="flex flex-wrap items-center gap-3">
26
- <KeyRoundIcon size={iconSize.small} className="text-text-secondary dark:text-text-secondary-dark" />
27
- <Typography
28
- variant="caption"
29
- className="text-text-secondary dark:text-text-secondary-dark text-xs font-medium"
30
- >
31
- Auth:
32
- </Typography>
33
- <Button
34
- size="small"
35
- variant={authMode === "jwt" ? "outlined" : "outlined"}
36
- color={authMode === "jwt" ? "primary" : "neutral"}
37
- onClick={() => setAuthMode("jwt")}
38
- className="rounded-full !px-3 !py-1 !text-xs"
39
- >
40
- JWT Token
41
- </Button>
42
- <Button
43
- size="small"
44
- variant={authMode === "none" ? "outlined" : "outlined"}
45
- color={authMode === "none" ? "error" : "neutral"}
46
- onClick={() => setAuthMode("none")}
47
- className="rounded-full !px-3 !py-1 !text-xs"
48
- >
49
- No Auth
50
- </Button>
51
-
52
- {authMode === "jwt" && (
53
- <>
54
- <div className="w-px h-4 bg-surface-300 dark:bg-surface-600 mx-1" />
55
- <Typography
56
- variant="caption"
57
- className="text-text-secondary dark:text-text-secondary-dark text-xs font-medium"
58
- >
59
- Run as:
60
- </Typography>
61
- <UserSelectPopover
62
- selectedUser={selectedUser}
63
- onUserSelected={setSelectedUser}
64
- users={users}
65
- loading={loading}
66
- currentUser={currentUser}
67
- />
68
- </>
69
- )}
70
- </div>
71
- );
72
- }
@@ -1,185 +0,0 @@
1
- import React, { useState, useEffect, useRef, useCallback } from "react";
2
- import {
3
- Alert,
4
- Button,
5
- Chip,
6
- CircularProgress,
7
- cls,
8
- DatabaseIcon,
9
- defaultBorderMixin,
10
- DownloadIcon,
11
- iconSize,
12
- IconButton,
13
- RefreshCwIcon,
14
- Typography
15
- } from "@rebasepro/ui";
16
- import { useRebaseClient, useSnackbarController } from "@rebasepro/app";
17
- import type { BackupInfo, RebaseClient } from "@rebasepro/types";
18
-
19
- function formatSize(bytes: number | undefined): string {
20
- if (bytes === undefined || bytes === null) return "—";
21
- if (bytes < 1024) return `${bytes} B`;
22
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
23
- if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
24
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
25
- }
26
-
27
- function formatDate(iso: string | undefined): string {
28
- if (!iso) return "—";
29
- const d = new Date(iso);
30
- if (isNaN(d.getTime())) return "—";
31
- return d.toLocaleString();
32
- }
33
-
34
- export function BackupsView() {
35
- const client = useRebaseClient<RebaseClient>();
36
- const snackbar = useSnackbarController();
37
-
38
- const [backups, setBackups] = useState<BackupInfo[]>([]);
39
- const [destinationKind, setDestinationKind] = useState<string>("local");
40
- const [configured, setConfigured] = useState(true);
41
- const [loading, setLoading] = useState(true);
42
- const [downloading, setDownloading] = useState<string | null>(null);
43
-
44
- const clientRef = useRef(client);
45
- clientRef.current = client;
46
- const snackbarRef = useRef(snackbar);
47
- snackbarRef.current = snackbar;
48
-
49
- const load = useCallback(async () => {
50
- const c = clientRef.current;
51
- if (!c?.backups) {
52
- setLoading(false);
53
- setConfigured(false);
54
- return;
55
- }
56
- try {
57
- const res = await c.backups.list();
58
- setBackups(res.backups);
59
- setDestinationKind(res.destinationKind);
60
- setConfigured(res.configured);
61
- } catch (e: unknown) {
62
- snackbarRef.current.open({
63
- type: "error",
64
- message: e instanceof Error ? e.message : String(e)
65
- });
66
- } finally {
67
- setLoading(false);
68
- }
69
- }, []);
70
-
71
- useEffect(() => {
72
- load();
73
- }, [load]);
74
-
75
- const handleDownload = async (backup: BackupInfo) => {
76
- const c = clientRef.current;
77
- if (!c?.backups) return;
78
- setDownloading(backup.key);
79
- try {
80
- const blob = await c.backups.download(backup.key);
81
- const url = URL.createObjectURL(blob);
82
- const a = document.createElement("a");
83
- a.href = url;
84
- a.download = backup.name;
85
- document.body.appendChild(a);
86
- a.click();
87
- a.remove();
88
- URL.revokeObjectURL(url);
89
- } catch (e: unknown) {
90
- snackbarRef.current.open({
91
- type: "error",
92
- message: e instanceof Error ? e.message : String(e)
93
- });
94
- } finally {
95
- setDownloading(null);
96
- }
97
- };
98
-
99
- // Backups API not available on this client (e.g. non-Postgres backend).
100
- if (!client?.backups) {
101
- return (
102
- <div className="flex flex-col items-center justify-center h-full gap-4 text-center p-8">
103
- <DatabaseIcon size={iconSize.large} className="text-surface-300 dark:text-surface-600"/>
104
- <Typography variant="h6" color="secondary">Backups Not Available</Typography>
105
- <Typography variant="body2" color="disabled" className="max-w-md">
106
- The backups API is not exposed by this backend.
107
- </Typography>
108
- </div>
109
- );
110
- }
111
-
112
- if (loading) {
113
- return (
114
- <div className="flex items-center justify-center h-full">
115
- <CircularProgress/>
116
- </div>
117
- );
118
- }
119
-
120
- return (
121
- <div className="flex flex-col h-full w-full overflow-hidden bg-white dark:bg-surface-950">
122
- {/* Header */}
123
- <div className={cls("flex items-center justify-between px-5 py-2.5 border-b bg-surface-50 dark:bg-surface-900 min-h-[48px]", defaultBorderMixin)}>
124
- <div className="flex items-center gap-2">
125
- <DatabaseIcon size={iconSize.small} className="text-primary"/>
126
- <Typography variant="subtitle2" className="font-semibold">Backups</Typography>
127
- <Chip size="smallest" className="bg-surface-200 dark:bg-surface-700 text-surface-600 dark:text-surface-300">{backups.length}</Chip>
128
- <Chip size="smallest" className="bg-surface-100 dark:bg-surface-800 text-surface-500 dark:text-surface-400 uppercase font-mono text-[10px]">{destinationKind}</Chip>
129
- </div>
130
- <IconButton size="small" onClick={load} title="Refresh">
131
- <RefreshCwIcon size={iconSize.smallest}/>
132
- </IconButton>
133
- </div>
134
-
135
- <div className="flex-1 overflow-y-auto p-5">
136
- {!configured ? (
137
- <Alert color="info">
138
- <Typography variant="body2" className="text-[13px]">
139
- <strong>Backups are not configured.</strong> Set
140
- <code className="mx-1 px-1.5 py-0.5 rounded bg-surface-100 dark:bg-surface-950 font-mono text-[12px]">BACKUP_DESTINATION</code>
141
- (a local path or an <code className="font-mono text-[12px]">s3://</code> / <code className="font-mono text-[12px]">gs://</code> URL) and create backups with
142
- <code className="mx-1 px-1.5 py-0.5 rounded bg-surface-100 dark:bg-surface-950 font-mono text-[12px]">rebase db backup</code>
143
- or a scheduled cron. See docs/backups.md.
144
- </Typography>
145
- </Alert>
146
- ) : backups.length === 0 ? (
147
- <div className="flex flex-col items-center justify-center h-full gap-3 text-center">
148
- <DatabaseIcon size={iconSize.large} className="text-surface-200 dark:text-surface-700"/>
149
- <Typography variant="body2" color="disabled">
150
- No backups found yet. Run <code className="font-mono text-[12px]">rebase db backup</code> or wait for the next scheduled run.
151
- </Typography>
152
- </div>
153
- ) : (
154
- <div className="space-y-2 max-w-3xl">
155
- {backups.map(backup => (
156
- <div
157
- key={backup.key}
158
- className={cls("flex items-center gap-3 px-4 py-3 rounded-lg border bg-white dark:bg-surface-900", defaultBorderMixin)}
159
- >
160
- <DatabaseIcon size={iconSize.small} className="text-surface-400 shrink-0"/>
161
- <div className="flex-1 min-w-0">
162
- <Typography variant="body2" className="truncate font-medium font-mono text-[12px]">{backup.name}</Typography>
163
- <Typography variant="caption" color="secondary" className="text-[11px]">
164
- {formatDate(backup.createdAt)} · {formatSize(backup.sizeBytes)}
165
- </Typography>
166
- </div>
167
- <Button
168
- size="small"
169
- variant="outlined"
170
- onClick={() => handleDownload(backup)}
171
- disabled={downloading === backup.key}
172
- startIcon={downloading === backup.key
173
- ? <CircularProgress size="smallest"/>
174
- : <DownloadIcon size={iconSize.smallest}/>}
175
- >
176
- {downloading === backup.key ? "Downloading…" : "Download"}
177
- </Button>
178
- </div>
179
- ))}
180
- </div>
181
- )}
182
- </div>
183
- </div>
184
- );
185
- }