@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,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
|
-
}
|