@plutonhq/core-frontend 0.1.13 → 0.1.15

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 (217) 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 +121 -94
  58. package/dist-lib/components/Plan/PlanForm/PlanForm.js.map +1 -1
  59. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.d.ts +16 -0
  60. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.d.ts.map +1 -0
  61. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.js +115 -0
  62. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.js.map +1 -0
  63. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.module.scss.js +26 -0
  64. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.module.scss.js.map +1 -0
  65. package/dist-lib/components/Plan/PlanItems/PlanItem.d.ts.map +1 -1
  66. package/dist-lib/components/Plan/PlanItems/PlanItem.js +58 -59
  67. package/dist-lib/components/Plan/PlanItems/PlanItem.js.map +1 -1
  68. package/dist-lib/components/Plan/PlanPendingBackup/PlanPendingBackup.d.ts +2 -2
  69. package/dist-lib/components/Plan/PlanPendingBackup/PlanPendingBackup.d.ts.map +1 -1
  70. package/dist-lib/components/Plan/PlanPendingBackup/PlanPendingBackup.js.map +1 -1
  71. package/dist-lib/components/Plan/PlanRemoveModal/PlanRemoveModal.js +8 -8
  72. package/dist-lib/components/Plan/PlanRemoveModal/PlanRemoveModal.js.map +1 -1
  73. package/dist-lib/components/Plan/PlanSettings/PlanReplicationSettings.d.ts +14 -0
  74. package/dist-lib/components/Plan/PlanSettings/PlanReplicationSettings.d.ts.map +1 -0
  75. package/dist-lib/components/Plan/PlanSettings/PlanReplicationSettings.js +290 -0
  76. package/dist-lib/components/Plan/PlanSettings/PlanReplicationSettings.js.map +1 -0
  77. package/dist-lib/components/Plan/PlanSettings/PlanReplicationSettings.module.scss.js +26 -0
  78. package/dist-lib/components/Plan/PlanSettings/PlanReplicationSettings.module.scss.js.map +1 -0
  79. package/dist-lib/components/Plan/PlanStats/PlanStats.d.ts.map +1 -1
  80. package/dist-lib/components/Plan/PlanStats/PlanStats.js +41 -42
  81. package/dist-lib/components/Plan/PlanStats/PlanStats.js.map +1 -1
  82. package/dist-lib/components/Plan/PlanStats/PlanStats.module.scss.js +5 -5
  83. package/dist-lib/components/Plan/PlanStorageInfo/PlanStorageInfo.d.ts +15 -0
  84. package/dist-lib/components/Plan/PlanStorageInfo/PlanStorageInfo.d.ts.map +1 -0
  85. package/dist-lib/components/Plan/PlanStorageInfo/PlanStorageInfo.js +69 -0
  86. package/dist-lib/components/Plan/PlanStorageInfo/PlanStorageInfo.js.map +1 -0
  87. package/dist-lib/components/Plan/PlanStorageInfo/PlanStorageInfo.module.scss.js +16 -0
  88. package/dist-lib/components/Plan/PlanStorageInfo/PlanStorageInfo.module.scss.js.map +1 -0
  89. package/dist-lib/components/Restore/RestoreWizard/RestoreConfirmStep.d.ts.map +1 -1
  90. package/dist-lib/components/Restore/RestoreWizard/RestoreConfirmStep.js +36 -34
  91. package/dist-lib/components/Restore/RestoreWizard/RestoreConfirmStep.js.map +1 -1
  92. package/dist-lib/components/Restore/RestoreWizard/RestorePreviewStep.d.ts.map +1 -1
  93. package/dist-lib/components/Restore/RestoreWizard/RestorePreviewStep.js +7 -5
  94. package/dist-lib/components/Restore/RestoreWizard/RestorePreviewStep.js.map +1 -1
  95. package/dist-lib/components/Restore/RestoreWizard/RestoreSettingsStep.d.ts +12 -4
  96. package/dist-lib/components/Restore/RestoreWizard/RestoreSettingsStep.d.ts.map +1 -1
  97. package/dist-lib/components/Restore/RestoreWizard/RestoreSettingsStep.js +44 -32
  98. package/dist-lib/components/Restore/RestoreWizard/RestoreSettingsStep.js.map +1 -1
  99. package/dist-lib/components/Restore/RestoreWizard/RestoreWizard.d.ts +5 -1
  100. package/dist-lib/components/Restore/RestoreWizard/RestoreWizard.d.ts.map +1 -1
  101. package/dist-lib/components/Restore/RestoreWizard/RestoreWizard.js +48 -44
  102. package/dist-lib/components/Restore/RestoreWizard/RestoreWizard.js.map +1 -1
  103. package/dist-lib/components/Restore/RestoreWizard/RestoreWizard.module.scss.js +32 -32
  104. package/dist-lib/components/Settings/GeneralSettings/GeneralSettings.d.ts +1 -1
  105. package/dist-lib/components/Settings/GeneralSettings/GeneralSettings.d.ts.map +1 -1
  106. package/dist-lib/components/Settings/GeneralSettings/GeneralSettings.js +52 -24
  107. package/dist-lib/components/Settings/GeneralSettings/GeneralSettings.js.map +1 -1
  108. package/dist-lib/components/Settings/IntegrationSettings/IntegrationSettings.d.ts.map +1 -1
  109. package/dist-lib/components/Settings/IntegrationSettings/IntegrationSettings.js +28 -19
  110. package/dist-lib/components/Settings/IntegrationSettings/IntegrationSettings.js.map +1 -1
  111. package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.d.ts +7 -0
  112. package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.d.ts.map +1 -0
  113. package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.js +79 -0
  114. package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.js.map +1 -0
  115. package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.module.scss.js +24 -0
  116. package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.module.scss.js.map +1 -0
  117. package/dist-lib/components/common/Icon/Icon.d.ts.map +1 -1
  118. package/dist-lib/components/common/Icon/Icon.js +11 -0
  119. package/dist-lib/components/common/Icon/Icon.js.map +1 -1
  120. package/dist-lib/components/common/PageHeader/PageHeader.module.scss.js +6 -6
  121. package/dist-lib/components/index.d.ts +6 -0
  122. package/dist-lib/components/index.d.ts.map +1 -1
  123. package/dist-lib/components.js +102 -90
  124. package/dist-lib/components.js.map +1 -1
  125. package/dist-lib/hooks/usePwaAutoUpdate.d.ts +11 -2
  126. package/dist-lib/hooks/usePwaAutoUpdate.d.ts.map +1 -1
  127. package/dist-lib/hooks/usePwaAutoUpdate.js +32 -10
  128. package/dist-lib/hooks/usePwaAutoUpdate.js.map +1 -1
  129. package/dist-lib/router.d.ts.map +1 -1
  130. package/dist-lib/router.js +46 -35
  131. package/dist-lib/router.js.map +1 -1
  132. package/dist-lib/routes/DeviceSingle/DeviceSingle.d.ts.map +1 -1
  133. package/dist-lib/routes/DeviceSingle/DeviceSingle.js +40 -40
  134. package/dist-lib/routes/DeviceSingle/DeviceSingle.js.map +1 -1
  135. package/dist-lib/routes/PlanSingle/PlanSingle.d.ts.map +1 -1
  136. package/dist-lib/routes/PlanSingle/PlanSingle.js +123 -98
  137. package/dist-lib/routes/PlanSingle/PlanSingle.js.map +1 -1
  138. package/dist-lib/services/backups.d.ts +15 -2
  139. package/dist-lib/services/backups.d.ts.map +1 -1
  140. package/dist-lib/services/backups.js +119 -100
  141. package/dist-lib/services/backups.js.map +1 -1
  142. package/dist-lib/services/plans.d.ts +20 -0
  143. package/dist-lib/services/plans.d.ts.map +1 -1
  144. package/dist-lib/services/plans.js +227 -172
  145. package/dist-lib/services/plans.js.map +1 -1
  146. package/dist-lib/services/restores.d.ts +10 -2
  147. package/dist-lib/services/restores.d.ts.map +1 -1
  148. package/dist-lib/services/restores.js +61 -57
  149. package/dist-lib/services/restores.js.map +1 -1
  150. package/dist-lib/services/settings.d.ts +16 -0
  151. package/dist-lib/services/settings.d.ts.map +1 -1
  152. package/dist-lib/services/settings.js +147 -68
  153. package/dist-lib/services/settings.js.map +1 -1
  154. package/dist-lib/services/users.d.ts.map +1 -1
  155. package/dist-lib/services/users.js +32 -32
  156. package/dist-lib/services/users.js.map +1 -1
  157. package/dist-lib/services.js +113 -101
  158. package/dist-lib/styles/core-frontend.css +1 -1
  159. package/dist-lib/utils/progressHelpers.d.ts +12 -1
  160. package/dist-lib/utils/progressHelpers.d.ts.map +1 -1
  161. package/dist-lib/utils/progressHelpers.js +121 -63
  162. package/dist-lib/utils/progressHelpers.js.map +1 -1
  163. package/dist-lib/utils.js +29 -28
  164. package/package.json +1 -1
  165. package/src/@types/backups.ts +28 -0
  166. package/src/@types/devices.ts +8 -0
  167. package/src/@types/plans.ts +23 -1
  168. package/src/@types/restores.ts +2 -0
  169. package/src/components/Device/DeviceBackups/DeviceBackups.tsx +11 -36
  170. package/src/components/Plan/BackupEvents/BackupEvents.module.scss +65 -0
  171. package/src/components/Plan/BackupEvents/BackupEvents.tsx +65 -4
  172. package/src/components/Plan/BackupProgress/BackupProgress.module.scss +121 -3
  173. package/src/components/Plan/BackupProgress/BackupProgress.tsx +149 -71
  174. package/src/components/Plan/Backups/Backups.tsx +52 -4
  175. package/src/components/Plan/EditPlan/EditPlan.tsx +1 -0
  176. package/src/components/Plan/Mirrors/MirrorDetails.module.scss +76 -0
  177. package/src/components/Plan/Mirrors/MirrorDetails.tsx +100 -0
  178. package/src/components/Plan/Mirrors/MirrorStatusBadge.module.scss +25 -0
  179. package/src/components/Plan/Mirrors/MirrorStatusBadge.tsx +65 -0
  180. package/src/components/Plan/Mirrors/MirrorStorageSelector.module.scss +97 -0
  181. package/src/components/Plan/Mirrors/MirrorStorageSelector.tsx +70 -0
  182. package/src/components/Plan/Mirrors/MirrorStorageSelectorModal.tsx +40 -0
  183. package/src/components/Plan/PlanBackups/PlanBackups.tsx +4 -1
  184. package/src/components/Plan/PlanForm/PlanForm.tsx +44 -17
  185. package/src/components/Plan/PlanIntegrity/PlanIntegrity.module.scss +110 -0
  186. package/src/components/Plan/PlanIntegrity/PlanIntegrity.tsx +187 -0
  187. package/src/components/Plan/PlanItems/PlanItem.tsx +3 -3
  188. package/src/components/Plan/PlanPendingBackup/PlanPendingBackup.tsx +2 -2
  189. package/src/components/Plan/PlanRemoveModal/PlanRemoveModal.tsx +1 -1
  190. package/src/components/Plan/PlanSettings/PlanReplicationSettings.module.scss +105 -0
  191. package/src/components/Plan/PlanSettings/PlanReplicationSettings.tsx +334 -0
  192. package/src/components/Plan/PlanStats/PlanStats.module.scss +1 -1
  193. package/src/components/Plan/PlanStats/PlanStats.tsx +8 -8
  194. package/src/components/Plan/PlanStorageInfo/PlanStorageInfo.module.scss +43 -0
  195. package/src/components/Plan/PlanStorageInfo/PlanStorageInfo.tsx +83 -0
  196. package/src/components/Restore/RestoreWizard/RestoreConfirmStep.tsx +2 -0
  197. package/src/components/Restore/RestoreWizard/RestorePreviewStep.tsx +2 -0
  198. package/src/components/Restore/RestoreWizard/RestoreSettingsStep.tsx +36 -13
  199. package/src/components/Restore/RestoreWizard/RestoreWizard.module.scss +4 -0
  200. package/src/components/Restore/RestoreWizard/RestoreWizard.tsx +9 -1
  201. package/src/components/Settings/GeneralSettings/GeneralSettings.tsx +38 -2
  202. package/src/components/Settings/IntegrationSettings/IntegrationSettings.tsx +9 -2
  203. package/src/components/Settings/TwoFactorSetup/TwoFactorSetup.module.scss +62 -0
  204. package/src/components/Settings/TwoFactorSetup/TwoFactorSetup.tsx +102 -0
  205. package/src/components/common/Icon/Icon.tsx +10 -1
  206. package/src/components/common/PageHeader/PageHeader.module.scss +3 -0
  207. package/src/components/index.ts +8 -0
  208. package/src/hooks/usePwaAutoUpdate.ts +51 -11
  209. package/src/router.tsx +26 -17
  210. package/src/routes/DeviceSingle/DeviceSingle.tsx +3 -3
  211. package/src/routes/PlanSingle/PlanSingle.tsx +21 -0
  212. package/src/services/backups.ts +32 -9
  213. package/src/services/plans.ts +75 -0
  214. package/src/services/restores.ts +10 -2
  215. package/src/services/settings.ts +90 -0
  216. package/src/services/users.ts +14 -5
  217. package/src/utils/progressHelpers.ts +85 -1
@@ -0,0 +1,334 @@
1
+ import { useState, useCallback } from 'react';
2
+ import { PlanReplicationSettings as ReplicationSettings, PlanReplicationStorage } from '../../../@types/plans';
3
+ import Icon from '../../common/Icon/Icon';
4
+ import StoragePicker from '../../common/form/StoragePicker/StoragePicker';
5
+ import Toggle from '../../common/form/Toggle/Toggle';
6
+ import classes from '../../Plan/AddPlan/AddPlan.module.scss';
7
+ import mirrorClasses from './PlanReplicationSettings.module.scss';
8
+ import ActionModal from '../../common/ActionModal/ActionModal';
9
+ import { useDeleteReplicationStorage } from '../../../services';
10
+ import { toast } from 'react-toastify';
11
+ import { nanoid } from 'nanoid';
12
+
13
+ interface PlanReplicationSettingsProps {
14
+ replication?: ReplicationSettings;
15
+ primaryStorageId: string;
16
+ primaryStoragePath?: string;
17
+ deviceId: string;
18
+ isEditing?: boolean;
19
+ planID?: string;
20
+ maxReplications?: number;
21
+ onUpdate: (replication: ReplicationSettings) => void;
22
+ }
23
+
24
+ const defaultReplication: ReplicationSettings = {
25
+ enabled: false,
26
+ concurrent: false,
27
+ storages: [],
28
+ };
29
+
30
+ const MAX_REPLICATIONS = 2;
31
+
32
+ const PlanReplicationSettings = ({
33
+ replication,
34
+ primaryStorageId,
35
+ primaryStoragePath,
36
+ deviceId,
37
+ isEditing = false,
38
+ planID,
39
+ maxReplications: maxReplicationsProp,
40
+ onUpdate,
41
+ }: PlanReplicationSettingsProps) => {
42
+ const [tempStorages, setTempStorages] = useState<PlanReplicationStorage[]>([]);
43
+ const [removeStorageData, setRemoveStorageData] = useState<boolean>(false);
44
+ const [showRemoveModal, setShowRemoveModal] = useState<boolean | PlanReplicationStorage>(false);
45
+
46
+ const deleteStorageMutation = useDeleteReplicationStorage();
47
+ const settings = replication || defaultReplication;
48
+
49
+ /** Check if a storage+path combination already exists in committed or temp storages */
50
+ const isDuplicateStorage = useCallback(
51
+ (storageId: string, storagePath: string, excludeCommittedIndex?: number, excludeTempIndex?: number) => {
52
+ // Skip duplicate check if storageId or storagePath is empty (incomplete selection)
53
+ if (!storageId || !storagePath) return false;
54
+ const committedDuplicate = settings.storages.some(
55
+ (s, i) =>
56
+ (excludeCommittedIndex === undefined || i !== excludeCommittedIndex) && s.storageId === storageId && s.storagePath === storagePath,
57
+ );
58
+ const tempDuplicate = tempStorages.some(
59
+ (s, i) => (excludeTempIndex === undefined || i !== excludeTempIndex) && s.storageId === storageId && s.storagePath === storagePath,
60
+ );
61
+ return committedDuplicate || tempDuplicate;
62
+ },
63
+ [settings.storages, tempStorages],
64
+ );
65
+
66
+ const updateEnabled = (enabled: boolean) => {
67
+ onUpdate({ ...settings, enabled });
68
+ };
69
+
70
+ const updateConcurrent = (concurrent: boolean) => {
71
+ onUpdate({ ...settings, concurrent });
72
+ };
73
+
74
+ const addReplicationStorage = () => {
75
+ if (isEditing) {
76
+ const maxReplications = maxReplicationsProp ?? MAX_REPLICATIONS;
77
+ if (settings.storages.length + tempStorages.length >= maxReplications) return;
78
+ setTempStorages([...tempStorages, { replicationId: nanoid(), storageId: '', storagePath: '', storageType: '', addedAt: Date.now() }]);
79
+ } else {
80
+ const maxReplications = maxReplicationsProp ?? MAX_REPLICATIONS;
81
+ if (settings.storages.length >= maxReplications) return;
82
+ onUpdate({
83
+ ...settings,
84
+ storages: [...settings.storages, { replicationId: nanoid(), storageId: '', storagePath: '', storageType: '', addedAt: Date.now() }],
85
+ });
86
+ }
87
+ };
88
+
89
+ const updateTempStorage = (index: number, storage: PlanReplicationStorage) => {
90
+ setTempStorages((prev) => {
91
+ const updated = [...prev];
92
+ updated[index] = storage;
93
+ return updated;
94
+ });
95
+ };
96
+
97
+ const confirmAddStorage = (index: number) => {
98
+ const storage = tempStorages[index];
99
+ if (!storage.storageId) return;
100
+ if (storage.storageId === primaryStorageId && storage.storagePath === primaryStoragePath) {
101
+ toast.error('Cannot replicate to the same storage as the primary backup.');
102
+ return;
103
+ }
104
+ if (isDuplicateStorage(storage.storageId, storage.storagePath, undefined, index)) {
105
+ toast.error('This storage destination is already added.');
106
+ return;
107
+ }
108
+ setTempStorages((prev) => prev.filter((_, i) => i !== index));
109
+ onUpdate({ ...settings, storages: [...settings.storages, storage] });
110
+ };
111
+
112
+ const removeTempStorage = (index: number) => {
113
+ setTempStorages((prev) => prev.filter((_, i) => i !== index));
114
+ };
115
+
116
+ const updateReplicationStorage = (index: number, storage: PlanReplicationStorage) => {
117
+ const updated = [...settings.storages];
118
+ updated[index] = storage;
119
+ onUpdate({ ...settings, storages: updated });
120
+ };
121
+
122
+ const removeAddedStorage = (index: number) => {
123
+ const updated = settings.storages.filter((_, i) => i !== index);
124
+ onUpdate({ ...settings, storages: updated });
125
+ };
126
+
127
+ const removeReplicationStorage = () => {
128
+ if (typeof showRemoveModal === 'object' && planID) {
129
+ const storageToRemove = showRemoveModal;
130
+ deleteStorageMutation.mutate(
131
+ {
132
+ planID: planID,
133
+ storageID: storageToRemove.storageId,
134
+ removeData: removeStorageData,
135
+ storagePath: storageToRemove.storagePath,
136
+ replicationId: storageToRemove.replicationId,
137
+ },
138
+ {
139
+ onSuccess: () => {
140
+ // Remove the storage from local state and close modal
141
+ const updated = settings.storages.filter((s) => s.replicationId !== storageToRemove.replicationId);
142
+ onUpdate({ ...settings, storages: updated, ...(updated.length === 0 && { enabled: false }) });
143
+ setShowRemoveModal(false);
144
+ setRemoveStorageData(false);
145
+ deleteStorageMutation.reset();
146
+ },
147
+ },
148
+ );
149
+ }
150
+ };
151
+
152
+ const openRemoveModal = (mirror: PlanReplicationStorage) => {
153
+ setRemoveStorageData(false);
154
+ deleteStorageMutation.reset();
155
+ setShowRemoveModal(mirror);
156
+ };
157
+
158
+ return (
159
+ <div className={classes.field}>
160
+ <div className={mirrorClasses.mirrorSection}>
161
+ <Toggle
162
+ label="Enable Replication"
163
+ description="Replicate backups to additional storage destinations for redundancy (3-2-1 backup strategy)"
164
+ fieldValue={settings.enabled}
165
+ onUpdate={updateEnabled}
166
+ />
167
+
168
+ {settings.enabled && (
169
+ <div className={mirrorClasses.mirrorSettings}>
170
+ {settings.storages.map((mirror, index) => (
171
+ <div key={mirror.addedAt || index} className={mirrorClasses.mirrorDestination}>
172
+ <div className={mirrorClasses.mirrorHeader}>
173
+ <label className={classes.label}>Replication Destination {index + 1}</label>
174
+ <div className={mirrorClasses.mirrorHeaderRight}>
175
+ {settings.storages.length > 1 && (
176
+ <div className={mirrorClasses.storagePositionControls}>
177
+ <button
178
+ title="Move Up"
179
+ disabled={index === 0}
180
+ onClick={() => {
181
+ const updatedStorages = [...(settings.storages || [])];
182
+ const [movedStorage] = updatedStorages.splice(index, 1);
183
+ updatedStorages.splice(index - 1, 0, movedStorage);
184
+ onUpdate({ ...settings, storages: updatedStorages });
185
+ }}
186
+ >
187
+ <Icon type={'caret-up'} size={14} />
188
+ </button>
189
+ <button
190
+ title="Move Down"
191
+ disabled={index === settings.storages.length - 1}
192
+ onClick={() => {
193
+ const updatedStorages = [...(settings.storages || [])];
194
+ const [movedStorage] = updatedStorages.splice(index, 1);
195
+ updatedStorages.splice(index + 1, 0, movedStorage);
196
+ onUpdate({ ...settings, storages: updatedStorages });
197
+ }}
198
+ >
199
+ <Icon type={'caret-down'} size={14} />
200
+ </button>
201
+ </div>
202
+ )}
203
+ <button
204
+ className={mirrorClasses.removeBtn}
205
+ onClick={() => (isEditing ? openRemoveModal(mirror) : removeAddedStorage(index))}
206
+ title="Remove replication destination"
207
+ >
208
+ <Icon type="trash" size={14} /> Remove
209
+ </button>
210
+ </div>
211
+ </div>
212
+ <StoragePicker
213
+ storagePath={mirror.storagePath}
214
+ storageId={mirror.storageId}
215
+ deviceId={deviceId}
216
+ disabled={isEditing}
217
+ onUpdate={(s) => {
218
+ if (s.storage.id === primaryStorageId && s.path === primaryStoragePath) {
219
+ toast.error('Cannot replicate to the same storage as the primary backup.');
220
+ return;
221
+ }
222
+ updateReplicationStorage(index, {
223
+ replicationId: mirror.replicationId,
224
+ storageId: s.storage.id,
225
+ storagePath: s.path,
226
+ storageType: s.storage.type,
227
+ storageName: s.storage.name,
228
+ addedAt: mirror.addedAt,
229
+ });
230
+ }}
231
+ />
232
+ </div>
233
+ ))}
234
+
235
+ {isEditing &&
236
+ tempStorages.map((tempStorage, index) => (
237
+ <div key={tempStorage.addedAt} className={mirrorClasses.mirrorDestination}>
238
+ <div className={mirrorClasses.mirrorHeader}>
239
+ <label className={classes.label}>Replication Destination {settings.storages.length + index + 1}</label>
240
+ <div className={mirrorClasses.mirrorHeaderRight}>
241
+ <button
242
+ className={mirrorClasses.confirmBtn}
243
+ onClick={() => confirmAddStorage(index)}
244
+ disabled={!tempStorage.storageId}
245
+ title={!tempStorage.storageId ? 'Select Storage Type to add the storage' : 'Confirm replication destination'}
246
+ >
247
+ <Icon type="check" size={11} /> Confirm
248
+ </button>
249
+ <button
250
+ className={mirrorClasses.removeBtn}
251
+ onClick={() => removeTempStorage(index)}
252
+ title="Remove replication destination"
253
+ >
254
+ <Icon type="trash" size={14} /> Remove
255
+ </button>
256
+ </div>
257
+ </div>
258
+ <StoragePicker
259
+ storagePath={tempStorage.storagePath}
260
+ storageId={tempStorage.storageId}
261
+ deviceId={deviceId}
262
+ disabled={false}
263
+ onUpdate={(s) => {
264
+ updateTempStorage(index, {
265
+ replicationId: tempStorage.replicationId,
266
+ storageId: s.storage.id,
267
+ storagePath: s.path,
268
+ storageType: s.storage.type,
269
+ storageName: s.storage.name,
270
+ addedAt: tempStorage.addedAt,
271
+ });
272
+ }}
273
+ />
274
+ </div>
275
+ ))}
276
+
277
+ {settings.storages.length + (isEditing ? tempStorages.length : 0) < (maxReplicationsProp ?? MAX_REPLICATIONS) && (
278
+ <button className={mirrorClasses.addMirrorBtn} onClick={addReplicationStorage}>
279
+ <Icon type="plus" size={12} /> + Add Replication Destination
280
+ </button>
281
+ )}
282
+
283
+ {settings.storages.length > 1 && (
284
+ <div className={mirrorClasses.concurrentOption}>
285
+ <Toggle
286
+ label="Run replications concurrently"
287
+ description="Run replications in parallel to speed up the backup process"
288
+ fieldValue={settings.concurrent}
289
+ onUpdate={updateConcurrent}
290
+ />
291
+ </div>
292
+ )}
293
+ </div>
294
+ )}
295
+ </div>
296
+
297
+ {showRemoveModal && (
298
+ <ActionModal
299
+ title="Remove Replication Storage"
300
+ message={
301
+ <div>
302
+ <p>Are you sure you want to remove this Replication storage from this plan?</p>
303
+ <Toggle
304
+ fieldValue={removeStorageData}
305
+ onUpdate={() => setRemoveStorageData((prev) => !prev)}
306
+ description={`Remove replicated data from the Storage`}
307
+ customClasses={classes.removeRemoteToggle}
308
+ />
309
+ </div>
310
+ }
311
+ closeModal={() => {
312
+ if (!deleteStorageMutation.isPending) {
313
+ setShowRemoveModal(false);
314
+ setRemoveStorageData(false);
315
+ deleteStorageMutation.reset();
316
+ }
317
+ }}
318
+ width="400px"
319
+ errorMessage={deleteStorageMutation.isError && 'Error Removing Replication Storage!'}
320
+ successMessage={deleteStorageMutation.isSuccess && 'Removed Replication Storage Successfully!'}
321
+ primaryAction={{
322
+ title: 'Yes, Remove Storage',
323
+ type: 'danger',
324
+ icon: 'trash',
325
+ isPending: deleteStorageMutation.isPending,
326
+ action: () => removeReplicationStorage(),
327
+ }}
328
+ />
329
+ )}
330
+ </div>
331
+ );
332
+ };
333
+
334
+ export default PlanReplicationSettings;
@@ -109,7 +109,7 @@
109
109
  }
110
110
  }
111
111
  .health {
112
- width: 38%;
112
+ width: 33%;
113
113
  }
114
114
  }
115
115
 
@@ -3,6 +3,7 @@ import { Plan } from '../../../@types/plans';
3
3
  import { formatBytes, formatNumberToK } from '../../../utils/helpers';
4
4
  import PlanHistory from '../PlanHistory/PlanHistory';
5
5
  import classes from './PlanStats.module.scss';
6
+ import PlanStorageInfo from '../PlanStorageInfo/PlanStorageInfo';
6
7
 
7
8
  interface PlanStatsProps {
8
9
  plan: Plan;
@@ -55,14 +56,13 @@ const PlanStats = ({ plan, isSync, lastBackupItem }: PlanStatsProps) => {
55
56
  <Icon type={isActive ? (isSync ? 'reload' : 'copy') : 'pause'} size={16} />
56
57
  <div className={classes.sourceArrow}>→</div>
57
58
  </div>
58
- <div
59
- data-tooltip-id="htmlToolTip"
60
- data-tooltip-place="top"
61
- data-tooltip-html={`<div><strong>Storage:</strong> ${storage?.name}</div><div><strong>Path:</strong> ${storagePath || '/'}</div>`}
62
- >
63
- {storage?.type && <img src={`/providers/${storage?.type}.png`} />}
64
- {storage?.name}
65
- </div>
59
+ <PlanStorageInfo
60
+ disableTooltip={false}
61
+ inline={false}
62
+ replicationSettings={plan.settings.replication}
63
+ storage={storage}
64
+ storagePath={storagePath}
65
+ />
66
66
  </div>
67
67
  </div>
68
68
  <div className={classes.snapshots}>
@@ -0,0 +1,43 @@
1
+ .planStorages {
2
+ .storageWithReplications {
3
+ display: flex;
4
+ flex-direction: column;
5
+ .storageIcons {
6
+ display: inline-block;
7
+ img {
8
+ width: auto;
9
+ max-height: 14px;
10
+ display: inline-block !important;
11
+ position: relative;
12
+ margin-left: -10px;
13
+ padding: 3px;
14
+ border-radius: 50%;
15
+ background: var(--content-background-color);
16
+ border: 1px solid var(--line-color);
17
+ }
18
+ img:nth-child(1) {
19
+ z-index: 99;
20
+ margin-left: 0;
21
+ }
22
+ }
23
+ }
24
+ &.inline {
25
+ display: inline-block;
26
+ margin-left: 5px;
27
+ .storageWithReplications {
28
+ flex-direction: row;
29
+ align-items: center;
30
+ .storageIcons {
31
+ img {
32
+ margin-left: 0px;
33
+ padding: 0;
34
+ background: transparent;
35
+ border: none;
36
+ }
37
+ }
38
+ }
39
+ .storageName {
40
+ display: inline-block;
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,83 @@
1
+ import { PlanReplicationSettings } from '../../../@types';
2
+ import classes from './PlanStorageInfo.module.scss';
3
+
4
+ interface PlanStorageInfoProps {
5
+ storage: { name: string; type: string; id: string };
6
+ storagePath: string;
7
+ replicationSettings?: PlanReplicationSettings;
8
+ disableTooltip?: boolean;
9
+ inline?: boolean;
10
+ }
11
+
12
+ const PlanStorageInfo = ({ replicationSettings, storage, storagePath, disableTooltip = true, inline = true }: PlanStorageInfoProps) => {
13
+ console.log('replicationSettings :', replicationSettings);
14
+ return (
15
+ <>
16
+ {replicationSettings && replicationSettings.enabled && replicationSettings.storages.length > 0 ? (
17
+ <div
18
+ className={`${classes.planStorages} ${inline ? classes.inline : ''}`}
19
+ data-tooltip-hidden={disableTooltip}
20
+ data-tooltip-id="htmlToolTip"
21
+ data-tooltip-place="top"
22
+ data-tooltip-html={`
23
+ <div style="display: flex; flex-direction: column; gap: 8px;">
24
+ <div>
25
+ <div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;">
26
+ <img style="width: 24px; height: 24px;" src="/providers/${storage?.type}.png" />
27
+ <div>
28
+ <strong style="display: block;">${storage?.name}</strong>
29
+ ${storagePath || '/'}
30
+ </div>
31
+ </div>
32
+ </div>
33
+ ${replicationSettings.storages
34
+ .slice(0, 3)
35
+ .map(
36
+ (s) => `
37
+ <div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;">
38
+ <img style="width: 24px; height: 24px;" src="/providers/${s?.storageType}.png" />
39
+ <div>
40
+ <strong style="display: block;">${s?.storageName} (Mirror)</strong>
41
+ ${s?.storagePath || '/'}
42
+ </div>
43
+ </div>
44
+ `,
45
+ )
46
+ .join('')}
47
+ </div>`}
48
+ >
49
+ <div className={classes.storageWithReplications}>
50
+ <div className={classes.storageIcons}>
51
+ <img src={`/providers/${storage?.type}.png`} />
52
+ {replicationSettings.storages.map((s, index) => (
53
+ <img key={s.replicationId} src={`/providers/${s.storageType}.png`} style={{ zIndex: 98 - index }} />
54
+ ))}
55
+ </div>
56
+ <div className={classes.storageName}>{replicationSettings.storages.length + 1} Storages</div>
57
+ </div>
58
+ </div>
59
+ ) : (
60
+ <div
61
+ className={`${classes.planStorages} ${inline ? classes.inline : ''}`}
62
+ data-tooltip-hidden={disableTooltip}
63
+ data-tooltip-id="htmlToolTip"
64
+ data-tooltip-place="top"
65
+ data-tooltip-html={`
66
+ <div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;">
67
+ <img style="width: 24px; height: 24px;" src="/providers/${storage?.type}.png" />
68
+ <div>
69
+ <strong style="display: block;">${storage?.name}</strong>
70
+ ${storagePath || '/'}
71
+ </div>
72
+ </div>
73
+ `}
74
+ >
75
+ <img src={`/providers/${storage?.type}.png`} />
76
+ <div className={classes.storageName}>{storage?.name}</div>
77
+ </div>
78
+ )}
79
+ </>
80
+ );
81
+ };
82
+
83
+ export default PlanStorageInfo;
@@ -33,6 +33,8 @@ const RestoreConfirmStep = ({ backupId, planId, settings, stats, snapshotsStats,
33
33
  includes: settings.includes,
34
34
  excludes: settings.excludes,
35
35
  deleteOption: settings.delete,
36
+ storageId: settings.storageId,
37
+ replicationId: settings.replicationId,
36
38
  },
37
39
  {
38
40
  onSuccess: (data: any, variables) => {
@@ -57,6 +57,8 @@ const RestorePreviewStep = ({ backupId, planId, settings, preview, nextLabel, go
57
57
  includes: settings.includes,
58
58
  excludes: settings.excludes,
59
59
  deleteOption: settings.delete,
60
+ storageId: settings.storageId,
61
+ replicationId: settings.replicationId,
60
62
  },
61
63
  {
62
64
  onSuccess: (data) => {
@@ -6,16 +6,20 @@ import classes from './RestoreWizard.module.scss';
6
6
  import FolderPicker from '../../common/FolderPicker/FolderPicker';
7
7
  import { RestoreSettings } from '../../../@types/restores';
8
8
  import Toggle from '../../common/form/Toggle/Toggle';
9
+ import { Backup } from '../../..';
9
10
 
10
- interface settingsStepProps {
11
+ interface RestoreSettingsStepProps {
11
12
  backupId: string;
13
+ deviceId: string;
12
14
  settings: RestoreSettings;
13
- updateSettings: (settings: settingsStepProps['settings']) => void;
15
+ mirrors?: Backup['mirrors'];
16
+ primaryStorage: { id: string; type: string; name: string };
17
+ updateSettings: (settings: RestoreSettingsStepProps['settings']) => void;
14
18
  goNext: () => void;
15
19
  close: () => void;
16
20
  }
17
21
 
18
- const settingsStep = ({ backupId, settings, updateSettings, goNext, close }: settingsStepProps) => {
22
+ const RestoreSettingsStep = ({ settings, mirrors = [], primaryStorage, updateSettings, goNext, close, deviceId }: RestoreSettingsStepProps) => {
19
23
  const [showFileManager, setShowFileManager] = useState(false);
20
24
  const [showCustomPathError, setShowCustomPathError] = useState(false);
21
25
 
@@ -30,17 +34,38 @@ const settingsStep = ({ backupId, settings, updateSettings, goNext, close }: set
30
34
  return (
31
35
  <div className={classes.stepContent}>
32
36
  <div className={classes.step}>
33
- <p>
34
- Select where you want to restore <strong>"backup-{backupId}"</strong>
35
- </p>
37
+ {mirrors && mirrors.length > 0 && (
38
+ <div className={classes.settingBlock}>
39
+ <Select
40
+ customClasses={classes.storageSelect}
41
+ label="Select Storage to Restore From"
42
+ options={[
43
+ {
44
+ label: primaryStorage!.name + ' (Primary)',
45
+ value: 'primary',
46
+ image: <img src={`/providers/${primaryStorage.type}.png`} />,
47
+ },
48
+ ...mirrors.map((m) => ({
49
+ label: m.storageName + ' (Mirror)',
50
+ value: m.replicationId,
51
+ image: <img src={`/providers/${m.storageType}.png`} />,
52
+ })),
53
+ ]}
54
+ fieldValue={settings.replicationId || primaryStorage.id}
55
+ full={true}
56
+ onUpdate={(value) => updateSettings({ ...settings, replicationId: value === 'primary' ? undefined : value })}
57
+ />
58
+ </div>
59
+ )}
60
+
36
61
  <div className={classes.settingBlock}>
37
62
  <Select
38
- label=""
63
+ label="Select where you want to restore the backup"
39
64
  options={[
40
65
  { label: 'Restore to Original Path(s)', value: 'original' },
41
66
  { label: 'Restore to a Custom Path', value: 'custom' },
42
67
  ]}
43
- fieldValue={settings.type || 'sun'}
68
+ fieldValue={settings.type || 'original'}
44
69
  onUpdate={(val: string) => updateSettings({ ...settings, type: val as 'original' | 'custom' })}
45
70
  full={true}
46
71
  />
@@ -138,9 +163,7 @@ const settingsStep = ({ backupId, settings, updateSettings, goNext, close }: set
138
163
  label="Delete Files that are not in the Snapshot from the Target Restore Path"
139
164
  fieldValue={settings.delete}
140
165
  inline={false}
141
- onUpdate={(val) => {
142
- updateSettings({ ...settings, delete: val });
143
- }}
166
+ onUpdate={(val) => updateSettings({ ...settings, delete: val })}
144
167
  />
145
168
  </div>
146
169
  </div>
@@ -156,7 +179,7 @@ const settingsStep = ({ backupId, settings, updateSettings, goNext, close }: set
156
179
 
157
180
  {showFileManager && (
158
181
  <FolderPicker
159
- deviceId={'main'}
182
+ deviceId={deviceId || 'main'}
160
183
  title="Choose a Path to Restore"
161
184
  footerText="Select a Path where you want to restore your backup"
162
185
  selected={settings.path}
@@ -168,4 +191,4 @@ const settingsStep = ({ backupId, settings, updateSettings, goNext, close }: set
168
191
  );
169
192
  };
170
193
 
171
- export default settingsStep;
194
+ export default RestoreSettingsStep;
@@ -356,6 +356,7 @@
356
356
  padding: 12px;
357
357
  cursor: pointer;
358
358
  border-bottom: 1px solid var(--line-color);
359
+ transition: all 0.12s linear;
359
360
  h5 {
360
361
  font-size: 0.75rem;
361
362
  font-weight: 600;
@@ -372,6 +373,9 @@
372
373
  color: var(--primary-color);
373
374
  }
374
375
  }
376
+ &:hover {
377
+ background-color: var(--primary-color-light);
378
+ }
375
379
  &:last-child {
376
380
  border-bottom: none;
377
381
  }
@@ -8,14 +8,18 @@ import RestoreConfirmStep from './RestoreConfirmStep';
8
8
  import { RestoredFileItem, RestoredItemsStats, RestoreFileItem, RestoreSettings } from '../../../@types/restores';
9
9
  import { useGetSnapshotFiles } from '../../../services/backups';
10
10
  import RestoreFileSelectorStep from './RestoreFileSelectorStep';
11
+ import { Backup, Plan } from '../../..';
11
12
 
12
13
  interface RestoreWizardProps {
13
14
  backupId: string;
14
15
  planId: string;
16
+ deviceId: string;
17
+ planStorage: Plan['storage'];
18
+ mirrors?: Backup['mirrors'];
15
19
  close: () => void;
16
20
  }
17
21
 
18
- const RestoreWizard = ({ backupId, planId, close }: RestoreWizardProps) => {
22
+ const RestoreWizard = ({ backupId, planId, deviceId, mirrors, planStorage, close }: RestoreWizardProps) => {
19
23
  const [step, setStep] = useState(1);
20
24
  const [restoreSettings, setRestoreSettings] = useState<RestoreSettings>({
21
25
  type: 'original',
@@ -24,6 +28,7 @@ const RestoreWizard = ({ backupId, planId, close }: RestoreWizardProps) => {
24
28
  includes: [],
25
29
  excludes: [],
26
30
  delete: false,
31
+ storageId: undefined,
27
32
  });
28
33
  const [restorePreview, setRestorePreview] = useState<{ stats: RestoredItemsStats | null; files: RestoredFileItem[] }>({ stats: null, files: [] });
29
34
  const isFetching = useIsFetching();
@@ -82,7 +87,10 @@ const RestoreWizard = ({ backupId, planId, close }: RestoreWizardProps) => {
82
87
  {step === 1 && (
83
88
  <RestoreSettingsStep
84
89
  backupId={backupId}
90
+ deviceId={deviceId}
85
91
  settings={restoreSettings}
92
+ primaryStorage={planStorage}
93
+ mirrors={mirrors}
86
94
  updateSettings={(settings) => setRestoreSettings(settings)}
87
95
  goNext={() => setStep(2)}
88
96
  close={close}