@rebasepro/studio 0.22.0 → 0.24.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/dist/{ApiExplorer-D-jLWAaY.js → ApiExplorer-CCtczj3F.js} +16 -32
- package/dist/ApiExplorer-CCtczj3F.js.map +1 -0
- package/dist/ApiKeysView-B66TdzhH.js +1738 -0
- package/dist/ApiKeysView-B66TdzhH.js.map +1 -0
- package/dist/BackupsView-DmSMzAbT.js +368 -0
- package/dist/BackupsView-DmSMzAbT.js.map +1 -0
- package/dist/{BranchesView-D6Sz1a5f.js → BranchesView-DP1a87E-.js} +12 -8
- package/dist/BranchesView-DP1a87E-.js.map +1 -0
- package/dist/{CronJobsView-JwRjQCde.js → CronJobsView-TDErHTuK.js} +143 -68
- package/dist/CronJobsView-TDErHTuK.js.map +1 -0
- package/dist/{JSEditor-qiuwmcvk.js → JSEditor-B59pwg4c.js} +120 -74
- package/dist/JSEditor-B59pwg4c.js.map +1 -0
- package/dist/{LogsExplorer-Xq8K4oo6.js → LogsExplorer-BKM4yfP8.js} +48 -22
- package/dist/LogsExplorer-BKM4yfP8.js.map +1 -0
- package/dist/{MonacoEditor-D0_QljcX.js → MonacoEditor-CUxy5_k5.js} +11 -9
- package/dist/MonacoEditor-CUxy5_k5.js.map +1 -0
- package/dist/{RLSEditor-B-qrgdHy.js → RLSEditor-BlLLV6xs.js} +350 -98
- package/dist/RLSEditor-BlLLV6xs.js.map +1 -0
- package/dist/{SQLEditor-sdtoZv5F.js → SQLEditor-1An7w0j6.js} +496 -224
- package/dist/SQLEditor-1An7w0j6.js.map +1 -0
- package/dist/{SchemaVisualizer-DtDzIUNy.js → SchemaVisualizer-DCLVfA5t.js} +20 -24
- package/dist/SchemaVisualizer-DCLVfA5t.js.map +1 -0
- package/dist/{StorageView-C52dSZNf.js → StorageView-CbsHuCBf.js} +382 -228
- package/dist/StorageView-CbsHuCBf.js.map +1 -0
- package/dist/components/ApiExplorer/TryItPanel.d.ts +6 -1
- package/dist/components/ApiKeys/CreateApiKeyDialog.d.ts +21 -2
- package/dist/components/ApiKeys/scope-words.d.ts +37 -0
- package/dist/components/ApiKeys/scopes.d.ts +113 -0
- package/dist/components/AuthSimulationSelector.d.ts +5 -1
- package/dist/components/LogsExplorer/useLogTail.d.ts +9 -0
- package/dist/components/RLSEditor/policyListing.d.ts +44 -0
- package/dist/components/RLSEditor/policyRules.d.ts +49 -0
- package/dist/components/SQLEditor/MonacoEditor.d.ts +13 -1
- package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +0 -1
- package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +6 -0
- package/dist/components/load-failure.d.ts +1 -1
- package/dist/components/useRunAsUsers.d.ts +25 -0
- package/dist/index.es.js +11 -11
- package/dist/{load-failure-C6OMJSGC.js → load-failure-Bd8lFIO7.js} +2 -2
- package/dist/load-failure-Bd8lFIO7.js.map +1 -0
- package/dist/useRunAsUsers-CLaAtXt6.js +122 -0
- package/dist/useRunAsUsers-CLaAtXt6.js.map +1 -0
- package/dist/utils/sql_utils.d.ts +126 -36
- package/package.json +9 -9
- package/dist/ApiExplorer-D-jLWAaY.js.map +0 -1
- package/dist/ApiKeysView-OfJLH2x7.js +0 -1239
- package/dist/ApiKeysView-OfJLH2x7.js.map +0 -1
- package/dist/AuthSimulationSelector-BewmhMBw.js +0 -56
- package/dist/AuthSimulationSelector-BewmhMBw.js.map +0 -1
- package/dist/BackupsView-DYTAD6aW.js +0 -253
- package/dist/BackupsView-DYTAD6aW.js.map +0 -1
- package/dist/BranchesView-D6Sz1a5f.js.map +0 -1
- package/dist/CronJobsView-JwRjQCde.js.map +0 -1
- package/dist/JSEditor-qiuwmcvk.js.map +0 -1
- package/dist/LogsExplorer-Xq8K4oo6.js.map +0 -1
- package/dist/MonacoEditor-D0_QljcX.js.map +0 -1
- package/dist/RLSEditor-B-qrgdHy.js.map +0 -1
- package/dist/SQLEditor-sdtoZv5F.js.map +0 -1
- package/dist/SchemaVisualizer-DtDzIUNy.js.map +0 -1
- package/dist/StorageView-C52dSZNf.js.map +0 -1
- package/dist/components/ApiKeys/permissions.d.ts +0 -66
- package/dist/load-failure-C6OMJSGC.js.map +0 -1
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { t as classifyLoadFailure } from "./load-failure-Bd8lFIO7.js";
|
|
2
|
+
import { t as LoadFailureView } from "./load-failure-view-CBTB_h-h.js";
|
|
3
|
+
import { useRebaseClient, useSnackbarController, useTranslation } from "@rebasepro/app";
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
|
+
import { Alert, Button, Chip, CircularProgress, DatabaseIcon, DownloadIcon, IconButton, RefreshCwIcon, Tooltip, Typography, cls, defaultBorderMixin, iconSize } from "@rebasepro/ui";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
//#region src/components/Backups/BackupsView.tsx
|
|
8
|
+
function formatSize(bytes) {
|
|
9
|
+
if (bytes === void 0 || bytes === null) return "—";
|
|
10
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
11
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
12
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
13
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
14
|
+
}
|
|
15
|
+
function formatDate(iso) {
|
|
16
|
+
if (!iso) return "—";
|
|
17
|
+
const d = new Date(iso);
|
|
18
|
+
if (isNaN(d.getTime())) return "—";
|
|
19
|
+
return d.toLocaleString();
|
|
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
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* How the scheduled backup is doing, above the list — and, apart from it, a run
|
|
30
|
+
* someone started by hand since, which is not the schedule's.
|
|
31
|
+
*/
|
|
32
|
+
function BackupScheduleSummary({ schedule }) {
|
|
33
|
+
const { t } = useTranslation();
|
|
34
|
+
const manual = schedule.lastManualRun;
|
|
35
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
36
|
+
className: "flex flex-col gap-2",
|
|
37
|
+
children: [/* @__PURE__ */ jsx(ScheduledBackupStatus, { schedule }), manual && (manual.success ? /* @__PURE__ */ jsx(Typography, {
|
|
38
|
+
variant: "caption",
|
|
39
|
+
color: "secondary",
|
|
40
|
+
className: "text-[12px]",
|
|
41
|
+
children: t("studio_backups_manual_run_ok", { when: formatDate(manual.startedAt) })
|
|
42
|
+
}) : /* @__PURE__ */ jsxs(Alert, {
|
|
43
|
+
color: "warning",
|
|
44
|
+
children: [/* @__PURE__ */ jsx(Typography, {
|
|
45
|
+
variant: "body2",
|
|
46
|
+
className: "text-[13px] font-semibold",
|
|
47
|
+
children: t("studio_backups_manual_run_failed", { when: formatDate(manual.startedAt) })
|
|
48
|
+
}), manual.error && /* @__PURE__ */ jsx(Typography, {
|
|
49
|
+
variant: "caption",
|
|
50
|
+
className: "font-mono text-[12px] whitespace-pre-wrap break-words",
|
|
51
|
+
children: manual.error
|
|
52
|
+
})]
|
|
53
|
+
}))]
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The scheduled backup's own state.
|
|
58
|
+
*
|
|
59
|
+
* A failed last run is an error, not a footnote: a backup that fails every
|
|
60
|
+
* night used to leave this panel empty under "wait for the next scheduled run",
|
|
61
|
+
* with the reason only in the cron history.
|
|
62
|
+
*/
|
|
63
|
+
function ScheduledBackupStatus({ schedule }) {
|
|
64
|
+
const { t } = useTranslation();
|
|
65
|
+
const last = schedule.lastRun;
|
|
66
|
+
if (schedule.refused) return /* @__PURE__ */ jsxs(Alert, {
|
|
67
|
+
color: "error",
|
|
68
|
+
children: [/* @__PURE__ */ jsx(Typography, {
|
|
69
|
+
variant: "body2",
|
|
70
|
+
className: "text-[13px] font-semibold",
|
|
71
|
+
children: t("studio_backups_job_refused", { job: schedule.jobId })
|
|
72
|
+
}), /* @__PURE__ */ jsxs(Typography, {
|
|
73
|
+
variant: "caption",
|
|
74
|
+
className: "font-mono text-[12px] whitespace-pre-wrap break-words",
|
|
75
|
+
children: [
|
|
76
|
+
schedule.schedule,
|
|
77
|
+
" — ",
|
|
78
|
+
schedule.refused
|
|
79
|
+
]
|
|
80
|
+
})]
|
|
81
|
+
});
|
|
82
|
+
if (last && !last.success) return /* @__PURE__ */ jsxs(Alert, {
|
|
83
|
+
color: "error",
|
|
84
|
+
children: [/* @__PURE__ */ jsx(Typography, {
|
|
85
|
+
variant: "body2",
|
|
86
|
+
className: "text-[13px] font-semibold",
|
|
87
|
+
children: t("studio_backups_last_run_failed", { when: formatDate(last.startedAt) })
|
|
88
|
+
}), last.error && /* @__PURE__ */ jsx(Typography, {
|
|
89
|
+
variant: "caption",
|
|
90
|
+
className: "font-mono text-[12px] whitespace-pre-wrap break-words",
|
|
91
|
+
children: last.error
|
|
92
|
+
})]
|
|
93
|
+
});
|
|
94
|
+
if (!schedule.enabled) return /* @__PURE__ */ jsx(Alert, {
|
|
95
|
+
color: "warning",
|
|
96
|
+
children: /* @__PURE__ */ jsx(Typography, {
|
|
97
|
+
variant: "body2",
|
|
98
|
+
className: "text-[13px]",
|
|
99
|
+
children: schedule.disabledInCode ? t("studio_backups_schedule_off_in_code", { job: schedule.jobId }) : t("studio_backups_schedule_paused", { job: schedule.jobId })
|
|
100
|
+
})
|
|
101
|
+
});
|
|
102
|
+
if (schedule.historyError) return /* @__PURE__ */ jsx(Alert, {
|
|
103
|
+
color: "warning",
|
|
104
|
+
children: /* @__PURE__ */ jsx(Typography, {
|
|
105
|
+
variant: "body2",
|
|
106
|
+
className: "text-[13px]",
|
|
107
|
+
children: t("studio_backups_history_unreadable")
|
|
108
|
+
})
|
|
109
|
+
});
|
|
110
|
+
return /* @__PURE__ */ jsx(Typography, {
|
|
111
|
+
variant: "caption",
|
|
112
|
+
color: "secondary",
|
|
113
|
+
className: "text-[12px]",
|
|
114
|
+
children: [last ? t("studio_backups_last_run_ok", { when: formatDate(last.startedAt) }) : t("studio_backups_never_ran"), schedule.nextRunAt ? t("studio_backups_next_run", { when: formatDate(schedule.nextRunAt) }) : null].filter((part) => Boolean(part)).join(" · ")
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function BackupsView() {
|
|
118
|
+
const client = useRebaseClient();
|
|
119
|
+
const snackbar = useSnackbarController();
|
|
120
|
+
const { t } = useTranslation();
|
|
121
|
+
const [backups, setBackups] = useState([]);
|
|
122
|
+
const [destinationKind, setDestinationKind] = useState("local");
|
|
123
|
+
const [configured, setConfigured] = useState(true);
|
|
124
|
+
const [localDiskOfThisProcess, setLocalDiskOfThisProcess] = useState(false);
|
|
125
|
+
const [schedule, setSchedule] = useState(null);
|
|
126
|
+
const [loading, setLoading] = useState(true);
|
|
127
|
+
const [downloading, setDownloading] = useState(null);
|
|
128
|
+
/** Why the listing failed, classified — see `load-failure.ts`. */
|
|
129
|
+
const [failure, setFailure] = useState(null);
|
|
130
|
+
const clientRef = useRef(client);
|
|
131
|
+
clientRef.current = client;
|
|
132
|
+
const snackbarRef = useRef(snackbar);
|
|
133
|
+
snackbarRef.current = snackbar;
|
|
134
|
+
const load = useCallback(async () => {
|
|
135
|
+
const c = clientRef.current;
|
|
136
|
+
if (!c?.backups) {
|
|
137
|
+
setLoading(false);
|
|
138
|
+
setConfigured(false);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
setFailure(null);
|
|
142
|
+
try {
|
|
143
|
+
const res = await c.backups.list();
|
|
144
|
+
setBackups(res.backups);
|
|
145
|
+
setDestinationKind(res.destinationKind);
|
|
146
|
+
setConfigured(res.configured);
|
|
147
|
+
setLocalDiskOfThisProcess(res.localDiskOfThisProcess === true);
|
|
148
|
+
setSchedule(res.schedule ?? null);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
setFailure(classifyLoadFailure(e));
|
|
151
|
+
} finally {
|
|
152
|
+
setLoading(false);
|
|
153
|
+
}
|
|
154
|
+
}, []);
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
load();
|
|
157
|
+
}, [load]);
|
|
158
|
+
const handleDownload = async (key, fileName) => {
|
|
159
|
+
const c = clientRef.current;
|
|
160
|
+
if (!c?.backups) return;
|
|
161
|
+
setDownloading(key);
|
|
162
|
+
try {
|
|
163
|
+
const blob = await c.backups.download(key);
|
|
164
|
+
const url = URL.createObjectURL(blob);
|
|
165
|
+
const a = document.createElement("a");
|
|
166
|
+
a.href = url;
|
|
167
|
+
a.download = fileName;
|
|
168
|
+
document.body.appendChild(a);
|
|
169
|
+
a.click();
|
|
170
|
+
a.remove();
|
|
171
|
+
URL.revokeObjectURL(url);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
snackbarRef.current.open({
|
|
174
|
+
type: "error",
|
|
175
|
+
message: e instanceof Error ? e.message : String(e)
|
|
176
|
+
});
|
|
177
|
+
} finally {
|
|
178
|
+
setDownloading(null);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
if (!client?.backups) return /* @__PURE__ */ jsxs("div", {
|
|
182
|
+
className: "flex flex-col items-center justify-center h-full gap-4 text-center p-8",
|
|
183
|
+
children: [
|
|
184
|
+
/* @__PURE__ */ jsx(DatabaseIcon, {
|
|
185
|
+
size: iconSize.large,
|
|
186
|
+
className: "text-surface-300 dark:text-surface-600"
|
|
187
|
+
}),
|
|
188
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
189
|
+
variant: "h6",
|
|
190
|
+
color: "secondary",
|
|
191
|
+
children: t("studio_backups_unavailable_title")
|
|
192
|
+
}),
|
|
193
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
194
|
+
variant: "body2",
|
|
195
|
+
color: "disabled",
|
|
196
|
+
className: "max-w-md",
|
|
197
|
+
children: t("studio_backups_unavailable_body")
|
|
198
|
+
})
|
|
199
|
+
]
|
|
200
|
+
});
|
|
201
|
+
if (loading) return /* @__PURE__ */ jsx("div", {
|
|
202
|
+
className: "flex items-center justify-center h-full",
|
|
203
|
+
children: /* @__PURE__ */ jsx(CircularProgress, {})
|
|
204
|
+
});
|
|
205
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
206
|
+
className: "flex flex-col h-full w-full overflow-hidden bg-surface-card",
|
|
207
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
208
|
+
className: cls("flex items-center justify-between px-5 py-2.5 border-b bg-surface-sheet min-h-[48px]", defaultBorderMixin),
|
|
209
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
210
|
+
className: "flex items-center gap-2",
|
|
211
|
+
children: [
|
|
212
|
+
/* @__PURE__ */ jsx(DatabaseIcon, {
|
|
213
|
+
size: iconSize.small,
|
|
214
|
+
className: "text-primary"
|
|
215
|
+
}),
|
|
216
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
217
|
+
variant: "subtitle2",
|
|
218
|
+
className: "font-semibold",
|
|
219
|
+
children: t("studio_tool_backups")
|
|
220
|
+
}),
|
|
221
|
+
/* @__PURE__ */ jsx(Chip, {
|
|
222
|
+
size: "smallest",
|
|
223
|
+
className: "bg-surface-raised text-surface-600 dark:text-surface-300",
|
|
224
|
+
children: backups.length
|
|
225
|
+
}),
|
|
226
|
+
/* @__PURE__ */ jsx(Chip, {
|
|
227
|
+
size: "smallest",
|
|
228
|
+
className: "bg-surface-raised text-surface-500 dark:text-surface-400 uppercase font-mono text-[10px]",
|
|
229
|
+
children: destinationKind
|
|
230
|
+
})
|
|
231
|
+
]
|
|
232
|
+
}), /* @__PURE__ */ jsx(IconButton, {
|
|
233
|
+
size: "small",
|
|
234
|
+
onClick: load,
|
|
235
|
+
title: t("refresh_data"),
|
|
236
|
+
children: /* @__PURE__ */ jsx(RefreshCwIcon, { size: iconSize.smallest })
|
|
237
|
+
})]
|
|
238
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
239
|
+
className: "flex-1 overflow-y-auto p-5",
|
|
240
|
+
children: failure ? /* @__PURE__ */ jsx(LoadFailureView, {
|
|
241
|
+
failure,
|
|
242
|
+
title: t("studio_backups_read_failed"),
|
|
243
|
+
deniedTitle: t("studio_backups_denied_title"),
|
|
244
|
+
deniedHint: t("studio_backups_denied_hint"),
|
|
245
|
+
onRetry: load
|
|
246
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
247
|
+
className: "flex flex-col gap-4 min-h-full",
|
|
248
|
+
children: [
|
|
249
|
+
schedule && /* @__PURE__ */ jsx("div", {
|
|
250
|
+
className: "max-w-3xl",
|
|
251
|
+
children: /* @__PURE__ */ jsx(BackupScheduleSummary, { schedule })
|
|
252
|
+
}),
|
|
253
|
+
configured && localDiskOfThisProcess && /* @__PURE__ */ jsx("div", {
|
|
254
|
+
className: "max-w-3xl",
|
|
255
|
+
children: /* @__PURE__ */ jsx(Alert, {
|
|
256
|
+
color: "info",
|
|
257
|
+
children: /* @__PURE__ */ jsx(Typography, {
|
|
258
|
+
variant: "body2",
|
|
259
|
+
className: "text-[13px]",
|
|
260
|
+
children: t("studio_backups_local_disk_of_this_process")
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
}),
|
|
264
|
+
!configured ? /* @__PURE__ */ jsx(Alert, {
|
|
265
|
+
color: "info",
|
|
266
|
+
children: /* @__PURE__ */ jsxs(Typography, {
|
|
267
|
+
variant: "body2",
|
|
268
|
+
className: "text-[13px]",
|
|
269
|
+
children: [
|
|
270
|
+
/* @__PURE__ */ jsx("strong", { children: t("studio_backups_not_configured_title") }),
|
|
271
|
+
" ",
|
|
272
|
+
t("studio_backups_not_configured_body"),
|
|
273
|
+
" ",
|
|
274
|
+
/* @__PURE__ */ jsx("a", {
|
|
275
|
+
href: "https://rebase.pro/docs/backend/jobs",
|
|
276
|
+
target: "_blank",
|
|
277
|
+
rel: "noreferrer",
|
|
278
|
+
className: "text-primary underline",
|
|
279
|
+
children: t("studio_read_the_docs")
|
|
280
|
+
})
|
|
281
|
+
]
|
|
282
|
+
})
|
|
283
|
+
}) : backups.length === 0 ? /* @__PURE__ */ jsxs("div", {
|
|
284
|
+
className: "flex flex-1 flex-col items-center justify-center gap-3 text-center",
|
|
285
|
+
children: [
|
|
286
|
+
/* @__PURE__ */ jsx(DatabaseIcon, {
|
|
287
|
+
size: iconSize.large,
|
|
288
|
+
className: "text-surface-200 dark:text-surface-700"
|
|
289
|
+
}),
|
|
290
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
291
|
+
variant: "body2",
|
|
292
|
+
color: "disabled",
|
|
293
|
+
children: t("studio_backups_empty")
|
|
294
|
+
}),
|
|
295
|
+
!schedule && /* @__PURE__ */ jsx(Typography, {
|
|
296
|
+
variant: "caption",
|
|
297
|
+
color: "disabled",
|
|
298
|
+
className: "max-w-md",
|
|
299
|
+
children: t("studio_backups_empty_hint")
|
|
300
|
+
})
|
|
301
|
+
]
|
|
302
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
303
|
+
className: "space-y-2 max-w-3xl",
|
|
304
|
+
children: backups.map((backup) => {
|
|
305
|
+
const globalsKey = backup.globalsKey;
|
|
306
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
307
|
+
className: cls("flex items-center gap-3 px-4 py-3 rounded-lg border bg-surface-card", defaultBorderMixin),
|
|
308
|
+
children: [
|
|
309
|
+
/* @__PURE__ */ jsx(DatabaseIcon, {
|
|
310
|
+
size: iconSize.small,
|
|
311
|
+
className: "text-surface-400 shrink-0"
|
|
312
|
+
}),
|
|
313
|
+
/* @__PURE__ */ jsxs("div", {
|
|
314
|
+
className: "flex-1 min-w-0",
|
|
315
|
+
children: [/* @__PURE__ */ jsx(Typography, {
|
|
316
|
+
variant: "body2",
|
|
317
|
+
className: "truncate font-medium font-mono text-[12px]",
|
|
318
|
+
children: backup.name
|
|
319
|
+
}), /* @__PURE__ */ jsxs(Typography, {
|
|
320
|
+
variant: "caption",
|
|
321
|
+
color: "secondary",
|
|
322
|
+
className: "text-[11px]",
|
|
323
|
+
children: [
|
|
324
|
+
formatDate(backup.createdAt),
|
|
325
|
+
" · ",
|
|
326
|
+
formatSize(backup.sizeBytes)
|
|
327
|
+
]
|
|
328
|
+
})]
|
|
329
|
+
}),
|
|
330
|
+
globalsKey ? /* @__PURE__ */ jsx(Tooltip, {
|
|
331
|
+
title: t("studio_backups_roles_file_hint"),
|
|
332
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
333
|
+
size: "small",
|
|
334
|
+
variant: "text",
|
|
335
|
+
onClick: () => handleDownload(globalsKey, rolesFileName(backup.name)),
|
|
336
|
+
disabled: downloading === globalsKey,
|
|
337
|
+
startIcon: downloading === globalsKey ? /* @__PURE__ */ jsx(CircularProgress, { size: "smallest" }) : /* @__PURE__ */ jsx(DownloadIcon, { size: iconSize.smallest }),
|
|
338
|
+
children: downloading === globalsKey ? t("studio_backups_downloading") : t("studio_backups_roles_file")
|
|
339
|
+
})
|
|
340
|
+
}) : /* @__PURE__ */ jsx(Tooltip, {
|
|
341
|
+
title: t("studio_backups_no_roles_file_hint"),
|
|
342
|
+
children: /* @__PURE__ */ jsx(Chip, {
|
|
343
|
+
size: "smallest",
|
|
344
|
+
colorScheme: "orangeDarker",
|
|
345
|
+
children: t("studio_backups_no_roles_file")
|
|
346
|
+
})
|
|
347
|
+
}),
|
|
348
|
+
/* @__PURE__ */ jsx(Button, {
|
|
349
|
+
size: "small",
|
|
350
|
+
variant: "outlined",
|
|
351
|
+
onClick: () => handleDownload(backup.key, backup.name),
|
|
352
|
+
disabled: downloading === backup.key,
|
|
353
|
+
startIcon: downloading === backup.key ? /* @__PURE__ */ jsx(CircularProgress, { size: "smallest" }) : /* @__PURE__ */ jsx(DownloadIcon, { size: iconSize.smallest }),
|
|
354
|
+
children: downloading === backup.key ? t("studio_backups_downloading") : t("download")
|
|
355
|
+
})
|
|
356
|
+
]
|
|
357
|
+
}, backup.key);
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
]
|
|
361
|
+
})
|
|
362
|
+
})]
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
//#endregion
|
|
366
|
+
export { BackupsView };
|
|
367
|
+
|
|
368
|
+
//# sourceMappingURL=BackupsView-DmSMzAbT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BackupsView-DmSMzAbT.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, BackupScheduleStatus, 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\n/**\n * How the scheduled backup is doing, above the list — and, apart from it, a run\n * someone started by hand since, which is not the schedule's.\n */\nfunction BackupScheduleSummary({ schedule }: { schedule: BackupScheduleStatus }) {\n const { t } = useTranslation();\n const manual = schedule.lastManualRun;\n return (\n <div className=\"flex flex-col gap-2\">\n <ScheduledBackupStatus schedule={schedule}/>\n {manual && (manual.success ? (\n <Typography variant=\"caption\" color=\"secondary\" className=\"text-[12px]\">\n {t(\"studio_backups_manual_run_ok\", { when: formatDate(manual.startedAt) })}\n </Typography>\n ) : (\n <Alert color=\"warning\">\n <Typography variant=\"body2\" className=\"text-[13px] font-semibold\">\n {t(\"studio_backups_manual_run_failed\", { when: formatDate(manual.startedAt) })}\n </Typography>\n {manual.error && (\n <Typography variant=\"caption\" className=\"font-mono text-[12px] whitespace-pre-wrap break-words\">\n {manual.error}\n </Typography>\n )}\n </Alert>\n ))}\n </div>\n );\n}\n\n/**\n * The scheduled backup's own state.\n *\n * A failed last run is an error, not a footnote: a backup that fails every\n * night used to leave this panel empty under \"wait for the next scheduled run\",\n * with the reason only in the cron history.\n */\nfunction ScheduledBackupStatus({ schedule }: { schedule: BackupScheduleStatus }) {\n const { t } = useTranslation();\n const last = schedule.lastRun;\n\n // Refused at boot: it never runs, and resuming it in Cron Jobs is not\n // possible — it is not a job there. Only its cron file can fix it.\n if (schedule.refused) {\n return (\n <Alert color=\"error\">\n <Typography variant=\"body2\" className=\"text-[13px] font-semibold\">\n {t(\"studio_backups_job_refused\", { job: schedule.jobId })}\n </Typography>\n <Typography variant=\"caption\" className=\"font-mono text-[12px] whitespace-pre-wrap break-words\">\n {schedule.schedule} — {schedule.refused}\n </Typography>\n </Alert>\n );\n }\n\n if (last && !last.success) {\n return (\n <Alert color=\"error\">\n <Typography variant=\"body2\" className=\"text-[13px] font-semibold\">\n {t(\"studio_backups_last_run_failed\", { when: formatDate(last.startedAt) })}\n </Typography>\n {last.error && (\n <Typography variant=\"caption\" className=\"font-mono text-[12px] whitespace-pre-wrap break-words\">\n {last.error}\n </Typography>\n )}\n </Alert>\n );\n }\n\n if (!schedule.enabled) {\n // Declared off is not paused: \"resume it in Cron Jobs\" would turn on\n // the placeholder the documented cron file exports while\n // BACKUP_SCHEDULE is unset.\n return (\n <Alert color=\"warning\">\n <Typography variant=\"body2\" className=\"text-[13px]\">\n {schedule.disabledInCode\n ? t(\"studio_backups_schedule_off_in_code\", { job: schedule.jobId })\n : t(\"studio_backups_schedule_paused\", { job: schedule.jobId })}\n </Typography>\n </Alert>\n );\n }\n\n // Unknown is not \"has not run yet\": that line would be read off an empty\n // history the server could not fetch.\n if (schedule.historyError) {\n return (\n <Alert color=\"warning\">\n <Typography variant=\"body2\" className=\"text-[13px]\">\n {t(\"studio_backups_history_unreadable\")}\n </Typography>\n </Alert>\n );\n }\n\n const parts = [\n last ? t(\"studio_backups_last_run_ok\", { when: formatDate(last.startedAt) }) : t(\"studio_backups_never_ran\"),\n schedule.nextRunAt ? t(\"studio_backups_next_run\", { when: formatDate(schedule.nextRunAt) }) : null\n ].filter((part): part is string => Boolean(part));\n\n return (\n <Typography variant=\"caption\" color=\"secondary\" className=\"text-[12px]\">\n {parts.join(\" · \")}\n </Typography>\n );\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 [localDiskOfThisProcess, setLocalDiskOfThisProcess] = useState(false);\n const [schedule, setSchedule] = useState<BackupScheduleStatus | null>(null);\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 setLocalDiskOfThisProcess(res.localDiskOfThisProcess === true);\n // Absent from a server that predates the field.\n setSchedule(res.schedule ?? null);\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\">{t(\"studio_backups_unavailable_title\")}</Typography>\n <Typography variant=\"body2\" color=\"disabled\" className=\"max-w-md\">\n {t(\"studio_backups_unavailable_body\")}\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={t(\"refresh_data\")}>\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 ) : (\n <div className=\"flex flex-col gap-4 min-h-full\">\n {schedule && (\n <div className=\"max-w-3xl\">\n <BackupScheduleSummary schedule={schedule}/>\n </div>\n )}\n {/* The api process of a split deployment lists its own disk; the worker writes elsewhere. */}\n {configured && localDiskOfThisProcess && (\n <div className=\"max-w-3xl\">\n <Alert color=\"info\">\n <Typography variant=\"body2\" className=\"text-[13px]\">\n {t(\"studio_backups_local_disk_of_this_process\")}\n </Typography>\n </Alert>\n </div>\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-1 flex-col items-center justify-center gap-3 text-center\">\n <DatabaseIcon size={iconSize.large} className=\"text-surface-200 dark:text-surface-700\"/>\n <Typography variant=\"body2\" color=\"disabled\">\n {t(\"studio_backups_empty\")}\n </Typography>\n {/* With a schedule, the summary above already says when the next one runs, or why the last one did not. */}\n {!schedule && (\n <Typography variant=\"caption\" color=\"disabled\" className=\"max-w-md\">\n {t(\"studio_backups_empty_hint\")}\n </Typography>\n )}\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 )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as classifyLoadFailure } from "./load-failure-
|
|
1
|
+
import { t as classifyLoadFailure } from "./load-failure-Bd8lFIO7.js";
|
|
2
2
|
import { t as LoadFailureView } from "./load-failure-view-CBTB_h-h.js";
|
|
3
3
|
import { ConfirmationDialog, useRebaseContext, useSnackbarController, useTranslation } from "@rebasepro/app";
|
|
4
4
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
@@ -39,7 +39,11 @@ function BranchesView() {
|
|
|
39
39
|
const [selectedBranch, setSelectedBranch] = useState(null);
|
|
40
40
|
const [createOpen, setCreateOpen] = useState(false);
|
|
41
41
|
const [newBranchName, setNewBranchName] = useState("");
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The database to copy, when it is not the main one: a branch's own
|
|
44
|
+
* database, which is not its name.
|
|
45
|
+
*/
|
|
46
|
+
const [sourceDatabase, setSourceDatabase] = useState(void 0);
|
|
43
47
|
const [creating, setCreating] = useState(false);
|
|
44
48
|
/**
|
|
45
49
|
* Why the last create attempt was refused, kept on screen.
|
|
@@ -81,14 +85,14 @@ function BranchesView() {
|
|
|
81
85
|
setCreating(true);
|
|
82
86
|
setCreateError(null);
|
|
83
87
|
try {
|
|
84
|
-
await branchAdmin.createBranch(newBranchName.trim(),
|
|
88
|
+
await branchAdmin.createBranch(newBranchName.trim(), sourceDatabase ? { source: sourceDatabase } : void 0);
|
|
85
89
|
snackbarRef.current.open({
|
|
86
90
|
type: "success",
|
|
87
91
|
message: `Branch "${newBranchName.trim()}" created successfully`
|
|
88
92
|
});
|
|
89
93
|
setCreateOpen(false);
|
|
90
94
|
setNewBranchName("");
|
|
91
|
-
|
|
95
|
+
setSourceDatabase(void 0);
|
|
92
96
|
await loadBranches();
|
|
93
97
|
} catch (e) {
|
|
94
98
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -451,15 +455,15 @@ function BranchesView() {
|
|
|
451
455
|
}) }),
|
|
452
456
|
branches.length > 0 && /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs(Select, {
|
|
453
457
|
label: "Source Database",
|
|
454
|
-
value:
|
|
455
|
-
onValueChange: (v) =>
|
|
458
|
+
value: sourceDatabase ?? "__main__",
|
|
459
|
+
onValueChange: (v) => setSourceDatabase(v === "__main__" ? void 0 : v),
|
|
456
460
|
placeholder: "Default (main database)",
|
|
457
461
|
size: "small",
|
|
458
462
|
children: [/* @__PURE__ */ jsx(SelectItem, {
|
|
459
463
|
value: "__main__",
|
|
460
464
|
children: "Default (main database)"
|
|
461
465
|
}), branches.map((b) => /* @__PURE__ */ jsx(SelectItem, {
|
|
462
|
-
value: b.
|
|
466
|
+
value: b.database,
|
|
463
467
|
children: b.name
|
|
464
468
|
}, b.name))]
|
|
465
469
|
}) })
|
|
@@ -515,4 +519,4 @@ function StatCard({ label, value, mono }) {
|
|
|
515
519
|
//#endregion
|
|
516
520
|
export { BranchesView };
|
|
517
521
|
|
|
518
|
-
//# sourceMappingURL=BranchesView-
|
|
522
|
+
//# sourceMappingURL=BranchesView-DP1a87E-.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BranchesView-DP1a87E-.js","names":[],"sources":["../src/components/Branches/BranchesView.tsx"],"sourcesContent":["\nimport React, { useState, useEffect, useRef, useCallback } from \"react\";\nimport {\n Alert,\n Button,\n Chip,\n CircularProgress,\n cls,\n CopyIcon,\n defaultBorderMixin,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n GitBranchIcon,\n IconButton,\n iconSize,\n Paper,\n PlusIcon,\n RefreshCwIcon,\n Select,\n SelectItem,\n TextField,\n Trash2Icon,\n Typography\n} from \"@rebasepro/ui\";\nimport { useRebaseContext, useSnackbarController, ConfirmationDialog, useTranslation } from \"@rebasepro/app\";\nimport { isBranchAdmin } from \"@rebasepro/types\";\nimport type { BranchInfo } from \"@rebasepro/types\";\nimport { formatRelativeTime } from \"@rebasepro/utils\";\n\nimport { classifyLoadFailure, type LoadFailure } from \"../load-failure\";\n\n/**\n * The prefix the driver puts on every branch database.\n *\n * Stated here rather than derived from the row, because `BranchInfo` carries\n * the *name* a person typed and not the database it became — and the detail\n * pane used to print the name under \"Database:\", which is a connection string\n * to nothing. Kept in step with `BranchService.BRANCH_DB_PREFIX` and the CLI's\n * `branch-pointer.ts`, which say the same thing for the same reason.\n */\nconst BRANCH_DB_PREFIX = \"rb_\";\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 formatRelative(date: Date | string | undefined): string {\n if (!date) return \"—\";\n const d = date instanceof Date ? date : new Date(date);\n if (isNaN(d.getTime())) return \"—\";\n return formatRelativeTime(d) ?? d.toLocaleDateString();\n}\n\nexport function BranchesView() {\n const { databaseAdmin } = useRebaseContext();\n const snackbar = useSnackbarController();\n const { t } = useTranslation();\n\n const [branches, setBranches] = useState<BranchInfo[]>([]);\n const [loading, setLoading] = useState(true);\n const [selectedBranch, setSelectedBranch] = useState<string | null>(null);\n\n // Create dialog\n const [createOpen, setCreateOpen] = useState(false);\n const [newBranchName, setNewBranchName] = useState(\"\");\n /**\n * The database to copy, when it is not the main one: a branch's own\n * database, which is not its name.\n */\n const [sourceDatabase, setSourceDatabase] = useState<string | undefined>(undefined);\n const [creating, setCreating] = useState(false);\n /**\n * Why the last create attempt was refused, kept on screen.\n *\n * The server refuses branching outright on the managed development\n * database — PGlite serves one database, so the \"branch\" would be the\n * parent. That refusal is the whole answer to what the reader just tried to\n * do, and a snackbar is gone in four seconds; the same argument\n * `load-failure.ts` makes for listings.\n */\n const [createError, setCreateError] = useState<string | null>(null);\n\n // Delete confirm\n const [deleteTarget, setDeleteTarget] = useState<string | null>(null);\n const [deleting, setDeleting] = useState(false);\n\n /** Why the branch listing failed, classified — see `load-failure.ts`. */\n const [failure, setFailure] = useState<LoadFailure | null>(null);\n\n // Refs\n const snackbarRef = useRef(snackbar);\n snackbarRef.current = snackbar;\n\n const branchAdmin = isBranchAdmin(databaseAdmin) ? databaseAdmin : undefined;\n\n const loadBranches = useCallback(async () => {\n if (!branchAdmin) {\n setLoading(false);\n return;\n }\n try {\n const result = await branchAdmin.listBranches();\n setBranches(result);\n setFailure(null);\n } catch (e: unknown) {\n // \"No branches yet. Create one…\" over a refused listing invites the\n // reader to create a branch they are not allowed to see, and then\n // hides the one they already have.\n setFailure(classifyLoadFailure(e));\n } finally {\n setLoading(false);\n }\n }, [branchAdmin]);\n\n useEffect(() => {\n loadBranches();\n }, [loadBranches]);\n\n const handleCreate = async () => {\n if (!branchAdmin || !newBranchName.trim()) return;\n setCreating(true);\n setCreateError(null);\n try {\n await branchAdmin.createBranch(newBranchName.trim(), sourceDatabase ? { source: sourceDatabase } : undefined);\n snackbarRef.current.open({\n type: \"success\",\n message: `Branch \"${newBranchName.trim()}\" created successfully`\n });\n setCreateOpen(false);\n setNewBranchName(\"\");\n setSourceDatabase(undefined);\n await loadBranches();\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n setCreateError(message);\n snackbarRef.current.open({ type: \"error\",\n message });\n } finally {\n setCreating(false);\n }\n };\n\n const handleDelete = async () => {\n if (!branchAdmin || !deleteTarget) return;\n setDeleting(true);\n try {\n await branchAdmin.deleteBranch(deleteTarget);\n snackbarRef.current.open({\n type: \"success\",\n message: `Branch \"${deleteTarget}\" deleted`\n });\n if (selectedBranch === deleteTarget) setSelectedBranch(null);\n setDeleteTarget(null);\n await loadBranches();\n } catch (e: unknown) {\n snackbarRef.current.open({\n type: \"error\",\n message: e instanceof Error ? e.message : String(e)\n });\n } finally {\n setDeleting(false);\n }\n };\n\n // Not supported\n if (!branchAdmin) {\n return (\n <div className=\"flex flex-col items-center justify-center h-full gap-4 text-center p-8\">\n <GitBranchIcon size={iconSize.large} className=\"text-surface-300 dark:text-surface-600\"/>\n <Typography variant=\"h6\" color=\"secondary\">{t(\"studio_branches_unavailable_title\")}</Typography>\n <Typography variant=\"body2\" color=\"disabled\" className=\"max-w-md\">\n {t(\"studio_branches_unavailable_body\")}\n </Typography>\n <a\n href=\"https://rebase.pro/docs/backend/branching\"\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"text-primary text-sm underline\"\n >\n {t(\"studio_read_the_docs\")}\n </a>\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 const selected = branches.find(b => b.name === selectedBranch);\n\n return (\n <div className=\"flex h-full w-full overflow-hidden bg-surface-card\">\n {/* ── Branch List ── */}\n <div className={cls(\"flex flex-col w-[340px] min-w-[280px] border-r h-full\", defaultBorderMixin)}>\n <div className={cls(\"flex items-center justify-between px-4 py-2.5 border-b bg-surface-sheet min-h-[48px]\", defaultBorderMixin)}>\n <div className=\"flex items-center gap-2\">\n <GitBranchIcon size={iconSize.small} className=\"text-primary\"/>\n <Typography variant=\"subtitle2\" className=\"font-semibold\">{t(\"studio_tool_branches\")}</Typography>\n <Chip size=\"smallest\" className=\"bg-surface-raised text-surface-600 dark:text-surface-300\">{branches.length}</Chip>\n </div>\n <div className=\"flex items-center gap-1\">\n <IconButton size=\"small\" onClick={loadBranches} title=\"Refresh\">\n <RefreshCwIcon size={iconSize.smallest}/>\n </IconButton>\n <IconButton size=\"small\" onClick={() => setCreateOpen(true)} title=\"Create branch\" className=\"text-primary\">\n <PlusIcon size={iconSize.smallest}/>\n </IconButton>\n </div>\n </div>\n <div className=\"flex-1 overflow-y-auto p-2 space-y-1\">\n {failure ? (\n <LoadFailureView\n failure={failure}\n title={t(\"studio_branches_read_failed\")}\n deniedTitle={t(\"studio_branches_denied_title\")}\n deniedHint={t(\"studio_branches_denied_hint\")}\n onRetry={loadBranches}\n />\n ) : branches.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center h-full gap-3 text-center px-4\">\n <CopyIcon size={iconSize.small} className=\"text-surface-300 dark:text-surface-600\"/>\n <Typography variant=\"body2\" color=\"disabled\" className=\"text-[13px]\">\n No branches yet. Create one to start working with an isolated database copy.\n </Typography>\n <Button\n size=\"small\"\n variant=\"outlined\"\n onClick={() => setCreateOpen(true)}\n startIcon={<PlusIcon size={iconSize.smallest}/>}\n >\n Create Branch\n </Button>\n </div>\n ) : (\n branches.map(branch => (\n <div\n key={branch.name}\n onClick={() => setSelectedBranch(branch.name)}\n className={cls(\n \"flex items-center gap-3 px-3 py-2.5 rounded-lg cursor-pointer transition-all\",\n selectedBranch === branch.name\n ? \"bg-primary/10 dark:bg-primary/15 ring-1 ring-primary/30\"\n : \"hover:bg-surface-hover\"\n )}\n >\n <div className=\"w-2 h-2 rounded-full shrink-0 bg-emerald-500\"/>\n <div className=\"flex-1 min-w-0\">\n <Typography variant=\"body2\" className=\"truncate font-medium text-[13px]\">{branch.name}</Typography>\n <Typography variant=\"caption\" color=\"secondary\" className=\"truncate text-[11px]\">\n from {branch.parentDatabase} · {formatRelative(branch.createdAt)}\n </Typography>\n </div>\n {branch.sizeBytes !== undefined && (\n <Typography variant=\"caption\" color=\"disabled\" className=\"font-mono text-[10px] shrink-0\">\n {formatSize(branch.sizeBytes)}\n </Typography>\n )}\n </div>\n ))\n )}\n </div>\n </div>\n\n {/* ── Detail Panel ── */}\n <div className=\"flex-1 flex flex-col min-w-0 h-full overflow-hidden\">\n {!selected ? (\n <div className=\"flex flex-col items-center justify-center h-full gap-3\">\n <GitBranchIcon size={iconSize.large} className=\"text-surface-200 dark:text-surface-700\"/>\n <Typography variant=\"body2\" color=\"disabled\">\n {branches.length === 0 ? \"Create a branch to get started\" : \"Select a branch to view details\"}\n </Typography>\n </div>\n ) : (\n <>\n {/* Header */}\n <div className={cls(\"flex items-center justify-between px-5 py-3 border-b bg-surface-card min-h-[56px]\", defaultBorderMixin)}>\n <div className=\"flex items-center gap-3 min-w-0\">\n <div className=\"w-2.5 h-2.5 rounded-full bg-emerald-500\"/>\n <div className=\"min-w-0\">\n <Typography variant=\"subtitle1\" className=\"font-semibold truncate\">{selected.name}</Typography>\n <Typography variant=\"caption\" color=\"secondary\" className=\"truncate\">\n Created from <span className=\"font-mono text-[11px]\">{selected.parentDatabase}</span>\n </Typography>\n </div>\n </div>\n <div className=\"flex items-center gap-2 shrink-0\">\n <Button\n size=\"small\"\n color=\"error\"\n variant=\"outlined\"\n onClick={() => setDeleteTarget(selected.name)}\n startIcon={<Trash2Icon size={iconSize.smallest}/>}\n >\n Delete\n </Button>\n </div>\n </div>\n\n {/* Info Cards */}\n <div className=\"px-5 py-4 bg-surface-sheet\">\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-3\">\n <StatCard label=\"Branch Name\" value={selected.name} mono/>\n <StatCard label=\"Source Database\" value={selected.parentDatabase} mono/>\n <StatCard label=\"Created\" value={formatRelative(selected.createdAt)}/>\n <StatCard label=\"Size\" value={formatSize(selected.sizeBytes)}/>\n </div>\n </div>\n\n {/* Usage Info */}\n <div className=\"flex-1 overflow-y-auto px-5 py-4\">\n <Alert color=\"info\">\n <Typography variant=\"body2\" className=\"text-[13px]\">\n <strong>How to use this branch:</strong> point this checkout at it with\n <code className=\"mx-1 px-1.5 py-0.5 rounded bg-surface-well font-mono text-[12px]\">rebase db branch switch {selected.name}</code>\n — every later command uses it, and\n <code className=\"mx-1 px-1.5 py-0.5 rounded bg-surface-well font-mono text-[12px]\">rebase db branch switch --off</code>\n goes back to the main database. Changes made here don't affect it.\n </Typography>\n </Alert>\n <div className=\"mt-4 p-4 rounded-lg border bg-surface-sheet border-hairline-strong\">\n <Typography variant=\"caption\" className=\"text-[10px] uppercase tracking-wider text-surface-400 mb-2 block font-medium\">Connection Details</Typography>\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n {/* The *database*, not the branch name. They are not the\n same string — the driver prefixes every branch database\n with `rb_` — and a connection string built from the name\n on this pane pointed at a database that does not\n exist. */}\n <Typography variant=\"caption\" color=\"secondary\" className=\"w-24 shrink-0 text-[11px]\">Database:</Typography>\n <Typography variant=\"body2\" className=\"font-mono text-[12px]\">{`${BRANCH_DB_PREFIX}${selected.name}`}</Typography>\n </div>\n <div className=\"flex items-start gap-2\">\n <Typography variant=\"caption\" color=\"secondary\" className=\"w-24 shrink-0 text-[11px]\">Connect with:</Typography>\n <Typography variant=\"body2\" className=\"font-mono text-[12px] break-all\">\n {`DATABASE_URL=…/${BRANCH_DB_PREFIX}${selected.name}`}\n </Typography>\n </div>\n <div className=\"flex items-center gap-2\">\n <Typography variant=\"caption\" color=\"secondary\" className=\"w-24 shrink-0 text-[11px]\">Branched from:</Typography>\n <Typography variant=\"body2\" className=\"font-mono text-[12px]\">{selected.parentDatabase}</Typography>\n </div>\n <div className=\"flex items-center gap-2\">\n <Typography variant=\"caption\" color=\"secondary\" className=\"w-24 shrink-0 text-[11px]\">Created at:</Typography>\n <Typography variant=\"body2\" className=\"font-mono text-[12px]\">\n {selected.createdAt instanceof Date\n ? selected.createdAt.toLocaleString()\n : new Date(selected.createdAt).toLocaleString()}\n </Typography>\n </div>\n </div>\n </div>\n </div>\n </>\n )}\n </div>\n\n {/* ── Create Dialog ── */}\n <Dialog open={createOpen} onOpenChange={(open) => {\n setCreateOpen(open);\n if (!open) setCreateError(null);\n }}>\n <DialogTitle>Create New Branch</DialogTitle>\n <DialogContent className=\"space-y-4 min-w-[400px]\">\n <Typography variant=\"body2\" color=\"secondary\" className=\"text-[13px]\">\n Create an isolated database copy. The branch will be a full clone of the source database at this point in time.\n </Typography>\n {createError && (\n <Alert color=\"error\">\n <Typography variant=\"body2\" className=\"text-[13px]\">{createError}</Typography>\n </Alert>\n )}\n <div>\n <TextField\n label=\"Branch Name\"\n value={newBranchName}\n onChange={(e) => setNewBranchName(e.target.value)}\n placeholder=\"e.g. feature-auth, staging, preview-pr-42\"\n size=\"small\"\n autoFocus\n />\n </div>\n {branches.length > 0 && (\n <div>\n <Select\n label=\"Source Database\"\n value={sourceDatabase ?? \"__main__\"}\n onValueChange={(v) => setSourceDatabase(v === \"__main__\" ? undefined : v)}\n placeholder=\"Default (main database)\"\n size=\"small\"\n >\n <SelectItem value=\"__main__\">Default (main database)</SelectItem>\n {branches.map(b => (\n <SelectItem key={b.name} value={b.database}>{b.name}</SelectItem>\n ))}\n </Select>\n </div>\n )}\n </DialogContent>\n <DialogActions>\n <Button variant=\"text\" onClick={() => setCreateOpen(false)} disabled={creating}>\n Cancel\n </Button>\n <Button\n color=\"primary\"\n onClick={handleCreate}\n disabled={!newBranchName.trim() || creating}\n startIcon={creating ? <CircularProgress size=\"smallest\"/> : <PlusIcon size={iconSize.smallest}/>}\n >\n {creating ? \"Creating...\" : \"Create Branch\"}\n </Button>\n </DialogActions>\n </Dialog>\n\n {/* ── Delete Confirm ── */}\n <ConfirmationDialog\n open={!!deleteTarget}\n onAccept={handleDelete}\n onCancel={() => setDeleteTarget(null)}\n title=\"Delete Branch\"\n body={\n <Typography variant=\"body2\">\n Are you sure you want to permanently delete the branch <strong>{deleteTarget}</strong>?\n This action cannot be undone and all data in this branch will be lost.\n </Typography>\n }\n loading={deleting}\n />\n </div>\n );\n}\n\nfunction StatCard({ label, value, mono }: {\n label: string; value: string; mono?: boolean;\n}) {\n return (\n <div className={cls(\"px-3 py-2 rounded-lg border bg-surface-card\", defaultBorderMixin)}>\n <Typography variant=\"caption\" color=\"secondary\" className=\"text-[10px] uppercase tracking-wider font-medium\">{label}</Typography>\n <Typography variant=\"body2\" className={cls(\n \"mt-0.5 font-semibold text-[13px] truncate\",\n mono && \"font-mono\"\n )}>{value}</Typography>\n </div>\n );\n}\n"],"mappings":"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"}
|