@trieb.work/payload-plugin-backup-mongodb 0.1.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.
Files changed (165) hide show
  1. package/README.md +319 -0
  2. package/dist/backup-dashboard.css +1132 -0
  3. package/dist/client.d.ts +3 -0
  4. package/dist/client.js +3 -0
  5. package/dist/client.js.map +1 -0
  6. package/dist/collections/BackupSettings.d.ts +6 -0
  7. package/dist/collections/BackupSettings.js +91 -0
  8. package/dist/collections/BackupSettings.js.map +1 -0
  9. package/dist/collections/BackupTasks.d.ts +2 -0
  10. package/dist/collections/BackupTasks.js +56 -0
  11. package/dist/collections/BackupTasks.js.map +1 -0
  12. package/dist/components/BackupDashboard/BackupItemActions.client.d.ts +7 -0
  13. package/dist/components/BackupDashboard/BackupItemActions.client.js +90 -0
  14. package/dist/components/BackupDashboard/BackupItemActions.client.js.map +1 -0
  15. package/dist/components/BackupDashboard/BackupListCollapsible.client.d.ts +16 -0
  16. package/dist/components/BackupDashboard/BackupListCollapsible.client.js +597 -0
  17. package/dist/components/BackupDashboard/BackupListCollapsible.client.js.map +1 -0
  18. package/dist/components/BackupDashboard/BackupSettingsModal.client.d.ts +2 -0
  19. package/dist/components/BackupDashboard/BackupSettingsModal.client.js +939 -0
  20. package/dist/components/BackupDashboard/BackupSettingsModal.client.js.map +1 -0
  21. package/dist/components/BackupDashboard/CollectionBackupPreviewBody.client.d.ts +18 -0
  22. package/dist/components/BackupDashboard/CollectionBackupPreviewBody.client.js +267 -0
  23. package/dist/components/BackupDashboard/CollectionBackupPreviewBody.client.js.map +1 -0
  24. package/dist/components/BackupDashboard/ManualBackupDialog.client.d.ts +1 -0
  25. package/dist/components/BackupDashboard/ManualBackupDialog.client.js +276 -0
  26. package/dist/components/BackupDashboard/ManualBackupDialog.client.js.map +1 -0
  27. package/dist/components/BackupDashboard/RestoreBackupDialog.client.d.ts +6 -0
  28. package/dist/components/BackupDashboard/RestoreBackupDialog.client.js +551 -0
  29. package/dist/components/BackupDashboard/RestoreBackupDialog.client.js.map +1 -0
  30. package/dist/components/BackupDashboard/TaskActionButton.client.d.ts +29 -0
  31. package/dist/components/BackupDashboard/TaskActionButton.client.js +240 -0
  32. package/dist/components/BackupDashboard/TaskActionButton.client.js.map +1 -0
  33. package/dist/components/BackupDashboard/backupDashboardInlineCss.d.ts +1 -0
  34. package/dist/components/BackupDashboard/backupDashboardInlineCss.js +3 -0
  35. package/dist/components/BackupDashboard/backupDashboardInlineCss.js.map +1 -0
  36. package/dist/components/BackupDashboard/index.client.d.ts +8 -0
  37. package/dist/components/BackupDashboard/index.client.js +9 -0
  38. package/dist/components/BackupDashboard/index.client.js.map +1 -0
  39. package/dist/components/BackupDashboard/index.d.ts +8 -0
  40. package/dist/components/BackupDashboard/index.js +196 -0
  41. package/dist/components/BackupDashboard/index.js.map +1 -0
  42. package/dist/core/archive.d.ts +8 -0
  43. package/dist/core/archive.js +47 -0
  44. package/dist/core/archive.js.map +1 -0
  45. package/dist/core/backup.d.ts +41 -0
  46. package/dist/core/backup.js +188 -0
  47. package/dist/core/backup.js.map +1 -0
  48. package/dist/core/backupBlobIO.d.ts +54 -0
  49. package/dist/core/backupBlobIO.js +179 -0
  50. package/dist/core/backupBlobIO.js.map +1 -0
  51. package/dist/core/backupBlobTransfer.d.ts +29 -0
  52. package/dist/core/backupBlobTransfer.js +85 -0
  53. package/dist/core/backupBlobTransfer.js.map +1 -0
  54. package/dist/core/backupSettings.d.ts +35 -0
  55. package/dist/core/backupSettings.js +95 -0
  56. package/dist/core/backupSettings.js.map +1 -0
  57. package/dist/core/backupSourcePreview.d.ts +10 -0
  58. package/dist/core/backupSourcePreview.js +33 -0
  59. package/dist/core/backupSourcePreview.js.map +1 -0
  60. package/dist/core/blobTokenValidate.d.ts +14 -0
  61. package/dist/core/blobTokenValidate.js +75 -0
  62. package/dist/core/blobTokenValidate.js.map +1 -0
  63. package/dist/core/db.d.ts +24 -0
  64. package/dist/core/db.js +12 -0
  65. package/dist/core/db.js.map +1 -0
  66. package/dist/core/restore.d.ts +27 -0
  67. package/dist/core/restore.js +199 -0
  68. package/dist/core/restore.js.map +1 -0
  69. package/dist/core/restorePreview.d.ts +64 -0
  70. package/dist/core/restorePreview.js +266 -0
  71. package/dist/core/restorePreview.js.map +1 -0
  72. package/dist/core/taskProgress.d.ts +26 -0
  73. package/dist/core/taskProgress.js +73 -0
  74. package/dist/core/taskProgress.js.map +1 -0
  75. package/dist/core/vercelBackupCron.d.ts +8 -0
  76. package/dist/core/vercelBackupCron.js +38 -0
  77. package/dist/core/vercelBackupCron.js.map +1 -0
  78. package/dist/endpoints/createBackupEndpoints.d.ts +1 -0
  79. package/dist/endpoints/createBackupEndpoints.js +3 -0
  80. package/dist/endpoints/createBackupEndpoints.js.map +1 -0
  81. package/dist/endpoints/index.d.ts +3 -0
  82. package/dist/endpoints/index.js +34 -0
  83. package/dist/endpoints/index.js.map +1 -0
  84. package/dist/endpoints/paths/admin-backup-download.d.ts +8 -0
  85. package/dist/endpoints/paths/admin-backup-download.js +72 -0
  86. package/dist/endpoints/paths/admin-backup-download.js.map +1 -0
  87. package/dist/endpoints/paths/admin-delete.d.ts +3 -0
  88. package/dist/endpoints/paths/admin-delete.js +55 -0
  89. package/dist/endpoints/paths/admin-delete.js.map +1 -0
  90. package/dist/endpoints/paths/admin-manual.d.ts +3 -0
  91. package/dist/endpoints/paths/admin-manual.js +61 -0
  92. package/dist/endpoints/paths/admin-manual.js.map +1 -0
  93. package/dist/endpoints/paths/admin-preview.d.ts +6 -0
  94. package/dist/endpoints/paths/admin-preview.js +102 -0
  95. package/dist/endpoints/paths/admin-preview.js.map +1 -0
  96. package/dist/endpoints/paths/admin-restore.d.ts +3 -0
  97. package/dist/endpoints/paths/admin-restore.js +74 -0
  98. package/dist/endpoints/paths/admin-restore.js.map +1 -0
  99. package/dist/endpoints/paths/admin-seed.d.ts +3 -0
  100. package/dist/endpoints/paths/admin-seed.js +47 -0
  101. package/dist/endpoints/paths/admin-seed.js.map +1 -0
  102. package/dist/endpoints/paths/admin-settings.d.ts +3 -0
  103. package/dist/endpoints/paths/admin-settings.js +230 -0
  104. package/dist/endpoints/paths/admin-settings.js.map +1 -0
  105. package/dist/endpoints/paths/admin-task.d.ts +3 -0
  106. package/dist/endpoints/paths/admin-task.js +34 -0
  107. package/dist/endpoints/paths/admin-task.js.map +1 -0
  108. package/dist/endpoints/paths/admin-validate-blob-token.d.ts +8 -0
  109. package/dist/endpoints/paths/admin-validate-blob-token.js +26 -0
  110. package/dist/endpoints/paths/admin-validate-blob-token.js.map +1 -0
  111. package/dist/endpoints/paths/cron-list.d.ts +2 -0
  112. package/dist/endpoints/paths/cron-list.js +33 -0
  113. package/dist/endpoints/paths/cron-list.js.map +1 -0
  114. package/dist/endpoints/paths/cron-restore.d.ts +2 -0
  115. package/dist/endpoints/paths/cron-restore.js +61 -0
  116. package/dist/endpoints/paths/cron-restore.js.map +1 -0
  117. package/dist/endpoints/paths/cron-run.d.ts +3 -0
  118. package/dist/endpoints/paths/cron-run.js +45 -0
  119. package/dist/endpoints/paths/cron-run.js.map +1 -0
  120. package/dist/endpoints/shared.d.ts +13 -0
  121. package/dist/endpoints/shared.js +58 -0
  122. package/dist/endpoints/shared.js.map +1 -0
  123. package/dist/exports/client.d.ts +1 -0
  124. package/dist/exports/client.js +3 -0
  125. package/dist/exports/client.js.map +1 -0
  126. package/dist/exports/rsc.d.ts +1 -0
  127. package/dist/exports/rsc.js +3 -0
  128. package/dist/exports/rsc.js.map +1 -0
  129. package/dist/index.d.ts +18 -0
  130. package/dist/index.js +14 -0
  131. package/dist/index.js.map +1 -0
  132. package/dist/plugin.d.ts +3 -0
  133. package/dist/plugin.js +82 -0
  134. package/dist/plugin.js.map +1 -0
  135. package/dist/publicApiPaths.d.ts +19 -0
  136. package/dist/publicApiPaths.js +20 -0
  137. package/dist/publicApiPaths.js.map +1 -0
  138. package/dist/types.d.ts +21 -0
  139. package/dist/types.js +3 -0
  140. package/dist/types.js.map +1 -0
  141. package/dist/utils/backupSelection.d.ts +6 -0
  142. package/dist/utils/backupSelection.js +16 -0
  143. package/dist/utils/backupSelection.js.map +1 -0
  144. package/dist/utils/blobName.d.ts +32 -0
  145. package/dist/utils/blobName.js +99 -0
  146. package/dist/utils/blobName.js.map +1 -0
  147. package/dist/utils/dashboardRoleAccess.d.ts +23 -0
  148. package/dist/utils/dashboardRoleAccess.js +64 -0
  149. package/dist/utils/dashboardRoleAccess.js.map +1 -0
  150. package/dist/utils/dialogBackdrop.d.ts +9 -0
  151. package/dist/utils/dialogBackdrop.js +17 -0
  152. package/dist/utils/dialogBackdrop.js.map +1 -0
  153. package/dist/utils/formatBytes.d.ts +2 -0
  154. package/dist/utils/formatBytes.js +20 -0
  155. package/dist/utils/formatBytes.js.map +1 -0
  156. package/dist/utils/hostname.d.ts +2 -0
  157. package/dist/utils/hostname.js +17 -0
  158. package/dist/utils/hostname.js.map +1 -0
  159. package/dist/utils/index.d.ts +5 -0
  160. package/dist/utils/index.js +7 -0
  161. package/dist/utils/index.js.map +1 -0
  162. package/dist/utils/maskBlobToken.d.ts +11 -0
  163. package/dist/utils/maskBlobToken.js +42 -0
  164. package/dist/utils/maskBlobToken.js.map +1 -0
  165. package/package.json +140 -0
@@ -0,0 +1,939 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Button } from '@payloadcms/ui';
4
+ import { useRouter } from 'next/navigation';
5
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
6
+ import { backupPluginPublicApiPaths } from '../../publicApiPaths';
7
+ import { selectedFromSkipMongoNames, skipMongoNamesFromPreview } from '../../utils/backupSelection';
8
+ import { closeNativeDialogOnBackdropPointer } from '../../utils/dialogBackdrop';
9
+ import { CollectionBackupPreviewBody } from './CollectionBackupPreviewBody.client';
10
+ function parseBlobTransferTaskMessage(msg) {
11
+ try {
12
+ const o = JSON.parse(msg);
13
+ if (typeof o.total !== 'number' || typeof o.transferred !== 'number') {
14
+ return null;
15
+ }
16
+ return {
17
+ failed: typeof o.failed === 'number' ? o.failed : 0,
18
+ pathname: typeof o.pathname === 'string' ? o.pathname : undefined,
19
+ total: o.total,
20
+ transferred: o.transferred
21
+ };
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+ const TIP_SETTINGS_SUBTITLE = 'These settings apply to scheduled (cron) backups only. Manual backups use the choices in the manual backup dialog each time you create one.';
27
+ const TIP_TRANSFER_COPY = 'When enabled, objects under backups/ are copied from the previous backup token (or from BLOB_READ_WRITE_TOKEN on first setup) into the new store. When disabled, only the new token is saved — existing backups stay in the old storage and remain reachable with the previous token.';
28
+ const TIP_TRANSFER_DELETE = 'Destructive: when enabled, each blob is removed from the old store only after a successful copy to the new store. Leave disabled to keep a full copy in the previous storage as a safety net.';
29
+ function settingsHelpIconClassName(multiline) {
30
+ return [
31
+ 'backup-help-icon',
32
+ 'backup-help-icon--settings-tooltip',
33
+ multiline ? 'backup-help-icon--multiline' : ''
34
+ ].filter(Boolean).join(' ');
35
+ }
36
+ /** Info icon + CSS tooltip; fixed positioning so scroll/clipping in the dialog body does not hide it. */ const SettingsHelpTip = ({ multiline, tip })=>{
37
+ const elRef = useRef(null);
38
+ const syncTooltipAnchor = useCallback(()=>{
39
+ const el = elRef.current;
40
+ if (!el) {
41
+ return;
42
+ }
43
+ const r = el.getBoundingClientRect();
44
+ el.style.setProperty('--backup-tip-x', `${r.left + r.width / 2}px`);
45
+ el.style.setProperty('--backup-tip-y', `${r.top}px`);
46
+ }, []);
47
+ return /*#__PURE__*/ _jsx("button", {
48
+ "aria-label": tip,
49
+ className: settingsHelpIconClassName(multiline),
50
+ "data-tip": tip,
51
+ onFocus: syncTooltipAnchor,
52
+ onMouseEnter: syncTooltipAnchor,
53
+ ref: elRef,
54
+ title: tip,
55
+ type: "button",
56
+ children: "i"
57
+ });
58
+ };
59
+ export const BackupSettingsModal = ()=>{
60
+ const router = useRouter();
61
+ const transferOptId = useId();
62
+ const deleteSourceOptId = useId();
63
+ const dialogRef = useRef(null);
64
+ const isPreparingOpenRef = useRef(false);
65
+ const [preview, setPreview] = useState(null);
66
+ const [phase, setPhase] = useState('idle');
67
+ const [errorMessage, setErrorMessage] = useState(null);
68
+ const [selected, setSelected] = useState({});
69
+ const [backupAllCollections, setBackupAllCollections] = useState(false);
70
+ const [includeMediaBlobs, setIncludeMediaBlobs] = useState(true);
71
+ const [backupsToKeep, setBackupsToKeep] = useState(10);
72
+ const [backupBlobReadWriteToken, setBackupBlobReadWriteToken] = useState('');
73
+ /**
74
+ * When saving a new raw token, copy backup blobs from the old store. Default on so rotating a
75
+ * token automatically migrates existing archives; delete-from-source stays opt-in.
76
+ */ const [transferBackupBlobs, setTransferBackupBlobs] = useState(true);
77
+ /**
78
+ * When copying blobs into the new store, also delete them from the old store.
79
+ * Default: off — the user must explicitly opt in to destructive behavior.
80
+ */ const [deleteBackupBlobsFromSource, setDeleteBackupBlobsFromSource] = useState(false);
81
+ const [, setHasExistingBackupBlobToken] = useState(false);
82
+ const [storedBackupBlobAccess, setStoredBackupBlobAccess] = useState(null);
83
+ const [effectiveBackupBlobAccess, setEffectiveBackupBlobAccess] = useState(null);
84
+ const [tokenCheck, setTokenCheck] = useState({
85
+ status: 'idle'
86
+ });
87
+ const [pluginOverridesRetention, setPluginOverridesRetention] = useState(false);
88
+ const [cronInfo, setCronInfo] = useState(undefined);
89
+ const [transferSummary, setTransferSummary] = useState(null);
90
+ const [activeTransfer, setActiveTransfer] = useState(null);
91
+ const [transferLive, setTransferLive] = useState(null);
92
+ const [savePhase, setSavePhase] = useState('idle');
93
+ const [saveError, setSaveError] = useState(null);
94
+ const [isPreparingOpen, setIsPreparingOpen] = useState(false);
95
+ const resetState = useCallback(()=>{
96
+ setPreview(null);
97
+ setPhase('idle');
98
+ setErrorMessage(null);
99
+ setSelected({});
100
+ setBackupAllCollections(false);
101
+ setIncludeMediaBlobs(false);
102
+ setBackupsToKeep(10);
103
+ setBackupBlobReadWriteToken('');
104
+ setTransferBackupBlobs(true);
105
+ setDeleteBackupBlobsFromSource(false);
106
+ setHasExistingBackupBlobToken(false);
107
+ setStoredBackupBlobAccess(null);
108
+ setEffectiveBackupBlobAccess(null);
109
+ setTokenCheck({
110
+ status: 'idle'
111
+ });
112
+ setPluginOverridesRetention(false);
113
+ setCronInfo(undefined);
114
+ setTransferSummary(null);
115
+ setActiveTransfer(null);
116
+ setTransferLive(null);
117
+ setSavePhase('idle');
118
+ setSaveError(null);
119
+ isPreparingOpenRef.current = false;
120
+ setIsPreparingOpen(false);
121
+ }, []);
122
+ useEffect(()=>{
123
+ const el = dialogRef.current;
124
+ if (!el) {
125
+ return;
126
+ }
127
+ const onClose = ()=>resetState();
128
+ el.addEventListener('close', onClose);
129
+ return ()=>el.removeEventListener('close', onClose);
130
+ }, [
131
+ resetState
132
+ ]);
133
+ const fetchSettingsAndPreview = useCallback(async ()=>{
134
+ const locale = typeof document !== 'undefined' ? (document.documentElement.lang || 'en').slice(0, 2) : 'en';
135
+ const [settingsRes, previewRes] = await Promise.all([
136
+ fetch(backupPluginPublicApiPaths.adminSettings, {
137
+ method: 'GET'
138
+ }),
139
+ fetch(backupPluginPublicApiPaths.adminBackupPreview, {
140
+ body: JSON.stringify({
141
+ locale
142
+ }),
143
+ headers: {
144
+ 'Content-Type': 'application/json'
145
+ },
146
+ method: 'POST'
147
+ })
148
+ ]);
149
+ const settingsJson = await settingsRes.json();
150
+ if (!settingsRes.ok) {
151
+ throw new Error(settingsJson.error || 'Could not load settings');
152
+ }
153
+ const previewJson = await previewRes.json();
154
+ if (!previewRes.ok) {
155
+ throw new Error(previewJson.error || 'Could not load collections');
156
+ }
157
+ return {
158
+ previewJson,
159
+ settingsJson
160
+ };
161
+ }, []);
162
+ const applyLoadedSettingsPreview = useCallback((settingsJson, previewJson)=>{
163
+ setActiveTransfer(null);
164
+ setTransferLive(null);
165
+ setPreview(previewJson);
166
+ setBackupsToKeep(typeof settingsJson.backupsToKeep === 'number' ? settingsJson.backupsToKeep : 10);
167
+ setBackupBlobReadWriteToken(settingsJson.backupBlobTokenMasked || '');
168
+ setHasExistingBackupBlobToken(settingsJson.hasBackupBlobReadWriteToken === true);
169
+ setStoredBackupBlobAccess(settingsJson.backupBlobAccess ?? null);
170
+ setEffectiveBackupBlobAccess(settingsJson.backupBlobAccessEffective ?? null);
171
+ setTokenCheck({
172
+ status: 'idle'
173
+ });
174
+ setPluginOverridesRetention(settingsJson.pluginBackupsToKeepOverride === true);
175
+ setCronInfo(settingsJson.cron ?? null);
176
+ setIncludeMediaBlobs(settingsJson.includeMediaForCron === true);
177
+ const skip = Array.isArray(settingsJson.skipMongoCollections) ? settingsJson.skipMongoCollections : [];
178
+ setSelected(selectedFromSkipMongoNames(previewJson, skip));
179
+ setBackupAllCollections(skip.length === 0);
180
+ setTransferBackupBlobs(true);
181
+ setDeleteBackupBlobsFromSource(false);
182
+ setPhase('ready');
183
+ }, []);
184
+ const loadPreview = useCallback(async ()=>{
185
+ setPhase('loading');
186
+ setErrorMessage(null);
187
+ setPreview(null);
188
+ setCronInfo(undefined);
189
+ try {
190
+ const { previewJson, settingsJson } = await fetchSettingsAndPreview();
191
+ applyLoadedSettingsPreview(settingsJson, previewJson);
192
+ } catch (e) {
193
+ setPhase('error');
194
+ setErrorMessage(e instanceof Error ? e.message : 'Could not load backup settings');
195
+ }
196
+ }, [
197
+ applyLoadedSettingsPreview,
198
+ fetchSettingsAndPreview
199
+ ]);
200
+ const openDialog = useCallback(()=>{
201
+ if (isPreparingOpenRef.current) {
202
+ return;
203
+ }
204
+ isPreparingOpenRef.current = true;
205
+ setSavePhase('idle');
206
+ setSaveError(null);
207
+ setIsPreparingOpen(true);
208
+ void (async ()=>{
209
+ try {
210
+ const { previewJson, settingsJson } = await fetchSettingsAndPreview();
211
+ applyLoadedSettingsPreview(settingsJson, previewJson);
212
+ dialogRef.current?.showModal();
213
+ } catch (e) {
214
+ setPhase('error');
215
+ setErrorMessage(e instanceof Error ? e.message : 'Could not load backup settings');
216
+ setPreview(null);
217
+ setCronInfo(undefined);
218
+ dialogRef.current?.showModal();
219
+ } finally{
220
+ isPreparingOpenRef.current = false;
221
+ setIsPreparingOpen(false);
222
+ }
223
+ })();
224
+ }, [
225
+ applyLoadedSettingsPreview,
226
+ fetchSettingsAndPreview
227
+ ]);
228
+ const skipCollections = useMemo(()=>{
229
+ if (!preview) {
230
+ return [];
231
+ }
232
+ if (backupAllCollections) {
233
+ return [];
234
+ }
235
+ return skipMongoNamesFromPreview(preview, selected);
236
+ }, [
237
+ backupAllCollections,
238
+ preview,
239
+ selected
240
+ ]);
241
+ const mediaGroupIncluded = preview ? backupAllCollections || selected['media'] !== false : false;
242
+ const hasMediaBlobOption = Boolean(preview && preview.mediaBlobCandidates > 0 && mediaGroupIncluded);
243
+ const willSaveNewRawToken = useMemo(()=>backupBlobReadWriteToken.trim().length > 0 && !/\*{2,}/.test(backupBlobReadWriteToken), [
244
+ backupBlobReadWriteToken
245
+ ]);
246
+ const onChangeBackupBlobToken = useCallback((value)=>{
247
+ setBackupBlobReadWriteToken(value);
248
+ setTokenCheck({
249
+ status: 'idle'
250
+ });
251
+ }, []);
252
+ // Auto-validate a freshly pasted/typed token after a short debounce. The masked token coming
253
+ // from the server (contains "**") never triggers this because `willSaveNewRawToken` stays false.
254
+ useEffect(()=>{
255
+ if (!willSaveNewRawToken) {
256
+ setTokenCheck((prev)=>prev.status === 'idle' ? prev : {
257
+ status: 'idle'
258
+ });
259
+ return;
260
+ }
261
+ const candidate = backupBlobReadWriteToken.trim();
262
+ if (!candidate) {
263
+ return;
264
+ }
265
+ let cancelled = false;
266
+ const timer = window.setTimeout(async ()=>{
267
+ if (cancelled) {
268
+ return;
269
+ }
270
+ setTokenCheck({
271
+ status: 'checking'
272
+ });
273
+ try {
274
+ const response = await fetch(backupPluginPublicApiPaths.adminValidateBlobToken, {
275
+ body: JSON.stringify({
276
+ token: candidate
277
+ }),
278
+ headers: {
279
+ 'Content-Type': 'application/json'
280
+ },
281
+ method: 'POST'
282
+ });
283
+ if (cancelled) {
284
+ return;
285
+ }
286
+ const data = await response.json();
287
+ if (cancelled) {
288
+ return;
289
+ }
290
+ if (response.ok && data.ok && (data.access === 'public' || data.access === 'private')) {
291
+ setTokenCheck({
292
+ access: data.access,
293
+ status: 'valid'
294
+ });
295
+ } else {
296
+ setTokenCheck({
297
+ message: data.error || 'Token rejected',
298
+ status: 'invalid'
299
+ });
300
+ }
301
+ } catch (error) {
302
+ if (cancelled) {
303
+ return;
304
+ }
305
+ setTokenCheck({
306
+ message: error instanceof Error ? error.message : 'Token validation failed',
307
+ status: 'invalid'
308
+ });
309
+ }
310
+ }, 150);
311
+ return ()=>{
312
+ cancelled = true;
313
+ window.clearTimeout(timer);
314
+ };
315
+ }, [
316
+ backupBlobReadWriteToken,
317
+ willSaveNewRawToken
318
+ ]);
319
+ const blockSaveForTokenCheck = willSaveNewRawToken && (tokenCheck.status === 'checking' || tokenCheck.status === 'invalid');
320
+ const tokenStatusPill = (()=>{
321
+ if (willSaveNewRawToken) {
322
+ if (tokenCheck.status === 'checking') {
323
+ return /*#__PURE__*/ _jsx("span", {
324
+ className: "backup-item__pill backup-item__pill--neutral",
325
+ children: "Checking…"
326
+ });
327
+ }
328
+ if (tokenCheck.status === 'valid' && tokenCheck.access) {
329
+ return /*#__PURE__*/ _jsx("span", {
330
+ className: `backup-item__pill backup-item__pill--${tokenCheck.access === 'private' ? 'cron' : 'manual'}`,
331
+ children: tokenCheck.access === 'private' ? 'Private' : 'Public'
332
+ });
333
+ }
334
+ if (tokenCheck.status === 'invalid') {
335
+ return /*#__PURE__*/ _jsx("span", {
336
+ className: "backup-item__pill backup-item__pill--danger",
337
+ children: "Invalid"
338
+ });
339
+ }
340
+ return null;
341
+ }
342
+ if (effectiveBackupBlobAccess) {
343
+ return /*#__PURE__*/ _jsx("span", {
344
+ className: `backup-item__pill backup-item__pill--${effectiveBackupBlobAccess === 'private' ? 'cron' : 'manual'}`,
345
+ title: storedBackupBlobAccess ? 'Detected and persisted after last validation' : 'Heuristic fallback — will be re-validated when saving a new token',
346
+ children: effectiveBackupBlobAccess === 'private' ? 'Private' : 'Public'
347
+ });
348
+ }
349
+ return null;
350
+ })();
351
+ const includeMediaForCron = useMemo(()=>{
352
+ if (!hasMediaBlobOption) {
353
+ return false;
354
+ }
355
+ return includeMediaBlobs;
356
+ }, [
357
+ hasMediaBlobOption,
358
+ includeMediaBlobs
359
+ ]);
360
+ const onToggleGroup = useCallback((groupId)=>{
361
+ setSelected((prev)=>{
362
+ const isOn = prev[groupId] !== false;
363
+ return {
364
+ ...prev,
365
+ [groupId]: !isOn
366
+ };
367
+ });
368
+ }, []);
369
+ const onToggleBackupAllCollections = useCallback(()=>{
370
+ setBackupAllCollections((prev)=>{
371
+ const next = !prev;
372
+ if (next && preview) {
373
+ setSelected(Object.fromEntries(preview.groups.map((g)=>[
374
+ g.groupId,
375
+ true
376
+ ])));
377
+ }
378
+ return next;
379
+ });
380
+ }, [
381
+ preview
382
+ ]);
383
+ const save = useCallback(async ()=>{
384
+ if (!preview) {
385
+ return;
386
+ }
387
+ setSavePhase('saving');
388
+ setSaveError(null);
389
+ setTransferSummary(null);
390
+ setActiveTransfer(null);
391
+ setTransferLive(null);
392
+ try {
393
+ const response = await fetch(backupPluginPublicApiPaths.adminSettings, {
394
+ body: JSON.stringify({
395
+ backupBlobReadWriteToken,
396
+ backupsToKeep,
397
+ deleteBackupBlobsFromSource: transferBackupBlobs && deleteBackupBlobsFromSource,
398
+ includeMediaForCron,
399
+ skipMongoCollections: skipCollections,
400
+ transferBackupBlobs
401
+ }),
402
+ headers: {
403
+ 'Content-Type': 'application/json'
404
+ },
405
+ method: 'PATCH'
406
+ });
407
+ const data = await response.json();
408
+ if (!response.ok) {
409
+ throw new Error(data.error || 'Save failed');
410
+ }
411
+ setHasExistingBackupBlobToken(data.hasBackupBlobReadWriteToken === true);
412
+ setBackupBlobReadWriteToken(data.backupBlobTokenMasked || '');
413
+ setStoredBackupBlobAccess(data.backupBlobAccess ?? null);
414
+ setEffectiveBackupBlobAccess(data.backupBlobAccessEffective ?? null);
415
+ setTokenCheck({
416
+ status: 'idle'
417
+ });
418
+ const tr = data.transfer;
419
+ if (tr?.performed && tr.deferred && tr.taskId && tr.pollSecret) {
420
+ setActiveTransfer({
421
+ pollSecret: tr.pollSecret,
422
+ taskId: tr.taskId,
423
+ total: tr.total
424
+ });
425
+ setTransferLive({
426
+ failed: 0,
427
+ total: tr.total,
428
+ transferred: 0
429
+ });
430
+ setSavePhase('transferring');
431
+ return;
432
+ }
433
+ setTransferSummary(tr ?? null);
434
+ setSavePhase('saved');
435
+ router.refresh();
436
+ if (!tr?.performed) {
437
+ window.setTimeout(()=>{
438
+ setSavePhase('idle');
439
+ dialogRef.current?.close();
440
+ }, 800);
441
+ } else {
442
+ window.setTimeout(()=>{
443
+ setSavePhase('idle');
444
+ dialogRef.current?.close();
445
+ }, 1200);
446
+ }
447
+ } catch (e) {
448
+ setSavePhase('error');
449
+ setSaveError(e instanceof Error ? e.message : 'Save failed');
450
+ }
451
+ }, [
452
+ preview,
453
+ backupsToKeep,
454
+ backupBlobReadWriteToken,
455
+ skipCollections,
456
+ includeMediaForCron,
457
+ transferBackupBlobs,
458
+ deleteBackupBlobsFromSource,
459
+ router
460
+ ]);
461
+ useEffect(()=>{
462
+ if (!activeTransfer) {
463
+ return;
464
+ }
465
+ let cancelled = false;
466
+ let intervalId = null;
467
+ let successCloseTimer = null;
468
+ const stopPolling = ()=>{
469
+ if (intervalId != null) {
470
+ window.clearInterval(intervalId);
471
+ intervalId = null;
472
+ }
473
+ };
474
+ const pollOnce = async ()=>{
475
+ const res = await fetch(backupPluginPublicApiPaths.adminTask(activeTransfer.taskId), {
476
+ cache: 'no-store',
477
+ headers: {
478
+ Authorization: `Bearer ${activeTransfer.pollSecret}`
479
+ }
480
+ });
481
+ if (!res.ok || cancelled) {
482
+ return;
483
+ }
484
+ const task = await res.json();
485
+ const parsed = parseBlobTransferTaskMessage(task.message);
486
+ if (parsed) {
487
+ setTransferLive({
488
+ failed: parsed.failed,
489
+ pathname: parsed.pathname,
490
+ total: parsed.total,
491
+ transferred: parsed.transferred
492
+ });
493
+ }
494
+ if (task.status === 'completed') {
495
+ stopPolling();
496
+ const final = parseBlobTransferTaskMessage(task.message);
497
+ setTransferSummary({
498
+ deferred: false,
499
+ failed: final?.failed ?? 0,
500
+ performed: true,
501
+ skipped: 0,
502
+ total: final?.total ?? activeTransfer.total,
503
+ transferred: final?.transferred ?? 0
504
+ });
505
+ setActiveTransfer(null);
506
+ setTransferLive(null);
507
+ setSavePhase('saved');
508
+ try {
509
+ const settingsRes = await fetch(backupPluginPublicApiPaths.adminSettings, {
510
+ method: 'GET'
511
+ });
512
+ const refreshed = await settingsRes.json();
513
+ if (settingsRes.ok) {
514
+ setBackupBlobReadWriteToken(refreshed.backupBlobTokenMasked || '');
515
+ setHasExistingBackupBlobToken(refreshed.hasBackupBlobReadWriteToken === true);
516
+ }
517
+ } catch {
518
+ /* ignore */ }
519
+ router.refresh();
520
+ successCloseTimer = window.setTimeout(()=>{
521
+ successCloseTimer = null;
522
+ setSavePhase('idle');
523
+ setTransferSummary(null);
524
+ dialogRef.current?.close();
525
+ }, 1400);
526
+ return;
527
+ }
528
+ if (task.status === 'failed') {
529
+ stopPolling();
530
+ setSavePhase('error');
531
+ setSaveError(task.error || task.message || 'Transfer failed');
532
+ setActiveTransfer(null);
533
+ setTransferLive(null);
534
+ }
535
+ };
536
+ intervalId = window.setInterval(()=>void pollOnce(), 900);
537
+ void pollOnce();
538
+ return ()=>{
539
+ cancelled = true;
540
+ stopPolling();
541
+ if (successCloseTimer != null) {
542
+ window.clearTimeout(successCloseTimer);
543
+ }
544
+ };
545
+ }, [
546
+ activeTransfer,
547
+ router
548
+ ]);
549
+ const retentionDisabled = pluginOverridesRetention;
550
+ return /*#__PURE__*/ _jsxs(_Fragment, {
551
+ children: [
552
+ /*#__PURE__*/ _jsx(Button, {
553
+ buttonStyle: "secondary",
554
+ disabled: isPreparingOpen,
555
+ onClick: openDialog,
556
+ size: "small",
557
+ children: isPreparingOpen ? 'Loading…' : 'Backup settings'
558
+ }),
559
+ /*#__PURE__*/ _jsxs("dialog", {
560
+ className: "backup-confirm-dialog backup-confirm-dialog--settings",
561
+ onMouseDown: (e)=>closeNativeDialogOnBackdropPointer(e, dialogRef),
562
+ ref: dialogRef,
563
+ children: [
564
+ /*#__PURE__*/ _jsx("p", {
565
+ className: "backup-confirm-dialog__title",
566
+ children: "Scheduled backup settings"
567
+ }),
568
+ /*#__PURE__*/ _jsx("p", {
569
+ className: "backup-confirm-dialog__subtitle backup-confirm-dialog__subtitle--with-help",
570
+ children: /*#__PURE__*/ _jsx("span", {
571
+ className: "backup-confirm-dialog__subtitle-text",
572
+ children: TIP_SETTINGS_SUBTITLE
573
+ })
574
+ }),
575
+ /*#__PURE__*/ _jsxs("div", {
576
+ className: "backup-confirm-dialog__body restore-preview",
577
+ children: [
578
+ phase !== 'idle' ? /*#__PURE__*/ _jsxs(_Fragment, {
579
+ children: [
580
+ /*#__PURE__*/ _jsx("p", {
581
+ className: "restore-preview__sticky-heading",
582
+ children: "Schedule"
583
+ }),
584
+ phase === 'loading' ? /*#__PURE__*/ _jsxs("div", {
585
+ "aria-live": "polite",
586
+ className: "restore-preview__loading-block restore-preview__loading-block--section",
587
+ children: [
588
+ /*#__PURE__*/ _jsx("p", {
589
+ className: "restore-preview__status",
590
+ children: "Loading cron configuration…"
591
+ }),
592
+ /*#__PURE__*/ _jsx("div", {
593
+ className: "restore-preview__loading-line"
594
+ }),
595
+ /*#__PURE__*/ _jsx("div", {
596
+ className: "restore-preview__loading-line restore-preview__loading-line--short"
597
+ })
598
+ ]
599
+ }) : phase === 'error' ? /*#__PURE__*/ _jsx("p", {
600
+ className: "restore-preview__intro",
601
+ children: "Schedule details could not be loaded."
602
+ }) : cronInfo ? /*#__PURE__*/ _jsxs(_Fragment, {
603
+ children: [
604
+ /*#__PURE__*/ _jsxs("p", {
605
+ className: "restore-preview__intro",
606
+ children: [
607
+ "Active job: ",
608
+ /*#__PURE__*/ _jsx("code", {
609
+ children: cronInfo.configFileRelative
610
+ }),
611
+ " — edit and redeploy to change the schedule."
612
+ ]
613
+ }),
614
+ /*#__PURE__*/ _jsxs("div", {
615
+ "aria-label": "Configured schedule",
616
+ className: "restore-preview__settings-block",
617
+ children: [
618
+ /*#__PURE__*/ _jsxs("p", {
619
+ className: "restore-preview__settings-row",
620
+ children: [
621
+ /*#__PURE__*/ _jsx("span", {
622
+ className: "restore-preview__settings-k",
623
+ children: "HTTP path"
624
+ }),
625
+ ' ',
626
+ /*#__PURE__*/ _jsx("code", {
627
+ children: cronInfo.path
628
+ })
629
+ ]
630
+ }),
631
+ /*#__PURE__*/ _jsxs("p", {
632
+ className: "restore-preview__settings-row",
633
+ children: [
634
+ /*#__PURE__*/ _jsx("span", {
635
+ className: "restore-preview__settings-k",
636
+ children: "Cron expression"
637
+ }),
638
+ ' ',
639
+ /*#__PURE__*/ _jsx("code", {
640
+ children: cronInfo.schedule
641
+ })
642
+ ]
643
+ }),
644
+ cronInfo.humanDescription ? /*#__PURE__*/ _jsx("p", {
645
+ className: "restore-preview__settings-human",
646
+ children: cronInfo.humanDescription
647
+ }) : /*#__PURE__*/ _jsx("p", {
648
+ className: "restore-preview__settings-human restore-preview__settings-human--muted",
649
+ children: "Could not turn the cron expression into a short description."
650
+ })
651
+ ]
652
+ })
653
+ ]
654
+ }) : /*#__PURE__*/ _jsxs(_Fragment, {
655
+ children: [
656
+ /*#__PURE__*/ _jsxs("p", {
657
+ className: "restore-preview__intro",
658
+ children: [
659
+ "No vercel ",
660
+ /*#__PURE__*/ _jsx("code", {
661
+ children: "crons"
662
+ }),
663
+ " entry for the backup path yet. You can either setup an external service to call /api/backup-mongodb/cron/run with your cron secret or use Vercel. To use vercel create a file ",
664
+ /*#__PURE__*/ _jsx("code", {
665
+ children: "vercel.json"
666
+ }),
667
+ " in your project root with the following content:"
668
+ ]
669
+ }),
670
+ /*#__PURE__*/ _jsx("p", {
671
+ className: "restore-preview__settings-hint",
672
+ children: "Example"
673
+ }),
674
+ /*#__PURE__*/ _jsx("pre", {
675
+ className: "restore-preview__settings-snippet",
676
+ children: `{
677
+ "crons": [
678
+ {
679
+ "path": "/api/backup-mongodb/cron/run",
680
+ "schedule": "0 3 * * *"
681
+ }
682
+ ]
683
+ }`
684
+ }),
685
+ /*#__PURE__*/ _jsxs("p", {
686
+ children: [
687
+ "Then add a ",
688
+ /*#__PURE__*/ _jsx("code", {
689
+ children: "CRON_SECRET"
690
+ }),
691
+ " environment variable to your Vercel project and redeploy on Vercel so the cron job is registered."
692
+ ]
693
+ })
694
+ ]
695
+ })
696
+ ]
697
+ }) : null,
698
+ /*#__PURE__*/ _jsx("p", {
699
+ className: "restore-preview__sticky-heading",
700
+ children: "Retention"
701
+ }),
702
+ /*#__PURE__*/ _jsx("p", {
703
+ className: "restore-preview__intro",
704
+ children: retentionDisabled ? /*#__PURE__*/ _jsxs(_Fragment, {
705
+ children: [
706
+ "Locked by ",
707
+ /*#__PURE__*/ _jsxs("code", {
708
+ children: [
709
+ "backupMongodbPlugin(",
710
+ '{ backupsToKeep }',
711
+ ")"
712
+ ]
713
+ }),
714
+ " in Payload config."
715
+ ]
716
+ }) : /*#__PURE__*/ _jsx(_Fragment, {
717
+ children: "Oldest cron archives are deleted once the count exceeds this limit."
718
+ })
719
+ }),
720
+ /*#__PURE__*/ _jsxs("div", {
721
+ className: "restore-preview__settings-fields",
722
+ children: [
723
+ /*#__PURE__*/ _jsx("label", {
724
+ className: "restore-preview__settings-label",
725
+ htmlFor: "backups-to-keep",
726
+ children: "Cron backups to keep"
727
+ }),
728
+ /*#__PURE__*/ _jsx("input", {
729
+ "aria-label": "Cron backups to keep",
730
+ className: "restore-preview__settings-input",
731
+ disabled: retentionDisabled,
732
+ id: "backups-to-keep",
733
+ max: 365,
734
+ min: 1,
735
+ onChange: (e)=>setBackupsToKeep(Number(e.target.value) || 1),
736
+ type: "number",
737
+ value: backupsToKeep
738
+ })
739
+ ]
740
+ }),
741
+ /*#__PURE__*/ _jsxs("p", {
742
+ className: "restore-preview__sticky-heading",
743
+ children: [
744
+ "Dedicated backup storage",
745
+ ' ',
746
+ /*#__PURE__*/ _jsx("span", {
747
+ className: "restore-preview__sticky-heading-hint",
748
+ children: "(optional)"
749
+ })
750
+ ]
751
+ }),
752
+ /*#__PURE__*/ _jsxs("p", {
753
+ className: "restore-preview__intro",
754
+ children: [
755
+ "Separate Vercel Blob token for ",
756
+ /*#__PURE__*/ _jsx("code", {
757
+ children: "backups/"
758
+ }),
759
+ ". Without one, archives share the default media store."
760
+ ]
761
+ }),
762
+ /*#__PURE__*/ _jsxs("div", {
763
+ className: "restore-preview__settings-fields",
764
+ children: [
765
+ /*#__PURE__*/ _jsx("label", {
766
+ className: "restore-preview__settings-label",
767
+ htmlFor: "backup-blob-token",
768
+ children: "Backup Blob read/write token"
769
+ }),
770
+ /*#__PURE__*/ _jsxs("div", {
771
+ className: "restore-preview__blob-token-row",
772
+ children: [
773
+ /*#__PURE__*/ _jsx("input", {
774
+ "aria-label": "Backup Blob read/write token",
775
+ autoComplete: "off",
776
+ className: "restore-preview__settings-input restore-preview__settings-input--wide",
777
+ id: "backup-blob-token",
778
+ onChange: (e)=>onChangeBackupBlobToken(e.target.value),
779
+ placeholder: "vercel_blob_rw_...",
780
+ spellCheck: false,
781
+ type: "text",
782
+ value: backupBlobReadWriteToken
783
+ }),
784
+ /*#__PURE__*/ _jsx("span", {
785
+ className: "restore-preview__blob-token-pill-slot",
786
+ children: tokenStatusPill
787
+ })
788
+ ]
789
+ }),
790
+ tokenCheck.status === 'invalid' && tokenCheck.message ? /*#__PURE__*/ _jsx("p", {
791
+ "aria-live": "polite",
792
+ className: "restore-preview__blob-token-error",
793
+ children: tokenCheck.message
794
+ }) : null,
795
+ savePhase === 'transferring' && transferLive && transferLive.total > 0 ? /*#__PURE__*/ _jsxs("div", {
796
+ "aria-live": "polite",
797
+ className: "restore-preview__transfer-panel",
798
+ children: [
799
+ /*#__PURE__*/ _jsx("div", {
800
+ className: "restore-preview__transfer-bar-track",
801
+ children: /*#__PURE__*/ _jsx("div", {
802
+ className: "restore-preview__transfer-bar-fill",
803
+ style: {
804
+ width: `${Math.min(100, (transferLive.transferred + transferLive.failed) / transferLive.total * 100)}%`
805
+ }
806
+ })
807
+ }),
808
+ /*#__PURE__*/ _jsxs("p", {
809
+ className: "restore-preview__transfer-meta",
810
+ children: [
811
+ "Copying backup files: ",
812
+ transferLive.transferred + transferLive.failed,
813
+ " /",
814
+ ' ',
815
+ transferLive.total,
816
+ transferLive.failed > 0 ? ` (${transferLive.failed} failed)` : '',
817
+ transferLive.pathname ? /*#__PURE__*/ _jsxs(_Fragment, {
818
+ children: [
819
+ /*#__PURE__*/ _jsx("br", {}),
820
+ /*#__PURE__*/ _jsx("span", {
821
+ className: "restore-preview__transfer-path",
822
+ title: transferLive.pathname,
823
+ children: transferLive.pathname.length > 56 ? `${transferLive.pathname.slice(0, 28)}…${transferLive.pathname.slice(-24)}` : transferLive.pathname
824
+ })
825
+ ]
826
+ }) : null
827
+ ]
828
+ })
829
+ ]
830
+ }) : null,
831
+ willSaveNewRawToken ? /*#__PURE__*/ _jsxs(_Fragment, {
832
+ children: [
833
+ /*#__PURE__*/ _jsxs("div", {
834
+ className: "field-type checkbox backup-dashboard__collapsible-checkbox restore-preview__blob-transfer-row restore-preview__blob-transfer-row--with-help-tip",
835
+ children: [
836
+ /*#__PURE__*/ _jsx("input", {
837
+ "aria-label": "Copy existing backup files into this storage when saving",
838
+ checked: transferBackupBlobs,
839
+ className: "checkbox-input__input",
840
+ id: transferOptId,
841
+ onChange: (e)=>setTransferBackupBlobs(e.target.checked),
842
+ type: "checkbox"
843
+ }),
844
+ /*#__PURE__*/ _jsx("label", {
845
+ className: "field-label",
846
+ htmlFor: transferOptId,
847
+ children: "Copy existing backup files into this storage when saving"
848
+ }),
849
+ /*#__PURE__*/ _jsx(SettingsHelpTip, {
850
+ multiline: true,
851
+ tip: TIP_TRANSFER_COPY
852
+ })
853
+ ]
854
+ }),
855
+ transferBackupBlobs ? /*#__PURE__*/ _jsxs("div", {
856
+ className: "field-type checkbox backup-dashboard__collapsible-checkbox restore-preview__blob-transfer-row restore-preview__blob-transfer-row--with-help-tip",
857
+ children: [
858
+ /*#__PURE__*/ _jsx("input", {
859
+ "aria-label": "Delete from previous storage after successful copy",
860
+ checked: deleteBackupBlobsFromSource,
861
+ className: "checkbox-input__input",
862
+ id: deleteSourceOptId,
863
+ onChange: (e)=>setDeleteBackupBlobsFromSource(e.target.checked),
864
+ type: "checkbox"
865
+ }),
866
+ /*#__PURE__*/ _jsx("label", {
867
+ className: "field-label",
868
+ htmlFor: deleteSourceOptId,
869
+ children: "Delete from previous storage after successful copy"
870
+ }),
871
+ /*#__PURE__*/ _jsx(SettingsHelpTip, {
872
+ multiline: true,
873
+ tip: TIP_TRANSFER_DELETE
874
+ })
875
+ ]
876
+ }) : null
877
+ ]
878
+ }) : null
879
+ ]
880
+ }),
881
+ /*#__PURE__*/ _jsx(CollectionBackupPreviewBody, {
882
+ embedded: true,
883
+ errorMessage: errorMessage,
884
+ includeAllCollections: backupAllCollections,
885
+ includeAllLabel: "Backup all collections",
886
+ includeMediaBlobs: includeMediaBlobs,
887
+ onRetry: loadPreview,
888
+ onToggleGroup: onToggleGroup,
889
+ onToggleIncludeAllCollections: onToggleBackupAllCollections,
890
+ onToggleIncludeMedia: ()=>setIncludeMediaBlobs((v)=>!v),
891
+ phase: phase,
892
+ preview: preview,
893
+ selected: selected
894
+ }),
895
+ saveError ? /*#__PURE__*/ _jsx("p", {
896
+ className: "restore-preview__error restore-preview__error--inline",
897
+ children: saveError
898
+ }) : null,
899
+ savePhase === 'saving' ? /*#__PURE__*/ _jsx("p", {
900
+ className: "restore-preview__status restore-preview__status--footer",
901
+ children: "Saving settings…"
902
+ }) : null,
903
+ transferSummary?.performed ? /*#__PURE__*/ _jsxs("p", {
904
+ className: "restore-preview__status restore-preview__status--footer",
905
+ children: [
906
+ "Migrated backups: ",
907
+ transferSummary.transferred,
908
+ "/",
909
+ transferSummary.total,
910
+ transferSummary.failed > 0 ? `, failed: ${transferSummary.failed}` : ''
911
+ ]
912
+ }) : null
913
+ ]
914
+ }),
915
+ /*#__PURE__*/ _jsxs("div", {
916
+ className: "backup-confirm-dialog__actions",
917
+ children: [
918
+ /*#__PURE__*/ _jsx(Button, {
919
+ buttonStyle: "primary",
920
+ disabled: phase !== 'ready' || savePhase === 'saving' || savePhase === 'transferring' || blockSaveForTokenCheck,
921
+ onClick: ()=>void save(),
922
+ size: "small",
923
+ children: savePhase === 'transferring' ? 'Transferring…' : savePhase === 'saving' ? 'Saving…' : savePhase === 'saved' ? 'Saved' : 'Save settings'
924
+ }),
925
+ /*#__PURE__*/ _jsx(Button, {
926
+ buttonStyle: "secondary",
927
+ onClick: ()=>dialogRef.current?.close(),
928
+ size: "small",
929
+ children: "Cancel"
930
+ })
931
+ ]
932
+ })
933
+ ]
934
+ })
935
+ ]
936
+ });
937
+ };
938
+
939
+ //# sourceMappingURL=BackupSettingsModal.client.js.map