@rebasepro/studio 0.21.0 → 0.21.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @rebasepro/studio
2
2
 
3
- Developer tools layer for Rebase — provides 9 lazy-loaded tools (SQL Console, JS Console, RLS Editor, Storage, Cron Jobs, Schema Visualizer, Branches, API Explorer, Logs Explorer) plus a customizable home page.
3
+ Developer tools layer for Rebase — provides 11 lazy-loaded tools (SQL Console, JS Console, RLS Editor, Storage, Cron Jobs, Schema Visualizer, Branches, Backups, API Explorer, Logs Explorer, API Keys) plus a customizable home page.
4
4
 
5
5
  ## Installation
6
6
 
@@ -9,13 +9,13 @@ pnpm add @rebasepro/studio
9
9
  ```
10
10
 
11
11
  ESM-only: `"type": "module"` with no CommonJS build, so it is loaded with
12
- `import`. `require()` of it resolves only on Node 22.12+, which supports
13
- `require(esm)`.
12
+ `import`. It needs Node `>=22.22.0` (its `engines` floor), where `require()`
13
+ of it resolves too: Node has supported `require(esm)` since 22.12.
14
14
 
15
15
  ### Peer Dependencies
16
16
 
17
- - `react` >= 19.0.0
18
- - `react-dom` >= 19.0.0
17
+ - `react` ^19.2.7
18
+ - `react-dom` ^19.2.7
19
19
  - `react-router` ^8.3.0
20
20
  - `@rebasepro/cms` (optional)
21
21
 
@@ -34,10 +34,12 @@ ESM-only: `"type": "module"` with no CommonJS build, so it is loaded with
34
34
  | `cron` | Cron Jobs | Compute | Monitor scheduled tasks |
35
35
  | `schema-visualizer` | Schema Visualizer | Database | Interactive database ERD |
36
36
  | `branches` | Branches | Database | Create and manage database branches |
37
+ | `backups` | Backups | Database | Download database backups |
37
38
  | `api` | API Explorer | API | Interactive API docs and testing |
38
39
  | `logs` | Logs Explorer | Database | Real-time system and query logs |
40
+ | `api-keys` | API Keys | Access Control | Create and manage scoped API keys |
39
41
 
40
- All 9 tools are enabled by default. The `schema` tool (collection editor) is auto-injected by the CMS when `collectionEditor` is enabled — it is **not** registered here.
42
+ All 11 tools are enabled by default. The `schema` tool (collection editor) is auto-injected by the CMS when `collectionEditor` is enabled — it is **not** registered here.
41
43
 
42
44
  ## Key Exports
43
45
 
@@ -48,7 +50,7 @@ All 9 tools are enabled by default. The `schema` tool (collection editor) is aut
48
50
  | `StudioBridgeProvider` | Component | Re-exported from `@rebasepro/app` |
49
51
  | `StudioBridgeContext` | Context | Re-exported from `@rebasepro/app` |
50
52
  | `useStudioCollectionRegistry` | Hook | Access the collection registry |
51
- | `useStudioSidePanelController` | Hook | Control the side snapshot panel |
53
+ | `useStudioSidePanelController` | Hook | Control the side entity panel |
52
54
  | `useStudioUrlController` | Hook | URL state management |
53
55
  | `useStudioNavigationState` | Hook | Navigation state |
54
56
  | `useStudioBreadcrumbs` | Hook | Breadcrumb management |
@@ -71,7 +73,7 @@ beside them, pass an `AppView` to `<RebaseStudio devViews>`.
71
73
  ```tsx
72
74
  import { RebaseStudio } from "@rebasepro/studio";
73
75
 
74
- // Inside your Rebase app — enable all 9 tools (default)
76
+ // Inside your Rebase app — enable all 11 tools (default)
75
77
  <RebaseStudio />
76
78
 
77
79
  // Or pick specific tools
@@ -2,7 +2,7 @@ import { t as classifyLoadFailure } from "./load-failure-C6OMJSGC.js";
2
2
  import { t as LoadFailureView } from "./load-failure-view-CBTB_h-h.js";
3
3
  import { useRebaseClient, useSnackbarController, useTranslation } from "@rebasepro/app";
4
4
  import { useCallback, useEffect, useRef, useState } from "react";
5
- import { Alert, Button, Chip, CircularProgress, DatabaseIcon, DownloadIcon, IconButton, RefreshCwIcon, Typography, cls, defaultBorderMixin, iconSize } from "@rebasepro/ui";
5
+ import { Alert, Button, Chip, CircularProgress, DatabaseIcon, DownloadIcon, IconButton, RefreshCwIcon, Tooltip, Typography, cls, defaultBorderMixin, iconSize } from "@rebasepro/ui";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
7
  //#region src/components/Backups/BackupsView.tsx
8
8
  function formatSize(bytes) {
@@ -18,6 +18,13 @@ function formatDate(iso) {
18
18
  if (isNaN(d.getTime())) return "—";
19
19
  return d.toLocaleString();
20
20
  }
21
+ /**
22
+ * The name the roles sidecar is saved under: the dump's, with the suffix
23
+ * `rebase db restore` looks for beside it.
24
+ */
25
+ function rolesFileName(dumpName) {
26
+ return dumpName.replace(/\.dump$/, "") + ".globals.sql";
27
+ }
21
28
  function BackupsView() {
22
29
  const client = useRebaseClient();
23
30
  const snackbar = useSnackbarController();
@@ -55,16 +62,16 @@ function BackupsView() {
55
62
  useEffect(() => {
56
63
  load();
57
64
  }, [load]);
58
- const handleDownload = async (backup) => {
65
+ const handleDownload = async (key, fileName) => {
59
66
  const c = clientRef.current;
60
67
  if (!c?.backups) return;
61
- setDownloading(backup.key);
68
+ setDownloading(key);
62
69
  try {
63
- const blob = await c.backups.download(backup.key);
70
+ const blob = await c.backups.download(key);
64
71
  const url = URL.createObjectURL(blob);
65
72
  const a = document.createElement("a");
66
73
  a.href = url;
67
- a.download = backup.name;
74
+ a.download = fileName;
68
75
  document.body.appendChild(a);
69
76
  a.click();
70
77
  a.remove();
@@ -181,40 +188,61 @@ function BackupsView() {
181
188
  })]
182
189
  }) : /* @__PURE__ */ jsx("div", {
183
190
  className: "space-y-2 max-w-3xl",
184
- children: backups.map((backup) => /* @__PURE__ */ jsxs("div", {
185
- className: cls("flex items-center gap-3 px-4 py-3 rounded-lg border bg-surface-card", defaultBorderMixin),
186
- children: [
187
- /* @__PURE__ */ jsx(DatabaseIcon, {
188
- size: iconSize.small,
189
- className: "text-surface-400 shrink-0"
190
- }),
191
- /* @__PURE__ */ jsxs("div", {
192
- className: "flex-1 min-w-0",
193
- children: [/* @__PURE__ */ jsx(Typography, {
194
- variant: "body2",
195
- className: "truncate font-medium font-mono text-[12px]",
196
- children: backup.name
197
- }), /* @__PURE__ */ jsxs(Typography, {
198
- variant: "caption",
199
- color: "secondary",
200
- className: "text-[11px]",
201
- children: [
202
- formatDate(backup.createdAt),
203
- " · ",
204
- formatSize(backup.sizeBytes)
205
- ]
206
- })]
207
- }),
208
- /* @__PURE__ */ jsx(Button, {
209
- size: "small",
210
- variant: "outlined",
211
- onClick: () => handleDownload(backup),
212
- disabled: downloading === backup.key,
213
- startIcon: downloading === backup.key ? /* @__PURE__ */ jsx(CircularProgress, { size: "smallest" }) : /* @__PURE__ */ jsx(DownloadIcon, { size: iconSize.smallest }),
214
- children: downloading === backup.key ? t("studio_backups_downloading") : t("download")
215
- })
216
- ]
217
- }, backup.key))
191
+ children: backups.map((backup) => {
192
+ const globalsKey = backup.globalsKey;
193
+ return /* @__PURE__ */ jsxs("div", {
194
+ className: cls("flex items-center gap-3 px-4 py-3 rounded-lg border bg-surface-card", defaultBorderMixin),
195
+ children: [
196
+ /* @__PURE__ */ jsx(DatabaseIcon, {
197
+ size: iconSize.small,
198
+ className: "text-surface-400 shrink-0"
199
+ }),
200
+ /* @__PURE__ */ jsxs("div", {
201
+ className: "flex-1 min-w-0",
202
+ children: [/* @__PURE__ */ jsx(Typography, {
203
+ variant: "body2",
204
+ className: "truncate font-medium font-mono text-[12px]",
205
+ children: backup.name
206
+ }), /* @__PURE__ */ jsxs(Typography, {
207
+ variant: "caption",
208
+ color: "secondary",
209
+ className: "text-[11px]",
210
+ children: [
211
+ formatDate(backup.createdAt),
212
+ " · ",
213
+ formatSize(backup.sizeBytes)
214
+ ]
215
+ })]
216
+ }),
217
+ globalsKey ? /* @__PURE__ */ jsx(Tooltip, {
218
+ title: t("studio_backups_roles_file_hint"),
219
+ children: /* @__PURE__ */ jsx(Button, {
220
+ size: "small",
221
+ variant: "text",
222
+ onClick: () => handleDownload(globalsKey, rolesFileName(backup.name)),
223
+ disabled: downloading === globalsKey,
224
+ startIcon: downloading === globalsKey ? /* @__PURE__ */ jsx(CircularProgress, { size: "smallest" }) : /* @__PURE__ */ jsx(DownloadIcon, { size: iconSize.smallest }),
225
+ children: downloading === globalsKey ? t("studio_backups_downloading") : t("studio_backups_roles_file")
226
+ })
227
+ }) : /* @__PURE__ */ jsx(Tooltip, {
228
+ title: t("studio_backups_no_roles_file_hint"),
229
+ children: /* @__PURE__ */ jsx(Chip, {
230
+ size: "smallest",
231
+ colorScheme: "orangeDarker",
232
+ children: t("studio_backups_no_roles_file")
233
+ })
234
+ }),
235
+ /* @__PURE__ */ jsx(Button, {
236
+ size: "small",
237
+ variant: "outlined",
238
+ onClick: () => handleDownload(backup.key, backup.name),
239
+ disabled: downloading === backup.key,
240
+ startIcon: downloading === backup.key ? /* @__PURE__ */ jsx(CircularProgress, { size: "smallest" }) : /* @__PURE__ */ jsx(DownloadIcon, { size: iconSize.smallest }),
241
+ children: downloading === backup.key ? t("studio_backups_downloading") : t("download")
242
+ })
243
+ ]
244
+ }, backup.key);
245
+ })
218
246
  })
219
247
  })]
220
248
  });
@@ -222,4 +250,4 @@ function BackupsView() {
222
250
  //#endregion
223
251
  export { BackupsView };
224
252
 
225
- //# sourceMappingURL=BackupsView-D0ZDqTEn.js.map
253
+ //# sourceMappingURL=BackupsView-DYTAD6aW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BackupsView-DYTAD6aW.js","names":[],"sources":["../src/components/Backups/BackupsView.tsx"],"sourcesContent":["import React, { useState, useEffect, useRef, useCallback } from \"react\";\nimport {\n Alert,\n Button,\n Chip,\n CircularProgress,\n cls,\n DatabaseIcon,\n defaultBorderMixin,\n DownloadIcon,\n iconSize,\n IconButton,\n RefreshCwIcon,\n Tooltip,\n Typography\n} from \"@rebasepro/ui\";\nimport { useRebaseClient, useSnackbarController, useTranslation } from \"@rebasepro/app\";\nimport type { BackupInfo, RebaseClient } from \"@rebasepro/types\";\n\nimport { classifyLoadFailure, type LoadFailure } from \"../load-failure\";\nimport { LoadFailureView } from \"../load-failure-view\";\n\nfunction formatSize(bytes: number | undefined): string {\n if (bytes === undefined || bytes === null) return \"—\";\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;\n}\n\nfunction formatDate(iso: string | undefined): string {\n if (!iso) return \"—\";\n const d = new Date(iso);\n if (isNaN(d.getTime())) return \"—\";\n return d.toLocaleString();\n}\n\n/**\n * The name the roles sidecar is saved under: the dump's, with the suffix\n * `rebase db restore` looks for beside it.\n */\nfunction rolesFileName(dumpName: string): string {\n return dumpName.replace(/\\.dump$/, \"\") + \".globals.sql\";\n}\n\nexport function BackupsView() {\n const client = useRebaseClient<RebaseClient>();\n const snackbar = useSnackbarController();\n const { t } = useTranslation();\n\n const [backups, setBackups] = useState<BackupInfo[]>([]);\n const [destinationKind, setDestinationKind] = useState<string>(\"local\");\n const [configured, setConfigured] = useState(true);\n const [loading, setLoading] = useState(true);\n const [downloading, setDownloading] = useState<string | null>(null);\n /** Why the listing failed, classified — see `load-failure.ts`. */\n const [failure, setFailure] = useState<LoadFailure | null>(null);\n\n const clientRef = useRef(client);\n clientRef.current = client;\n const snackbarRef = useRef(snackbar);\n snackbarRef.current = snackbar;\n\n const load = useCallback(async () => {\n const c = clientRef.current;\n if (!c?.backups) {\n setLoading(false);\n setConfigured(false);\n return;\n }\n setFailure(null);\n try {\n const res = await c.backups.list();\n setBackups(res.backups);\n setDestinationKind(res.destinationKind);\n setConfigured(res.configured);\n } catch (e: unknown) {\n // The snackbar is gone in seconds, and the list underneath it is\n // empty — which is how a refused listing came to read \"No backups\n // found yet\" about a database that has backups. Keep the reason on\n // screen instead.\n setFailure(classifyLoadFailure(e));\n } finally {\n setLoading(false);\n }\n }, []);\n\n useEffect(() => {\n load();\n }, [load]);\n\n const handleDownload = async (key: string, fileName: string) => {\n const c = clientRef.current;\n if (!c?.backups) return;\n setDownloading(key);\n try {\n const blob = await c.backups.download(key);\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = fileName;\n document.body.appendChild(a);\n a.click();\n a.remove();\n URL.revokeObjectURL(url);\n } catch (e: unknown) {\n snackbarRef.current.open({\n type: \"error\",\n message: e instanceof Error ? e.message : String(e)\n });\n } finally {\n setDownloading(null);\n }\n };\n\n // Backups API not available on this client (e.g. non-Postgres backend).\n if (!client?.backups) {\n return (\n <div className=\"flex flex-col items-center justify-center h-full gap-4 text-center p-8\">\n <DatabaseIcon size={iconSize.large} className=\"text-surface-300 dark:text-surface-600\"/>\n <Typography variant=\"h6\" color=\"secondary\">Backups Not Available</Typography>\n <Typography variant=\"body2\" color=\"disabled\" className=\"max-w-md\">\n The backups API is not exposed by this backend.\n </Typography>\n </div>\n );\n }\n\n if (loading) {\n return (\n <div className=\"flex items-center justify-center h-full\">\n <CircularProgress/>\n </div>\n );\n }\n\n return (\n <div className=\"flex flex-col h-full w-full overflow-hidden bg-surface-card\">\n {/* Header */}\n <div className={cls(\"flex items-center justify-between px-5 py-2.5 border-b bg-surface-sheet min-h-[48px]\", defaultBorderMixin)}>\n <div className=\"flex items-center gap-2\">\n <DatabaseIcon size={iconSize.small} className=\"text-primary\"/>\n <Typography variant=\"subtitle2\" className=\"font-semibold\">{t(\"studio_tool_backups\")}</Typography>\n <Chip size=\"smallest\" className=\"bg-surface-raised text-surface-600 dark:text-surface-300\">{backups.length}</Chip>\n <Chip size=\"smallest\" className=\"bg-surface-raised text-surface-500 dark:text-surface-400 uppercase font-mono text-[10px]\">{destinationKind}</Chip>\n </div>\n <IconButton size=\"small\" onClick={load} title=\"Refresh\">\n <RefreshCwIcon size={iconSize.smallest}/>\n </IconButton>\n </div>\n\n <div className=\"flex-1 overflow-y-auto p-5\">\n {failure ? (\n <LoadFailureView\n failure={failure}\n title={t(\"studio_backups_read_failed\")}\n deniedTitle={t(\"studio_backups_denied_title\")}\n deniedHint={t(\"studio_backups_denied_hint\")}\n onRetry={load}\n />\n ) : !configured ? (\n <Alert color=\"info\">\n <Typography variant=\"body2\" className=\"text-[13px]\">\n <strong>{t(\"studio_backups_not_configured_title\")}</strong>{\" \"}\n {t(\"studio_backups_not_configured_body\")}{\" \"}\n <a\n href=\"https://rebase.pro/docs/backend/jobs\"\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"text-primary underline\"\n >\n {t(\"studio_read_the_docs\")}\n </a>\n </Typography>\n </Alert>\n ) : backups.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center h-full gap-3 text-center\">\n <DatabaseIcon size={iconSize.large} className=\"text-surface-200 dark:text-surface-700\"/>\n <Typography variant=\"body2\" color=\"disabled\">\n No backups found yet. Run <code className=\"font-mono text-[12px]\">rebase db backup</code> or wait for the next scheduled run.\n </Typography>\n </div>\n ) : (\n <div className=\"space-y-2 max-w-3xl\">\n {backups.map(backup => {\n // The roles sidecar travels with the dump: a restore\n // into a new Postgres needs both files.\n const globalsKey = backup.globalsKey;\n return (\n <div\n key={backup.key}\n className={cls(\"flex items-center gap-3 px-4 py-3 rounded-lg border bg-surface-card\", defaultBorderMixin)}\n >\n <DatabaseIcon size={iconSize.small} className=\"text-surface-400 shrink-0\"/>\n <div className=\"flex-1 min-w-0\">\n <Typography variant=\"body2\" className=\"truncate font-medium font-mono text-[12px]\">{backup.name}</Typography>\n <Typography variant=\"caption\" color=\"secondary\" className=\"text-[11px]\">\n {formatDate(backup.createdAt)} · {formatSize(backup.sizeBytes)}\n </Typography>\n </div>\n {globalsKey ? (\n <Tooltip title={t(\"studio_backups_roles_file_hint\")}>\n <Button\n size=\"small\"\n variant=\"text\"\n onClick={() => handleDownload(globalsKey, rolesFileName(backup.name))}\n disabled={downloading === globalsKey}\n startIcon={downloading === globalsKey\n ? <CircularProgress size=\"smallest\"/>\n : <DownloadIcon size={iconSize.smallest}/>}\n >\n {downloading === globalsKey ? t(\"studio_backups_downloading\") : t(\"studio_backups_roles_file\")}\n </Button>\n </Tooltip>\n ) : (\n <Tooltip title={t(\"studio_backups_no_roles_file_hint\")}>\n <Chip size=\"smallest\" colorScheme=\"orangeDarker\">{t(\"studio_backups_no_roles_file\")}</Chip>\n </Tooltip>\n )}\n <Button\n size=\"small\"\n variant=\"outlined\"\n onClick={() => handleDownload(backup.key, backup.name)}\n disabled={downloading === backup.key}\n startIcon={downloading === backup.key\n ? <CircularProgress size=\"smallest\"/>\n : <DownloadIcon size={iconSize.smallest}/>}\n >\n {downloading === backup.key ? t(\"studio_backups_downloading\") : t(\"download\")}\n </Button>\n </div>\n );\n })}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
package/dist/index.es.js CHANGED
@@ -557,7 +557,7 @@ var StorageView = lazyChunk(() => import("./StorageView-C52dSZNf.js").then((m) =
557
557
  var CronJobsView = lazyChunk(() => import("./CronJobsView-JwRjQCde.js").then((m) => ({ default: m.CronJobsView })));
558
558
  var SchemaVisualizer = lazyChunk(() => import("./SchemaVisualizer-DtDzIUNy.js").then((m) => ({ default: m.SchemaVisualizer })));
559
559
  var BranchesView = lazyChunk(() => import("./BranchesView-D6Sz1a5f.js").then((m) => ({ default: m.BranchesView })));
560
- var BackupsView = lazyChunk(() => import("./BackupsView-D0ZDqTEn.js").then((m) => ({ default: m.BackupsView })));
560
+ var BackupsView = lazyChunk(() => import("./BackupsView-DYTAD6aW.js").then((m) => ({ default: m.BackupsView })));
561
561
  var ApiExplorer = lazyChunk(() => import("./ApiExplorer-D-jLWAaY.js").then((m) => ({ default: m.ApiExplorer })));
562
562
  var LogsExplorer = lazyChunk(() => import("./LogsExplorer-Xq8K4oo6.js").then((m) => ({ default: m.LogsExplorer })));
563
563
  var ApiKeysView = lazyChunk(() => import("./ApiKeysView-OfJLH2x7.js").then((m) => ({ default: m.ApiKeysView })));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/studio",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
4
4
  "description": "Rebase Studio — the SQL editor, schema visualiser, RLS policy editor and API explorer.",
5
5
  "keywords": [
6
6
  "rebase",
@@ -35,19 +35,19 @@
35
35
  "pgsql-ast-parser": "12.0.2",
36
36
  "prism-react-renderer": "^2.4.1",
37
37
  "react-dropzone": "^19.1.1",
38
- "@rebasepro/cms-types": "0.21.0",
39
- "@rebasepro/app": "0.21.0",
40
- "@rebasepro/client": "0.21.0",
41
- "@rebasepro/common": "0.21.0",
42
- "@rebasepro/types": "0.21.0",
43
- "@rebasepro/utils": "0.21.0",
44
- "@rebasepro/ui": "0.21.0"
38
+ "@rebasepro/cms-types": "0.21.1",
39
+ "@rebasepro/app": "0.21.1",
40
+ "@rebasepro/client": "0.21.1",
41
+ "@rebasepro/common": "0.21.1",
42
+ "@rebasepro/ui": "0.21.1",
43
+ "@rebasepro/types": "0.21.1",
44
+ "@rebasepro/utils": "0.21.1"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "react": "^19.2.7",
48
48
  "react-dom": "^19.2.7",
49
49
  "react-router": "^8.3.0",
50
- "@rebasepro/cms": "0.21.0"
50
+ "@rebasepro/cms": "0.21.1"
51
51
  },
52
52
  "peerDependenciesMeta": {
53
53
  "@rebasepro/cms": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"BackupsView-D0ZDqTEn.js","names":[],"sources":["../src/components/Backups/BackupsView.tsx"],"sourcesContent":["import React, { useState, useEffect, useRef, useCallback } from \"react\";\nimport {\n Alert,\n Button,\n Chip,\n CircularProgress,\n cls,\n DatabaseIcon,\n defaultBorderMixin,\n DownloadIcon,\n iconSize,\n IconButton,\n RefreshCwIcon,\n Typography\n} from \"@rebasepro/ui\";\nimport { useRebaseClient, useSnackbarController, useTranslation } from \"@rebasepro/app\";\nimport type { BackupInfo, RebaseClient } from \"@rebasepro/types\";\n\nimport { classifyLoadFailure, type LoadFailure } from \"../load-failure\";\nimport { LoadFailureView } from \"../load-failure-view\";\n\nfunction formatSize(bytes: number | undefined): string {\n if (bytes === undefined || bytes === null) return \"—\";\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;\n}\n\nfunction formatDate(iso: string | undefined): string {\n if (!iso) return \"—\";\n const d = new Date(iso);\n if (isNaN(d.getTime())) return \"—\";\n return d.toLocaleString();\n}\n\nexport function BackupsView() {\n const client = useRebaseClient<RebaseClient>();\n const snackbar = useSnackbarController();\n const { t } = useTranslation();\n\n const [backups, setBackups] = useState<BackupInfo[]>([]);\n const [destinationKind, setDestinationKind] = useState<string>(\"local\");\n const [configured, setConfigured] = useState(true);\n const [loading, setLoading] = useState(true);\n const [downloading, setDownloading] = useState<string | null>(null);\n /** Why the listing failed, classified — see `load-failure.ts`. */\n const [failure, setFailure] = useState<LoadFailure | null>(null);\n\n const clientRef = useRef(client);\n clientRef.current = client;\n const snackbarRef = useRef(snackbar);\n snackbarRef.current = snackbar;\n\n const load = useCallback(async () => {\n const c = clientRef.current;\n if (!c?.backups) {\n setLoading(false);\n setConfigured(false);\n return;\n }\n setFailure(null);\n try {\n const res = await c.backups.list();\n setBackups(res.backups);\n setDestinationKind(res.destinationKind);\n setConfigured(res.configured);\n } catch (e: unknown) {\n // The snackbar is gone in seconds, and the list underneath it is\n // empty — which is how a refused listing came to read \"No backups\n // found yet\" about a database that has backups. Keep the reason on\n // screen instead.\n setFailure(classifyLoadFailure(e));\n } finally {\n setLoading(false);\n }\n }, []);\n\n useEffect(() => {\n load();\n }, [load]);\n\n const handleDownload = async (backup: BackupInfo) => {\n const c = clientRef.current;\n if (!c?.backups) return;\n setDownloading(backup.key);\n try {\n const blob = await c.backups.download(backup.key);\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = backup.name;\n document.body.appendChild(a);\n a.click();\n a.remove();\n URL.revokeObjectURL(url);\n } catch (e: unknown) {\n snackbarRef.current.open({\n type: \"error\",\n message: e instanceof Error ? e.message : String(e)\n });\n } finally {\n setDownloading(null);\n }\n };\n\n // Backups API not available on this client (e.g. non-Postgres backend).\n if (!client?.backups) {\n return (\n <div className=\"flex flex-col items-center justify-center h-full gap-4 text-center p-8\">\n <DatabaseIcon size={iconSize.large} className=\"text-surface-300 dark:text-surface-600\"/>\n <Typography variant=\"h6\" color=\"secondary\">Backups Not Available</Typography>\n <Typography variant=\"body2\" color=\"disabled\" className=\"max-w-md\">\n The backups API is not exposed by this backend.\n </Typography>\n </div>\n );\n }\n\n if (loading) {\n return (\n <div className=\"flex items-center justify-center h-full\">\n <CircularProgress/>\n </div>\n );\n }\n\n return (\n <div className=\"flex flex-col h-full w-full overflow-hidden bg-surface-card\">\n {/* Header */}\n <div className={cls(\"flex items-center justify-between px-5 py-2.5 border-b bg-surface-sheet min-h-[48px]\", defaultBorderMixin)}>\n <div className=\"flex items-center gap-2\">\n <DatabaseIcon size={iconSize.small} className=\"text-primary\"/>\n <Typography variant=\"subtitle2\" className=\"font-semibold\">{t(\"studio_tool_backups\")}</Typography>\n <Chip size=\"smallest\" className=\"bg-surface-raised text-surface-600 dark:text-surface-300\">{backups.length}</Chip>\n <Chip size=\"smallest\" className=\"bg-surface-raised text-surface-500 dark:text-surface-400 uppercase font-mono text-[10px]\">{destinationKind}</Chip>\n </div>\n <IconButton size=\"small\" onClick={load} title=\"Refresh\">\n <RefreshCwIcon size={iconSize.smallest}/>\n </IconButton>\n </div>\n\n <div className=\"flex-1 overflow-y-auto p-5\">\n {failure ? (\n <LoadFailureView\n failure={failure}\n title={t(\"studio_backups_read_failed\")}\n deniedTitle={t(\"studio_backups_denied_title\")}\n deniedHint={t(\"studio_backups_denied_hint\")}\n onRetry={load}\n />\n ) : !configured ? (\n <Alert color=\"info\">\n <Typography variant=\"body2\" className=\"text-[13px]\">\n <strong>{t(\"studio_backups_not_configured_title\")}</strong>{\" \"}\n {t(\"studio_backups_not_configured_body\")}{\" \"}\n <a\n href=\"https://rebase.pro/docs/backend/jobs\"\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"text-primary underline\"\n >\n {t(\"studio_read_the_docs\")}\n </a>\n </Typography>\n </Alert>\n ) : backups.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center h-full gap-3 text-center\">\n <DatabaseIcon size={iconSize.large} className=\"text-surface-200 dark:text-surface-700\"/>\n <Typography variant=\"body2\" color=\"disabled\">\n No backups found yet. Run <code className=\"font-mono text-[12px]\">rebase db backup</code> or wait for the next scheduled run.\n </Typography>\n </div>\n ) : (\n <div className=\"space-y-2 max-w-3xl\">\n {backups.map(backup => (\n <div\n key={backup.key}\n className={cls(\"flex items-center gap-3 px-4 py-3 rounded-lg border bg-surface-card\", defaultBorderMixin)}\n >\n <DatabaseIcon size={iconSize.small} className=\"text-surface-400 shrink-0\"/>\n <div className=\"flex-1 min-w-0\">\n <Typography variant=\"body2\" className=\"truncate font-medium font-mono text-[12px]\">{backup.name}</Typography>\n <Typography variant=\"caption\" color=\"secondary\" className=\"text-[11px]\">\n {formatDate(backup.createdAt)} · {formatSize(backup.sizeBytes)}\n </Typography>\n </div>\n <Button\n size=\"small\"\n variant=\"outlined\"\n onClick={() => handleDownload(backup)}\n disabled={downloading === backup.key}\n startIcon={downloading === backup.key\n ? <CircularProgress size=\"smallest\"/>\n : <DownloadIcon size={iconSize.smallest}/>}\n >\n {downloading === backup.key ? t(\"studio_backups_downloading\") : t(\"download\")}\n </Button>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}