@plutonhq/core-frontend 0.1.13 → 0.1.14

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 (186) hide show
  1. package/dist-lib/@types/backups.d.ts +26 -0
  2. package/dist-lib/@types/backups.d.ts.map +1 -1
  3. package/dist-lib/@types/devices.d.ts +7 -0
  4. package/dist-lib/@types/devices.d.ts.map +1 -1
  5. package/dist-lib/@types/plans.d.ts +21 -1
  6. package/dist-lib/@types/plans.d.ts.map +1 -1
  7. package/dist-lib/@types/restores.d.ts +2 -0
  8. package/dist-lib/@types/restores.d.ts.map +1 -1
  9. package/dist-lib/components/Device/DeviceBackups/DeviceBackups.d.ts +3 -2
  10. package/dist-lib/components/Device/DeviceBackups/DeviceBackups.d.ts.map +1 -1
  11. package/dist-lib/components/Device/DeviceBackups/DeviceBackups.js +73 -85
  12. package/dist-lib/components/Device/DeviceBackups/DeviceBackups.js.map +1 -1
  13. package/dist-lib/components/Plan/BackupEvents/BackupEvents.d.ts.map +1 -1
  14. package/dist-lib/components/Plan/BackupEvents/BackupEvents.js +88 -50
  15. package/dist-lib/components/Plan/BackupEvents/BackupEvents.js.map +1 -1
  16. package/dist-lib/components/Plan/BackupEvents/BackupEvents.module.scss.js +70 -38
  17. package/dist-lib/components/Plan/BackupEvents/BackupEvents.module.scss.js.map +1 -1
  18. package/dist-lib/components/Plan/BackupProgress/BackupProgress.d.ts.map +1 -1
  19. package/dist-lib/components/Plan/BackupProgress/BackupProgress.js +166 -123
  20. package/dist-lib/components/Plan/BackupProgress/BackupProgress.js.map +1 -1
  21. package/dist-lib/components/Plan/BackupProgress/BackupProgress.module.scss.js +64 -30
  22. package/dist-lib/components/Plan/BackupProgress/BackupProgress.module.scss.js.map +1 -1
  23. package/dist-lib/components/Plan/Backups/Backups.d.ts +8 -1
  24. package/dist-lib/components/Plan/Backups/Backups.d.ts.map +1 -1
  25. package/dist-lib/components/Plan/Backups/Backups.js +154 -125
  26. package/dist-lib/components/Plan/Backups/Backups.js.map +1 -1
  27. package/dist-lib/components/Plan/EditPlan/EditPlan.d.ts.map +1 -1
  28. package/dist-lib/components/Plan/EditPlan/EditPlan.js +11 -10
  29. package/dist-lib/components/Plan/EditPlan/EditPlan.js.map +1 -1
  30. package/dist-lib/components/Plan/Mirrors/MirrorDetails.d.ts +12 -0
  31. package/dist-lib/components/Plan/Mirrors/MirrorDetails.d.ts.map +1 -0
  32. package/dist-lib/components/Plan/Mirrors/MirrorDetails.js +68 -0
  33. package/dist-lib/components/Plan/Mirrors/MirrorDetails.js.map +1 -0
  34. package/dist-lib/components/Plan/Mirrors/MirrorDetails.module.scss.js +26 -0
  35. package/dist-lib/components/Plan/Mirrors/MirrorDetails.module.scss.js.map +1 -0
  36. package/dist-lib/components/Plan/Mirrors/MirrorStatusBadge.d.ts +11 -0
  37. package/dist-lib/components/Plan/Mirrors/MirrorStatusBadge.d.ts.map +1 -0
  38. package/dist-lib/components/Plan/Mirrors/MirrorStatusBadge.js +38 -0
  39. package/dist-lib/components/Plan/Mirrors/MirrorStatusBadge.js.map +1 -0
  40. package/dist-lib/components/Plan/Mirrors/MirrorStatusBadge.module.scss.js +16 -0
  41. package/dist-lib/components/Plan/Mirrors/MirrorStatusBadge.module.scss.js.map +1 -0
  42. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelector.d.ts +14 -0
  43. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelector.d.ts.map +1 -0
  44. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelector.js +54 -0
  45. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelector.js.map +1 -0
  46. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelector.module.scss.js +26 -0
  47. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelector.module.scss.js.map +1 -0
  48. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelectorModal.d.ts +15 -0
  49. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelectorModal.d.ts.map +1 -0
  50. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelectorModal.js +34 -0
  51. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelectorModal.js.map +1 -0
  52. package/dist-lib/components/Plan/PlanBackups/PlanBackups.d.ts.map +1 -1
  53. package/dist-lib/components/Plan/PlanBackups/PlanBackups.js +20 -17
  54. package/dist-lib/components/Plan/PlanBackups/PlanBackups.js.map +1 -1
  55. package/dist-lib/components/Plan/PlanForm/PlanForm.d.ts +2 -1
  56. package/dist-lib/components/Plan/PlanForm/PlanForm.d.ts.map +1 -1
  57. package/dist-lib/components/Plan/PlanForm/PlanForm.js +85 -58
  58. package/dist-lib/components/Plan/PlanForm/PlanForm.js.map +1 -1
  59. package/dist-lib/components/Plan/PlanItems/PlanItem.d.ts.map +1 -1
  60. package/dist-lib/components/Plan/PlanItems/PlanItem.js +58 -59
  61. package/dist-lib/components/Plan/PlanItems/PlanItem.js.map +1 -1
  62. package/dist-lib/components/Plan/PlanRemoveModal/PlanRemoveModal.js +8 -8
  63. package/dist-lib/components/Plan/PlanRemoveModal/PlanRemoveModal.js.map +1 -1
  64. package/dist-lib/components/Plan/PlanSettings/PlanReplicationSettings.d.ts +14 -0
  65. package/dist-lib/components/Plan/PlanSettings/PlanReplicationSettings.d.ts.map +1 -0
  66. package/dist-lib/components/Plan/PlanSettings/PlanReplicationSettings.js +290 -0
  67. package/dist-lib/components/Plan/PlanSettings/PlanReplicationSettings.js.map +1 -0
  68. package/dist-lib/components/Plan/PlanSettings/PlanReplicationSettings.module.scss.js +26 -0
  69. package/dist-lib/components/Plan/PlanSettings/PlanReplicationSettings.module.scss.js.map +1 -0
  70. package/dist-lib/components/Plan/PlanStats/PlanStats.d.ts.map +1 -1
  71. package/dist-lib/components/Plan/PlanStats/PlanStats.js +41 -42
  72. package/dist-lib/components/Plan/PlanStats/PlanStats.js.map +1 -1
  73. package/dist-lib/components/Plan/PlanStats/PlanStats.module.scss.js +5 -5
  74. package/dist-lib/components/Plan/PlanStorageInfo/PlanStorageInfo.d.ts +15 -0
  75. package/dist-lib/components/Plan/PlanStorageInfo/PlanStorageInfo.d.ts.map +1 -0
  76. package/dist-lib/components/Plan/PlanStorageInfo/PlanStorageInfo.js +69 -0
  77. package/dist-lib/components/Plan/PlanStorageInfo/PlanStorageInfo.js.map +1 -0
  78. package/dist-lib/components/Plan/PlanStorageInfo/PlanStorageInfo.module.scss.js +16 -0
  79. package/dist-lib/components/Plan/PlanStorageInfo/PlanStorageInfo.module.scss.js.map +1 -0
  80. package/dist-lib/components/Restore/RestoreWizard/RestoreConfirmStep.d.ts.map +1 -1
  81. package/dist-lib/components/Restore/RestoreWizard/RestoreConfirmStep.js +36 -34
  82. package/dist-lib/components/Restore/RestoreWizard/RestoreConfirmStep.js.map +1 -1
  83. package/dist-lib/components/Restore/RestoreWizard/RestorePreviewStep.d.ts.map +1 -1
  84. package/dist-lib/components/Restore/RestoreWizard/RestorePreviewStep.js +7 -5
  85. package/dist-lib/components/Restore/RestoreWizard/RestorePreviewStep.js.map +1 -1
  86. package/dist-lib/components/Restore/RestoreWizard/RestoreSettingsStep.d.ts +12 -4
  87. package/dist-lib/components/Restore/RestoreWizard/RestoreSettingsStep.d.ts.map +1 -1
  88. package/dist-lib/components/Restore/RestoreWizard/RestoreSettingsStep.js +44 -32
  89. package/dist-lib/components/Restore/RestoreWizard/RestoreSettingsStep.js.map +1 -1
  90. package/dist-lib/components/Restore/RestoreWizard/RestoreWizard.d.ts +5 -1
  91. package/dist-lib/components/Restore/RestoreWizard/RestoreWizard.d.ts.map +1 -1
  92. package/dist-lib/components/Restore/RestoreWizard/RestoreWizard.js +48 -44
  93. package/dist-lib/components/Restore/RestoreWizard/RestoreWizard.js.map +1 -1
  94. package/dist-lib/components/Restore/RestoreWizard/RestoreWizard.module.scss.js +32 -32
  95. package/dist-lib/components/Settings/GeneralSettings/GeneralSettings.js +14 -14
  96. package/dist-lib/components/Settings/GeneralSettings/GeneralSettings.js.map +1 -1
  97. package/dist-lib/components/Settings/IntegrationSettings/IntegrationSettings.d.ts.map +1 -1
  98. package/dist-lib/components/Settings/IntegrationSettings/IntegrationSettings.js +28 -19
  99. package/dist-lib/components/Settings/IntegrationSettings/IntegrationSettings.js.map +1 -1
  100. package/dist-lib/components/common/Icon/Icon.d.ts.map +1 -1
  101. package/dist-lib/components/common/Icon/Icon.js +11 -0
  102. package/dist-lib/components/common/Icon/Icon.js.map +1 -1
  103. package/dist-lib/components/common/PageHeader/PageHeader.module.scss.js +6 -6
  104. package/dist-lib/components/index.d.ts +4 -0
  105. package/dist-lib/components/index.d.ts.map +1 -1
  106. package/dist-lib/components.js +86 -78
  107. package/dist-lib/components.js.map +1 -1
  108. package/dist-lib/hooks/usePwaAutoUpdate.d.ts +11 -2
  109. package/dist-lib/hooks/usePwaAutoUpdate.d.ts.map +1 -1
  110. package/dist-lib/hooks/usePwaAutoUpdate.js +32 -10
  111. package/dist-lib/hooks/usePwaAutoUpdate.js.map +1 -1
  112. package/dist-lib/router.d.ts.map +1 -1
  113. package/dist-lib/router.js +46 -35
  114. package/dist-lib/router.js.map +1 -1
  115. package/dist-lib/routes/DeviceSingle/DeviceSingle.d.ts.map +1 -1
  116. package/dist-lib/routes/DeviceSingle/DeviceSingle.js +40 -40
  117. package/dist-lib/routes/DeviceSingle/DeviceSingle.js.map +1 -1
  118. package/dist-lib/services/backups.d.ts +15 -2
  119. package/dist-lib/services/backups.d.ts.map +1 -1
  120. package/dist-lib/services/backups.js +119 -100
  121. package/dist-lib/services/backups.js.map +1 -1
  122. package/dist-lib/services/plans.d.ts +14 -0
  123. package/dist-lib/services/plans.d.ts.map +1 -1
  124. package/dist-lib/services/plans.js +160 -129
  125. package/dist-lib/services/plans.js.map +1 -1
  126. package/dist-lib/services/restores.d.ts +10 -2
  127. package/dist-lib/services/restores.d.ts.map +1 -1
  128. package/dist-lib/services/restores.js +61 -57
  129. package/dist-lib/services/restores.js.map +1 -1
  130. package/dist-lib/services/users.d.ts.map +1 -1
  131. package/dist-lib/services/users.js +32 -32
  132. package/dist-lib/services/users.js.map +1 -1
  133. package/dist-lib/services.js +107 -103
  134. package/dist-lib/styles/core-frontend.css +1 -1
  135. package/dist-lib/utils/progressHelpers.d.ts +12 -1
  136. package/dist-lib/utils/progressHelpers.d.ts.map +1 -1
  137. package/dist-lib/utils/progressHelpers.js +121 -63
  138. package/dist-lib/utils/progressHelpers.js.map +1 -1
  139. package/dist-lib/utils.js +29 -28
  140. package/package.json +1 -1
  141. package/src/@types/backups.ts +28 -0
  142. package/src/@types/devices.ts +8 -0
  143. package/src/@types/plans.ts +23 -1
  144. package/src/@types/restores.ts +2 -0
  145. package/src/components/Device/DeviceBackups/DeviceBackups.tsx +11 -36
  146. package/src/components/Plan/BackupEvents/BackupEvents.module.scss +65 -0
  147. package/src/components/Plan/BackupEvents/BackupEvents.tsx +65 -4
  148. package/src/components/Plan/BackupProgress/BackupProgress.module.scss +121 -3
  149. package/src/components/Plan/BackupProgress/BackupProgress.tsx +149 -71
  150. package/src/components/Plan/Backups/Backups.tsx +52 -4
  151. package/src/components/Plan/EditPlan/EditPlan.tsx +1 -0
  152. package/src/components/Plan/Mirrors/MirrorDetails.module.scss +76 -0
  153. package/src/components/Plan/Mirrors/MirrorDetails.tsx +100 -0
  154. package/src/components/Plan/Mirrors/MirrorStatusBadge.module.scss +25 -0
  155. package/src/components/Plan/Mirrors/MirrorStatusBadge.tsx +65 -0
  156. package/src/components/Plan/Mirrors/MirrorStorageSelector.module.scss +97 -0
  157. package/src/components/Plan/Mirrors/MirrorStorageSelector.tsx +70 -0
  158. package/src/components/Plan/Mirrors/MirrorStorageSelectorModal.tsx +40 -0
  159. package/src/components/Plan/PlanBackups/PlanBackups.tsx +4 -1
  160. package/src/components/Plan/PlanForm/PlanForm.tsx +30 -3
  161. package/src/components/Plan/PlanItems/PlanItem.tsx +3 -3
  162. package/src/components/Plan/PlanRemoveModal/PlanRemoveModal.tsx +1 -1
  163. package/src/components/Plan/PlanSettings/PlanReplicationSettings.module.scss +105 -0
  164. package/src/components/Plan/PlanSettings/PlanReplicationSettings.tsx +334 -0
  165. package/src/components/Plan/PlanStats/PlanStats.module.scss +1 -1
  166. package/src/components/Plan/PlanStats/PlanStats.tsx +8 -8
  167. package/src/components/Plan/PlanStorageInfo/PlanStorageInfo.module.scss +43 -0
  168. package/src/components/Plan/PlanStorageInfo/PlanStorageInfo.tsx +83 -0
  169. package/src/components/Restore/RestoreWizard/RestoreConfirmStep.tsx +2 -0
  170. package/src/components/Restore/RestoreWizard/RestorePreviewStep.tsx +2 -0
  171. package/src/components/Restore/RestoreWizard/RestoreSettingsStep.tsx +36 -13
  172. package/src/components/Restore/RestoreWizard/RestoreWizard.module.scss +4 -0
  173. package/src/components/Restore/RestoreWizard/RestoreWizard.tsx +9 -1
  174. package/src/components/Settings/GeneralSettings/GeneralSettings.tsx +1 -1
  175. package/src/components/Settings/IntegrationSettings/IntegrationSettings.tsx +9 -2
  176. package/src/components/common/Icon/Icon.tsx +10 -1
  177. package/src/components/common/PageHeader/PageHeader.module.scss +3 -0
  178. package/src/components/index.ts +6 -0
  179. package/src/hooks/usePwaAutoUpdate.ts +51 -11
  180. package/src/router.tsx +26 -17
  181. package/src/routes/DeviceSingle/DeviceSingle.tsx +3 -3
  182. package/src/services/backups.ts +32 -9
  183. package/src/services/plans.ts +45 -0
  184. package/src/services/restores.ts +10 -2
  185. package/src/services/users.ts +14 -5
  186. package/src/utils/progressHelpers.ts +85 -1
@@ -12,6 +12,8 @@ import RestoreWizard from '../../Restore/RestoreWizard/RestoreWizard';
12
12
  import StatusLabel from '../../common/StatusLabel/StatusLabel';
13
13
  import BackupEvents from '../BackupEvents/BackupEvents';
14
14
  import Input from '../../common/form/Input/Input';
15
+ import MirrorStatusBadge from '../Mirrors/MirrorStatusBadge';
16
+ import MirrorStorageSelectorModal from '../Mirrors/MirrorStorageSelectorModal';
15
17
 
16
18
  const DownloadLabel = ({ download, downloadBackup }: { download: Backup['download']; downloadBackup: () => void }) => {
17
19
  if (download?.status === 'started') {
@@ -67,6 +69,9 @@ const Backups = ({
67
69
  backups = [],
68
70
  sourceId,
69
71
  sourceType,
72
+ replicationSettings,
73
+ storage,
74
+ deviceId,
70
75
  // snapLimit,
71
76
  }: {
72
77
  planId: string;
@@ -75,12 +80,20 @@ const Backups = ({
75
80
  sourceId: string;
76
81
  sourceType: string;
77
82
  snapLimit: number;
83
+ deviceId: string;
84
+ storage: {
85
+ id: string;
86
+ type: string;
87
+ name: string;
88
+ };
89
+ replicationSettings: Plan['settings']['replication'];
78
90
  }) => {
79
91
  const [showSnapOptions, setShowSnapOptions] = useState<false | string>(false);
80
92
  const [showDeleteModal, setShowDeleteModal] = useState<Backup | false>(false);
81
93
  const [showRestoreModal, setShowRestoreModal] = useState<Backup | false>(false);
82
94
  const [showBackupEvents, setShowBackupEvents] = useState<false | string>(false);
83
95
  const [showEditModal, setShowEditModal] = useState<Backup | false>(false);
96
+ const [showStorageSelector, setShowStorageSelector] = useState<Backup | false>(false);
84
97
  const queryClient = useQueryClient();
85
98
  const deleteBackupMutation = useDeleteBackup();
86
99
  const updateBackupMutation = useUpdateBackup();
@@ -129,8 +142,16 @@ const Backups = ({
129
142
  );
130
143
  };
131
144
 
132
- const downloadBackup = (backupId: string) => {
133
- toast.promise(downloadBackupMutation.mutateAsync({ backupId, planId }), {
145
+ const handleDownloadClick = (backup: Backup) => {
146
+ if (replicationSettings?.enabled && backup.mirrors && backup.mirrors.some((m) => m.status === 'completed')) {
147
+ setShowStorageSelector(backup);
148
+ } else {
149
+ downloadBackup(backup.id);
150
+ }
151
+ };
152
+
153
+ const downloadBackup = (backupId: string, replicationId?: string) => {
154
+ toast.promise(downloadBackupMutation.mutateAsync({ backupId, planId, replicationId }), {
134
155
  pending: 'Sending Download Request...',
135
156
  success: 'Generating Download. This might take a while..',
136
157
  error: {
@@ -199,6 +220,9 @@ const Backups = ({
199
220
  <Icon type="bolt" size={14} />
200
221
  </span>
201
222
  )}
223
+ {replicationSettings?.enabled && snapshot.mirrors && snapshot.mirrors.length > 0 && (
224
+ <MirrorStatusBadge mirrors={snapshot.mirrors} planId={planId} backupId={id} replicationSettings={replicationSettings} />
225
+ )}
202
226
  {download && <DownloadLabel download={download} downloadBackup={() => getDownloadMutation.mutate(id)} />}
203
227
  </div>
204
228
  <div
@@ -257,7 +281,7 @@ const Backups = ({
257
281
  if (isDownloading) {
258
282
  cancelDownload(id);
259
283
  } else {
260
- downloadBackup(id);
284
+ handleDownloadClick(snapshot);
261
285
  }
262
286
  setShowSnapOptions(false);
263
287
  }}
@@ -319,7 +343,20 @@ const Backups = ({
319
343
  }}
320
344
  />
321
345
  )}
322
- {showRestoreModal && <RestoreWizard close={() => setShowRestoreModal(false)} planId={planId} backupId={showRestoreModal.id} />}
346
+ {showRestoreModal && (
347
+ <RestoreWizard
348
+ close={() => setShowRestoreModal(false)}
349
+ planId={planId}
350
+ backupId={showRestoreModal.id}
351
+ deviceId={deviceId}
352
+ planStorage={storage}
353
+ mirrors={
354
+ replicationSettings?.enabled && showRestoreModal.mirrors && showRestoreModal.mirrors.some((m) => m.status === 'completed')
355
+ ? showRestoreModal.mirrors
356
+ : []
357
+ }
358
+ />
359
+ )}
323
360
  {showBackupEvents && (
324
361
  <BackupEvents
325
362
  id={showBackupEvents}
@@ -366,6 +403,17 @@ const Backups = ({
366
403
  }}
367
404
  />
368
405
  )}
406
+ {showStorageSelector && (
407
+ <MirrorStorageSelectorModal
408
+ mirrors={showStorageSelector.mirrors || []}
409
+ primaryStorage={storage}
410
+ onSelect={(replicationId) => {
411
+ downloadBackup(showStorageSelector.id, replicationId);
412
+ setShowStorageSelector(false);
413
+ }}
414
+ onClose={() => setShowStorageSelector(false)}
415
+ />
416
+ )}
369
417
  </div>
370
418
  );
371
419
  };
@@ -42,6 +42,7 @@ const EditPlan = ({ close, plan }: EditPlanProps) => {
42
42
  <PlanForm
43
43
  title="Edit Plan"
44
44
  type="edit"
45
+ planId={plan.id}
45
46
  planSettings={newPlan}
46
47
  isSubmitting={updatePlanMutation.isPending}
47
48
  storagePath={plan.storagePath}
@@ -0,0 +1,76 @@
1
+ .mirrorStats {
2
+ display: flex;
3
+ flex-direction: row;
4
+ justify-content: space-between;
5
+ padding: 12px 4px;
6
+ }
7
+
8
+ .mirrorList {
9
+ border: 1px solid var(--line-color);
10
+ border-radius: 6px;
11
+ .mirrorItem {
12
+ padding: 12px 20px;
13
+ border-bottom: 1px solid var(--line-color);
14
+
15
+ .mirrorItemContent {
16
+ display: flex;
17
+ flex-direction: row;
18
+ justify-content: space-between;
19
+ }
20
+ img {
21
+ max-width: 14px;
22
+ vertical-align: middle;
23
+ margin-right: 2px;
24
+ }
25
+ .mirrorItemLabel {
26
+ i {
27
+ color: var(--content-text-color-light);
28
+ font-size: 0.7rem;
29
+ font-style: normal;
30
+ margin-left: 5px;
31
+ }
32
+ }
33
+ .mirrorItemStatus {
34
+ .retryButton {
35
+ border-radius: 12px;
36
+ padding: 2px 12px;
37
+ margin-right: 5px;
38
+ border: 1px solid var(--line-active-color);
39
+ cursor: pointer;
40
+ :global(.icon) {
41
+ color: var(--icon-color) !important;
42
+ }
43
+ &:hover {
44
+ background-color: var(--primary-color);
45
+ color: white;
46
+ border-color: var(--primary-color);
47
+ :global(.icon) {
48
+ color: white !important;
49
+ }
50
+ }
51
+ }
52
+ span {
53
+ color: var(--icon-color);
54
+ }
55
+ &.mirrorItemStatus.completed {
56
+ span {
57
+ color: var(--success-text-color);
58
+ }
59
+ }
60
+ &.mirrorItemStatus.failed {
61
+ span {
62
+ color: var(--error-button-color);
63
+ }
64
+ }
65
+ }
66
+ .mirrorItemError {
67
+ width: 100%;
68
+ background-color: var(--error-bg-color);
69
+ color: var(--error-text-color);
70
+ border-radius: 6px;
71
+ padding: 12px;
72
+ box-sizing: border-box;
73
+ margin-top: 12px;
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,100 @@
1
+ import { toast } from 'react-toastify';
2
+ import { Backup, PlanReplicationSettings } from '../../../@types';
3
+ import { useRetryFailedReplications } from '../../../services';
4
+ import { formatBytes, formatDuration } from '../../../utils';
5
+ import Icon from '../../common/Icon/Icon';
6
+ import Modal from '../../common/Modal/Modal';
7
+ import classes from './MirrorDetails.module.scss';
8
+
9
+ interface MirrorDetailsProps {
10
+ mirrors: Backup['mirrors'];
11
+ replicationSettings?: PlanReplicationSettings;
12
+ backupId: string;
13
+ backupTitle?: string;
14
+ planId: string;
15
+ close: () => void;
16
+ }
17
+
18
+ const MirrorDetails = ({ mirrors = [], backupId, backupTitle, planId, close }: MirrorDetailsProps) => {
19
+ const retryReplicationsMutation = useRetryFailedReplications();
20
+
21
+ const retryReplication = (backupId: string, replicationId: string) => {
22
+ toast.promise(retryReplicationsMutation.mutateAsync({ backupId, planId, replicationId }), {
23
+ pending: 'Retrying failed replication...',
24
+ success: 'Replication retry queued!',
25
+ error: {
26
+ render({ data }: any) {
27
+ return `Failed to retry replication. ${data?.message || 'Unknown Error.'}`;
28
+ },
29
+ },
30
+ });
31
+ };
32
+
33
+ return (
34
+ <Modal title={`Mirrors for ${backupTitle ? backupTitle : 'backup-' + backupId}`} closeModal={close} width="560px">
35
+ <div>
36
+ <div className={classes.mirrorList}>
37
+ {mirrors && mirrors.length > 0 ? (
38
+ mirrors.map((mirror, index) => {
39
+ const duration = mirror.ended && mirror.started ? formatDuration((mirror.ended - mirror.started) / 1000) : 'N/A';
40
+ const mirrorSize = mirror.size ? formatBytes(mirror.size) : '';
41
+ const label =
42
+ mirror.status === 'completed'
43
+ ? 'Completed'
44
+ : mirror.status === 'failed'
45
+ ? 'Failed'
46
+ : mirror.status === 'started'
47
+ ? 'In Progress'
48
+ : 'Pending';
49
+ const iconType =
50
+ mirror.status === 'completed'
51
+ ? 'check'
52
+ : mirror.status === 'failed'
53
+ ? 'error'
54
+ : mirror.status === 'started'
55
+ ? 'loading'
56
+ : 'clock';
57
+ return (
58
+ <div key={index} className={classes.mirrorItem}>
59
+ <div className={classes.mirrorItemContent}>
60
+ <div className={classes.mirrorItemLabel}>
61
+ <img src={`/providers/${mirror.storageType}.png`} /> {mirror.storageName}{' '}
62
+ {mirror.status === 'completed' && (
63
+ <>
64
+ {mirrorSize && (
65
+ <i>
66
+ <Icon type="disk" size={12} /> {mirrorSize}
67
+ </i>
68
+ )}{' '}
69
+ <i>
70
+ <Icon type="clock" size={12} /> {duration}
71
+ </i>
72
+ </>
73
+ )}
74
+ </div>
75
+ <div className={`${classes.mirrorItemStatus} ${classes[mirror.status]}`}>
76
+ {mirror.status === 'failed' && (
77
+ <button
78
+ className={classes.retryButton}
79
+ onClick={() => !retryReplicationsMutation.isPending && retryReplication(backupId, mirror.replicationId)}
80
+ >
81
+ <Icon type={'reload'} size={14} /> Retry
82
+ </button>
83
+ )}{' '}
84
+ <Icon type={iconType} size={12} /> {label}
85
+ </div>
86
+ </div>
87
+ {mirror.error && <div className={classes.mirrorItemError}>{mirror.error}</div>}
88
+ </div>
89
+ );
90
+ })
91
+ ) : (
92
+ <div>No mirrors available.</div>
93
+ )}
94
+ </div>
95
+ </div>
96
+ </Modal>
97
+ );
98
+ };
99
+
100
+ export default MirrorDetails;
@@ -0,0 +1,25 @@
1
+ .badge {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: 3px;
5
+ padding: 1px 4px;
6
+ cursor: default;
7
+ white-space: nowrap;
8
+ cursor: pointer;
9
+ }
10
+
11
+ .badgeSuccess {
12
+ color: var(--success-text-color);
13
+ }
14
+
15
+ .badgePartial {
16
+ color: var(--warning-button-color);
17
+ }
18
+
19
+ .badgeFailed {
20
+ color: var(--error-text-color);
21
+ }
22
+
23
+ .badgeProgress {
24
+ color: #7e90c5;
25
+ }
@@ -0,0 +1,65 @@
1
+ import { useState } from 'react';
2
+ import { BackupMirror } from '../../../@types/backups';
3
+ import Icon from '../../common/Icon/Icon';
4
+ import classes from './MirrorStatusBadge.module.scss';
5
+ import MirrorDetails from './MirrorDetails';
6
+ import { PlanReplicationSettings } from '../../../@types';
7
+
8
+ interface MirrorStatusBadgeProps {
9
+ mirrors: BackupMirror[];
10
+ planId: string;
11
+ backupId: string;
12
+ replicationSettings?: PlanReplicationSettings;
13
+ }
14
+
15
+ const MirrorStatusBadge = ({ mirrors, planId, backupId, replicationSettings }: MirrorStatusBadgeProps) => {
16
+ const [showMirrorDetails, setShowMirrorDetails] = useState(false);
17
+ if (!mirrors || mirrors.length === 0) return null;
18
+
19
+ const completed = mirrors.filter((m) => m.status === 'completed').length;
20
+ const failed = mirrors.filter((m) => m.status === 'failed').length;
21
+ const inProgress = mirrors.filter((m) => m.status === 'started' || m.status === 'pending').length;
22
+ const total = mirrors.length;
23
+
24
+ let badgeClass = classes.badge;
25
+ let label: string;
26
+
27
+ if (inProgress > 0) {
28
+ badgeClass += ` ${classes.badgeProgress}`;
29
+ label = 'Replicating...';
30
+ } else if (failed === total) {
31
+ badgeClass += ` ${classes.badgeFailed}`;
32
+ label = 'Replication failed';
33
+ } else if (failed > 0) {
34
+ badgeClass += ` ${classes.badgePartial}`;
35
+ label = `${completed}/${total} mirrors`;
36
+ } else {
37
+ badgeClass += ` ${classes.badgeSuccess}`;
38
+ label = `${total} mirror${total > 1 ? 's' : ''}`;
39
+ }
40
+
41
+ return (
42
+ <>
43
+ <span
44
+ className={badgeClass}
45
+ data-tooltip-id="htmlToolTip"
46
+ data-tooltip-place="top"
47
+ data-tooltip-html={`<div>${label}</div>`}
48
+ onClick={() => setShowMirrorDetails(true)}
49
+ >
50
+ <Icon type={'mirrors'} size={13} />
51
+ </span>
52
+ {showMirrorDetails && (
53
+ <MirrorDetails
54
+ mirrors={mirrors}
55
+ planId={planId}
56
+ backupId={backupId}
57
+ replicationSettings={replicationSettings}
58
+ close={() => setShowMirrorDetails(false)}
59
+ />
60
+ )}
61
+ </>
62
+ );
63
+ };
64
+
65
+ export default MirrorStatusBadge;
@@ -0,0 +1,97 @@
1
+ .mirrorSelector {
2
+ .selectorContent {
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: 8px;
6
+ margin-top: 10px;
7
+ }
8
+
9
+ .storageOption {
10
+ display: flex;
11
+ align-items: center;
12
+ gap: 10px;
13
+ padding: 10px 12px;
14
+ border: 1px solid var(--line-color);
15
+ border-radius: 6px;
16
+ cursor: pointer;
17
+ transition: all 0.12s linear;
18
+ &:hover {
19
+ background-color: var(--primary-color-light);
20
+ }
21
+ }
22
+
23
+ .storageDisabled {
24
+ opacity: 0.5;
25
+ cursor: not-allowed;
26
+ &:hover {
27
+ border-color: var(--line-color);
28
+ background-color: transparent;
29
+ }
30
+ }
31
+
32
+ .radio {
33
+ color: var(--icon-color);
34
+ &.radioSelected {
35
+ color: var(--primary-color);
36
+ }
37
+ }
38
+
39
+ .storageName {
40
+ display: flex;
41
+ align-items: center;
42
+ gap: 6px;
43
+ img {
44
+ max-width: 20px;
45
+ vertical-align: middle;
46
+ }
47
+ .storageLabel {
48
+ font-size: 0.65rem;
49
+ border-radius: 12px;
50
+ padding: 1px 10px;
51
+ background-color: var(--background-color);
52
+ }
53
+ }
54
+
55
+ .statusLabel {
56
+ margin-left: auto;
57
+ font-size: 0.6875rem;
58
+ padding: 1px 8px;
59
+ border-radius: 12px;
60
+ text-transform: capitalize;
61
+ background-color: var(--line-color);
62
+ color: var(--content-text-color-light);
63
+ }
64
+
65
+ .noMirrors {
66
+ padding: 15px;
67
+ text-align: center;
68
+ color: var(--content-text-color-light);
69
+ font-size: 0.8125rem;
70
+ }
71
+
72
+ &.dropdown {
73
+ position: relative;
74
+ .dropdownLabel {
75
+ padding: 6px 10px;
76
+ border: 1px solid var(--line-color);
77
+ border-radius: 6px;
78
+ cursor: pointer;
79
+ min-width: 160px;
80
+ }
81
+ .selectorContent {
82
+ display: none;
83
+ position: absolute;
84
+ margin-top: 0px;
85
+ right: 0;
86
+ min-width: 260px;
87
+ z-index: 99;
88
+ padding: 10px;
89
+ background-color: var(--content-background-color);
90
+ border: 1px solid var(--line-color);
91
+ border-radius: 6px;
92
+ &.showOptions {
93
+ display: block;
94
+ }
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,70 @@
1
+ import { useState } from 'react';
2
+ import { BackupMirror } from '../../../@types/backups';
3
+ import Icon from '../../common/Icon/Icon';
4
+ import classes from './MirrorStorageSelector.module.scss';
5
+
6
+ interface MirrorStorageSelectorProps {
7
+ mirrors: BackupMirror[];
8
+ primaryStorage: { id: string; type: string; name: string };
9
+ onChange: (replicationId?: string) => void;
10
+ selectedReplicationId?: string;
11
+ }
12
+
13
+ const MirrorStorageSelector = ({ mirrors, primaryStorage, onChange, selectedReplicationId }: MirrorStorageSelectorProps) => {
14
+ const [selected, setSelected] = useState<string | undefined>(selectedReplicationId);
15
+ const completedMirrors = mirrors.filter((m) => m.status === 'completed');
16
+
17
+ return (
18
+ <div className={classes.mirrorSelector}>
19
+ <div className={classes.selectorContent}>
20
+ <div
21
+ className={classes.storageOption}
22
+ onClick={() => {
23
+ setSelected(undefined);
24
+ onChange(undefined);
25
+ }}
26
+ >
27
+ <span className={`${classes.radio} ${selected === undefined ? classes.radioSelected : ''}`}>
28
+ <Icon type={selected === undefined ? 'check-circle-filled' : 'check-circle'} size={16} />
29
+ </span>
30
+ <span className={classes.storageName}>
31
+ <img src={`/providers/${primaryStorage.type}.png`} />
32
+ {primaryStorage.name}
33
+ <span className={classes.storageLabel}>Primary</span>
34
+ </span>
35
+ </div>
36
+ {mirrors.map((mirror) => {
37
+ const isCompleted = mirror.status === 'completed';
38
+ const isSelected = selected === mirror.replicationId;
39
+ return (
40
+ <div
41
+ key={mirror.replicationId}
42
+ className={`${classes.storageOption} ${!isCompleted ? classes.storageDisabled : ''}`}
43
+ onClick={() => {
44
+ if (isCompleted) {
45
+ setSelected(mirror.replicationId);
46
+ onChange(mirror.replicationId);
47
+ }
48
+ }}
49
+ >
50
+ <span className={`${classes.radio} ${isSelected ? classes.radioSelected : ''}`}>
51
+ <Icon type={isSelected ? 'check-circle-filled' : 'check-circle'} size={16} />
52
+ </span>
53
+
54
+ <span className={classes.storageName}>
55
+ <img src={`/providers/${mirror.storageType}.png`} /> {mirror.storageName}
56
+ <span className={classes.storageLabel}>Mirror</span>
57
+ </span>
58
+ {!isCompleted && <span className={classes.statusLabel}>{mirror.status}</span>}
59
+ </div>
60
+ );
61
+ })}
62
+ {completedMirrors.length === 0 && (
63
+ <div className={classes.noMirrors}>No completed mirrors available. Only the primary storage can be used.</div>
64
+ )}
65
+ </div>
66
+ </div>
67
+ );
68
+ };
69
+
70
+ export default MirrorStorageSelector;
@@ -0,0 +1,40 @@
1
+ import { useState } from 'react';
2
+ import { BackupMirror } from '../../../@types/backups';
3
+ import ActionModal from '../../common/ActionModal/ActionModal';
4
+ import MirrorStorageSelector from './MirrorStorageSelector';
5
+
6
+ interface MirrorStorageSelectorProps {
7
+ mirrors: BackupMirror[];
8
+ primaryStorage: { id: string; type: string; name: string };
9
+ onSelect: (replicationId?: string) => void;
10
+ onClose: () => void;
11
+ actionLabel?: string;
12
+ }
13
+
14
+ const MirrorStorageSelectorModal = ({ mirrors, primaryStorage, onSelect, onClose, actionLabel = 'Download' }: MirrorStorageSelectorProps) => {
15
+ const [selected, setSelected] = useState<string | undefined>(undefined);
16
+
17
+ return (
18
+ <ActionModal
19
+ title={`Choose a Mirror to ${actionLabel} From`}
20
+ message={
21
+ <MirrorStorageSelector
22
+ mirrors={mirrors}
23
+ primaryStorage={primaryStorage}
24
+ onChange={(replicationId) => replicationId && setSelected(replicationId)}
25
+ />
26
+ }
27
+ closeModal={onClose}
28
+ width="450px"
29
+ primaryAction={{
30
+ title: actionLabel,
31
+ type: 'default',
32
+ isPending: false,
33
+ icon: 'download',
34
+ action: () => onSelect(selected),
35
+ }}
36
+ />
37
+ );
38
+ };
39
+
40
+ export default MirrorStorageSelectorModal;
@@ -14,7 +14,7 @@ const PlanBackups = ({ plan }: PlanBackupsProps) => {
14
14
  const AllBackups = useComponentOverride('Backups', Backups);
15
15
  const [historyTab, setHistoryTab] = useState<'backups' | 'restores'>('backups');
16
16
 
17
- const { backups = [], stats, restores = [], method, sourceId, sourceType, settings } = plan;
17
+ const { backups = [], stats, restores = [], method, sourceId, sourceType, settings, storage } = plan;
18
18
 
19
19
  const sortedHistory = [...(backups || [])].sort((a, b) => new Date(b.started).getTime() - new Date(a.started).getTime());
20
20
  const isSync = method === 'sync';
@@ -44,7 +44,10 @@ const PlanBackups = ({ plan }: PlanBackupsProps) => {
44
44
  method={method}
45
45
  sourceId={sourceId}
46
46
  sourceType={sourceType}
47
+ deviceId={plan.device.id}
48
+ storage={storage}
47
49
  snapLimit={settings.prune.snapCount}
50
+ replicationSettings={settings.replication}
48
51
  />
49
52
  )}
50
53
  {historyTab === 'restores' && <Restores restores={restores} planId={plan.id} method={method} sourceId={sourceId} sourceType={sourceType} />}
@@ -15,6 +15,7 @@ import IntervalField from '../../common/form/IntervalField/IntervalField';
15
15
  import PlanFormNav from './PlanFormNav';
16
16
  import { useGetDevice } from '../../../services/devices';
17
17
  import PlanPruneSettings from '../PlanSettings/PlanPruneSettings';
18
+ import PlanReplicationSettings from '../PlanSettings/PlanReplicationSettings';
18
19
 
19
20
  type PlanFormProps = {
20
21
  title: string;
@@ -26,9 +27,21 @@ type PlanFormProps = {
26
27
  close: () => void;
27
28
  storagePath?: string;
28
29
  storageId?: string;
30
+ planId?: string;
29
31
  };
30
32
 
31
- const PlanForm = ({ title, planSettings, type, onPlanSettingsChange, onSubmit, isSubmitting, close, storagePath, storageId }: PlanFormProps) => {
33
+ const PlanForm = ({
34
+ title,
35
+ planSettings,
36
+ type,
37
+ onPlanSettingsChange,
38
+ onSubmit,
39
+ isSubmitting,
40
+ close,
41
+ storagePath,
42
+ storageId,
43
+ planId,
44
+ }: PlanFormProps) => {
32
45
  const [step, setStep] = useState<number>(1);
33
46
 
34
47
  const { data: settingsData } = useGetSettings();
@@ -208,8 +221,8 @@ const PlanForm = ({ title, planSettings, type, onPlanSettingsChange, onSubmit, i
208
221
  <label className={classes.label}>Backup Destination*</label>
209
222
  {!planSettings.storage.name && <span className={classes.fieldErrorLabel}>{'Required'}</span>}
210
223
  <StoragePicker
211
- storagePath={storagePath}
212
- storageId={storageId}
224
+ storagePath={storagePath || planSettings.storagePath}
225
+ storageId={storageId || planSettings.storage?.id || ''}
213
226
  deviceId={planSettings.sourceId || 'main'}
214
227
  disabled={type === 'edit' ? true : false}
215
228
  onUpdate={(s: { storage: { name: string; id: string; type: string }; path: string }) =>
@@ -221,6 +234,20 @@ const PlanForm = ({ title, planSettings, type, onPlanSettingsChange, onSubmit, i
221
234
  }
222
235
  />
223
236
  </div>
237
+ <PlanReplicationSettings
238
+ replication={planSettings.settings.replication}
239
+ primaryStorageId={planSettings.storage.id}
240
+ primaryStoragePath={planSettings.storagePath}
241
+ deviceId={planSettings.sourceId || 'main'}
242
+ isEditing={type === 'edit' ? true : false}
243
+ planID={planId}
244
+ onUpdate={(replication) =>
245
+ onPlanSettingsChange({
246
+ ...planSettings,
247
+ settings: { ...planSettings.settings, replication: replication },
248
+ })
249
+ }
250
+ />
224
251
  </div>
225
252
  )}
226
253
  {step === 3 && (