@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
@@ -1,41 +1,15 @@
1
- import { useMemo } from 'react';
2
1
  import { NavLink } from 'react-router';
3
- import { DevicePlan } from '../../../@types/devices';
2
+ import { DevicePlan, DeviceStorage } from '../../../@types/devices';
4
3
  import Icon from '../../common/Icon/Icon';
5
4
  import classes from './DeviceBackups.module.scss';
6
5
  import { formatBytes } from '../../../utils/helpers';
7
6
 
8
7
  interface DeviceBackupsProps {
9
8
  plans: DevicePlan[];
9
+ storages: DeviceStorage[];
10
10
  }
11
11
 
12
- const DeviceBackups = ({ plans }: DeviceBackupsProps) => {
13
- const plansStorages = useMemo(() => {
14
- const storages: (DevicePlan['storage'] & { size: number })[] = [];
15
-
16
- //first calculate each storage's size from the plan size.
17
- const storageSizes: { [key: string]: number } = {};
18
- plans.forEach((plan) => {
19
- const planStorageId = plan.storage.id;
20
- if (!storageSizes[planStorageId]) {
21
- storageSizes[planStorageId] = 0;
22
- }
23
- storageSizes[planStorageId] += plan.size;
24
- });
25
-
26
- plans.forEach((plan) => {
27
- if (!storages.some((storage) => storage.id === plan.storage.id)) {
28
- // if (plan.storage.id !== 'local') {
29
- // TODO: implement this check
30
- storages.push({ ...plan.storage, size: storageSizes[plan.storage.id] });
31
- // }
32
- }
33
- });
34
- return storages;
35
- }, [plans]);
36
-
37
- console.log('plansStorages :', plansStorages);
38
-
12
+ const DeviceBackups = ({ plans, storages }: DeviceBackupsProps) => {
39
13
  return (
40
14
  <div className={classes.devicePlans}>
41
15
  {/* Backup Plans */}
@@ -46,7 +20,7 @@ const DeviceBackups = ({ plans }: DeviceBackupsProps) => {
46
20
  <div className={classes.widgetContent}>
47
21
  {plans.length === 0 && <div className={classes.noData}>No backup plans found for this device.</div>}
48
22
  {plans.length > 0 &&
49
- plans.map(({ id, title, sourceConfig, storage, isActive, method, size }) => (
23
+ plans.map(({ id, title, sourceConfig, storage, isActive, method, size, replicatedStorages }) => (
50
24
  <div key={id} className={classes.planItem}>
51
25
  <div className={`${classes.status} ${!isActive ? classes.paused : ''}`}>
52
26
  <div className={classes.iconBlock}>
@@ -63,7 +37,7 @@ const DeviceBackups = ({ plans }: DeviceBackupsProps) => {
63
37
  {sourceConfig.includes.length > 1 ? 'paths' : 'path'}
64
38
  </div>
65
39
  <div>
66
- <Icon type="storages" size={12} /> {storage.name}
40
+ <Icon type="storages" size={12} /> {replicatedStorages > 0 ? `${replicatedStorages + 1} storages` : storage.name}
67
41
  </div>
68
42
  <div>
69
43
  <Icon type="disk" size={12} /> {formatBytes(size)}
@@ -109,12 +83,13 @@ const DeviceBackups = ({ plans }: DeviceBackupsProps) => {
109
83
  <Icon type="storages" size={12} /> Connected Storages
110
84
  </div>
111
85
  <div className={classes.widgetContent}>
112
- {plans.length === 0 && <div className={classes.noData}>No Remote storages are being used by this device.</div>}
113
- {plans.length > 0 &&
114
- plansStorages.map(({ id, name, size, type, typeName }) => (
86
+ {storages && storages.length === 0 && <div className={classes.noData}>No Remote storages are being used by this device.</div>}
87
+ {storages &&
88
+ storages.length > 0 &&
89
+ storages.map(({ id, name, type, storageTypeName }) => (
115
90
  <div key={id} className={classes.planItem}>
116
91
  <div className={`${classes.status}`}>
117
- <div className={classes.iconBlock} title={typeName}>
92
+ <div className={classes.iconBlock}>
118
93
  {type === 'local' ? <Icon type="storages" size={12} /> : <img src={`/providers/${type}.png`} />}
119
94
  </div>
120
95
  </div>
@@ -122,7 +97,7 @@ const DeviceBackups = ({ plans }: DeviceBackupsProps) => {
122
97
  <h4>
123
98
  <NavLink to={`/storage/${id}`}>{name}</NavLink>
124
99
  </h4>
125
- <div className={classes.planStats}>{formatBytes(size)}</div>
100
+ <div className={classes.planStats}>{storageTypeName}</div>
126
101
  </div>
127
102
  </div>
128
103
  ))}
@@ -195,3 +195,68 @@
195
195
  }
196
196
  }
197
197
  }
198
+
199
+ .mirrorsEventsSection {
200
+ margin-top: 20px;
201
+ }
202
+
203
+ .mirrorEventsGroup {
204
+ margin-bottom: 16px;
205
+ }
206
+
207
+ .mirrorEventsHeader {
208
+ display: flex;
209
+ align-items: center;
210
+ gap: 8px;
211
+ padding: 8px 0;
212
+ font-weight: 600;
213
+ font-size: 0.95em;
214
+ }
215
+
216
+ .mirrorProviderIcon {
217
+ width: 20px;
218
+ height: 20px;
219
+ object-fit: contain;
220
+ }
221
+
222
+ .mirrorEventsName {
223
+ color: var(--content-text-color);
224
+ }
225
+
226
+ .mirrorEventsStatus {
227
+ padding: 2px 10px;
228
+ font-size: 0.85em;
229
+ border-radius: 12px;
230
+ text-transform: capitalize;
231
+ font-weight: 500;
232
+
233
+ &.mirror_pending {
234
+ background-color: var(--primary-color-lighter);
235
+ color: var(--content-text-color-light);
236
+ }
237
+ &.mirror_running {
238
+ background-color: var(--primary-color-lighter);
239
+ color: var(--primary-color);
240
+ }
241
+ &.mirror_completed {
242
+ background-color: var(--success-bg-color);
243
+ color: var(--success-text-color);
244
+ }
245
+ &.mirror_failed {
246
+ background-color: var(--error-bg-color);
247
+ color: var(--error-button-color);
248
+ }
249
+ }
250
+
251
+ .noEvents {
252
+ padding: 12px;
253
+ color: var(--content-text-color-light);
254
+ font-style: italic;
255
+ font-size: 0.9em;
256
+ }
257
+
258
+ .errorDetails {
259
+ padding: 10px;
260
+ word-break: break-word;
261
+ color: var(--error-button-color);
262
+ }
@@ -2,7 +2,7 @@ import Icon from '../../common/Icon/Icon';
2
2
  import SidePanel from '../../common/SidePanel/SidePanel';
3
3
  import { getBackupEventActionMessage, getRestoreEventActionMessage } from '../../../utils/progressHelpers';
4
4
  import classes from './BackupEvents.module.scss';
5
- import { BackupProgressData } from '../../../@types/backups';
5
+ import { BackupProgressData, ReplicationProgressData } from '../../../@types/backups';
6
6
  import { useGetBackupProgressOnce } from '../../../services/backups';
7
7
  import { useGetRestoreProgressOnce } from '../../../services/restores';
8
8
  import { useMemo, useState } from 'react';
@@ -21,7 +21,7 @@ interface BackupEventsProps {
21
21
  }
22
22
 
23
23
  const BackupEvents = ({ id, type = 'backup', sourceId, sourceType, planId, inProgress, progressData, close }: BackupEventsProps) => {
24
- const [showError, setShowError] = useState<false | number>(false);
24
+ const [showError, setShowError] = useState<false | string>(false);
25
25
  const { data: fetchedProgressData, isLoading } =
26
26
  type === 'backup'
27
27
  ? useGetBackupProgressOnce({ id, sourceId, sourceType, planId })
@@ -95,7 +95,7 @@ const BackupEvents = ({ id, type = 'backup', sourceId, sourceType, planId, inPro
95
95
  <span className={classes.action}>
96
96
  {type === 'backup' ? getBackupEventActionMessage(event.action) : getRestoreEventActionMessage(event.action)}
97
97
  {event.error && (
98
- <span className={classes.viewError} onClick={() => setShowError(index)}>
98
+ <span className={classes.viewError} onClick={() => setShowError(`main-${index}`)}>
99
99
  View Error
100
100
  </span>
101
101
  )}
@@ -105,10 +105,71 @@ const BackupEvents = ({ id, type = 'backup', sourceId, sourceType, planId, inPro
105
105
  })}
106
106
  </ul>
107
107
  ) : null}
108
+ {/* Mirror Events */}
109
+ {progressDataToUse?.mirrors && Object.keys(progressDataToUse.mirrors).length > 0 && (
110
+ <div className={classes.mirrorsEventsSection}>
111
+ {Object.entries(progressDataToUse.mirrors as Record<string, ReplicationProgressData>).map(([storageId, mirror]) => (
112
+ <div key={storageId} className={classes.mirrorEventsGroup}>
113
+ <div className={classes.mirrorEventsHeader}>
114
+ <img src={`/providers/${mirror.storageType}.png`} className={classes.mirrorProviderIcon} />
115
+ <span className={classes.mirrorEventsName}>{mirror.storageName}</span>
116
+ <span className={`${classes.mirrorEventsStatus} ${classes[`mirror_${mirror.status}`] || ''}`}>{mirror.status}</span>
117
+ </div>
118
+ {mirror.events && mirror.events.length > 0 ? (
119
+ <ul className={classes.eventList}>
120
+ {mirror.events.map((event: any, index: number) => {
121
+ const isError = event.error;
122
+ const isCompleted = event.action === 'REPLICATION_COMPLETE';
123
+ const isFailed = event.action === 'REPLICATION_FAILED';
124
+ return (
125
+ <li
126
+ key={index}
127
+ className={`${classes.eventItem} ${isError ? classes.error : ''} ${isCompleted ? classes.completed : ''} ${isFailed ? classes.failed : ''}`}
128
+ >
129
+ <span className={classes.icon}>
130
+ <Icon type={isError ? 'error-circle-filled' : 'check-circle-filled'} size={16} />
131
+ </span>
132
+ <span className={classes.time}>{new Date(event.timestamp).toLocaleTimeString()}</span>
133
+ <span className={classes.phase}>{event.phase}</span>
134
+ <span className={classes.action}>
135
+ {getBackupEventActionMessage(event.action, mirror.storageName)}
136
+ {event.error && (
137
+ <span className={classes.viewError} onClick={() => setShowError(`mirror-${storageId}-${index}`)}>
138
+ View Error
139
+ </span>
140
+ )}
141
+ </span>
142
+ </li>
143
+ );
144
+ })}
145
+ </ul>
146
+ ) : (
147
+ <div className={classes.noEvents}>No events recorded</div>
148
+ )}
149
+ </div>
150
+ ))}
151
+ </div>
152
+ )}
108
153
  </div>
109
154
  {showError && (
110
155
  <Modal title="Error Details" closeModal={() => setShowError(false)} width="400px">
111
- <div className={classes.errorDetails}>{progressDataToUse?.events[showError]?.error || 'Unknown error occurred.'}</div>
156
+ <div className={classes.errorDetails}>
157
+ {(() => {
158
+ if (typeof showError === 'string') {
159
+ if (showError.startsWith('main-')) {
160
+ const idx = parseInt(showError.replace('main-', ''));
161
+ return progressDataToUse?.events[idx]?.error || 'Unknown error occurred.';
162
+ }
163
+ if (showError.startsWith('mirror-')) {
164
+ const parts = showError.replace('mirror-', '').split('-');
165
+ const eventIdx = parseInt(parts.pop()!);
166
+ const sId = parts.join('-');
167
+ return (progressDataToUse?.mirrors as any)?.[sId]?.events?.[eventIdx]?.error || 'Unknown error occurred.';
168
+ }
169
+ }
170
+ return 'Unknown error occurred.';
171
+ })()}
172
+ </div>
112
173
  </Modal>
113
174
  )}
114
175
  </SidePanel>
@@ -10,13 +10,15 @@
10
10
  .backup {
11
11
  width: 100%;
12
12
  padding: 10px 15px;
13
- display: flex;
14
- justify-content: left;
15
- align-items: center;
16
13
  box-sizing: border-box;
17
14
  border-radius: 6px;
18
15
  border: 1px solid var(--line-color);
19
16
  margin-bottom: 15px;
17
+ .backupProgress {
18
+ display: flex;
19
+ justify-content: left;
20
+ align-items: center;
21
+ }
20
22
  .backupIcon {
21
23
  margin-right: 15px;
22
24
  }
@@ -118,3 +120,119 @@
118
120
  }
119
121
  }
120
122
  }
123
+
124
+ .mirrorsSection {
125
+ width: 100%;
126
+ margin-top: 10px;
127
+ padding: 10px;
128
+ box-sizing: border-box;
129
+ border-top: 1px solid var(--line-color);
130
+ }
131
+
132
+ .mirrorsTitle {
133
+ font-size: 0.6875rem;
134
+ font-weight: 600;
135
+ color: var(--content-text-color-light);
136
+ margin-bottom: 8px;
137
+ }
138
+
139
+ .mirrorsList {
140
+ display: flex;
141
+ flex-direction: column;
142
+ gap: 6px;
143
+ }
144
+
145
+ .mirrorItem {
146
+ display: flex;
147
+ flex-direction: row;
148
+ gap: 10px;
149
+ }
150
+ .mirrorProgress {
151
+ width: 100%;
152
+ display: flex;
153
+ flex-direction: column;
154
+ gap: 5px;
155
+ }
156
+
157
+ .mirrorStorageIcon {
158
+ padding: 5px;
159
+ border-radius: 4px;
160
+ border: 1px solid var(--line-color);
161
+ position: relative;
162
+ span {
163
+ position: absolute;
164
+ z-index: 2;
165
+ top: -5px;
166
+ margin-left: -10px;
167
+ }
168
+ img {
169
+ max-width: 20px;
170
+ vertical-align: middle;
171
+ }
172
+ }
173
+
174
+ .mirrorInfo {
175
+ display: flex;
176
+ justify-content: space-between;
177
+ align-items: center;
178
+ }
179
+
180
+ .mirrorName {
181
+ font-size: 0.7rem;
182
+ color: var(--content-text-color);
183
+ }
184
+
185
+ .mirrorStatus {
186
+ font-size: 0.7rem;
187
+ font-weight: 600;
188
+ color: var(--content-text-color-light);
189
+ &.mirrorCompleted {
190
+ color: var(--success-text-color);
191
+ }
192
+ &.mirrorFailed {
193
+ color: var(--error-button-color);
194
+ }
195
+ }
196
+
197
+ .mirrorBar {
198
+ width: 100%;
199
+ height: 6px;
200
+ border-radius: 3px;
201
+ background-color: var(--line-color);
202
+ overflow: hidden;
203
+ .mirrorBarFill {
204
+ height: 100%;
205
+ border-radius: 3px;
206
+ transition: all 0.3s linear;
207
+ &.mirrorCompleted {
208
+ background-color: turquoise;
209
+ }
210
+ &.mirrorFailed {
211
+ background-color: var(--error-button-color);
212
+ }
213
+ &.mirrorRunning {
214
+ background-color: turquoise;
215
+ }
216
+ &.mirrorPending {
217
+ background-color: var(--line-color);
218
+ width: 0;
219
+ }
220
+
221
+ &.mirrorRunningIndeterminate {
222
+ background-color: turquoise;
223
+ width: 100% !important;
224
+ opacity: 0.4;
225
+ animation: mirrorPulse 1.8s ease-in-out infinite;
226
+ }
227
+ }
228
+ }
229
+
230
+ @keyframes mirrorPulse {
231
+ 0%,
232
+ 100% {
233
+ opacity: 0.25;
234
+ }
235
+ 50% {
236
+ opacity: 0.6;
237
+ }
238
+ }
@@ -5,12 +5,18 @@ import Icon from '../../common/Icon/Icon';
5
5
  import { useCancelBackup, useGetBackupProgress } from '../../../services/backups';
6
6
  import classes from './BackupProgress.module.scss';
7
7
  import { formatBytes, formatDateTime, formatSeconds, timeAgo } from '../../../utils/helpers';
8
- import { generateBackupProgressMessage, extractResticData, generateRestoreProgressMessage } from '../../../utils/progressHelpers';
8
+ import {
9
+ generateBackupProgressMessage,
10
+ extractResticData,
11
+ generateRestoreProgressMessage,
12
+ generateMirrorProgressMessage,
13
+ } from '../../../utils/progressHelpers';
9
14
  import { Backup } from '../../../@types/backups';
10
15
  import { RestoreSlim } from '../../../@types/restores';
11
16
  import ActionModal from '../../common/ActionModal/ActionModal';
12
17
  import { useCancelRestore, useGetRestoreProgress } from '../../../services/restores';
13
18
  import BackupEvents from '../BackupEvents/BackupEvents';
19
+ import { ReplicationProgressData } from '../../../@types/backups';
14
20
 
15
21
  interface BackupProgressProps {
16
22
  item: Backup | RestoreSlim;
@@ -127,84 +133,156 @@ const BackupProgress = ({ item, sourceId, sourceType, planId, type = 'backup' }:
127
133
 
128
134
  return (
129
135
  <div key={item.id} className={classes.backup}>
130
- <div className={classes.backupIcon}>
131
- <Icon type="loading" size={24} />
132
- </div>
133
- <div className={classes.backupLeft}>
134
- <div className={classes.backupId}>
135
- {type === 'restore' ? 'Restoring ' : ''}backup-{id}
136
- <span className={classes.backupTime} title={formatDateTime(started)}>
137
- <Icon type="clock" size={12} /> Started {started ? timeAgo(new Date(started)) : 'a few seconds ago'}
138
- </span>
139
- {item.errorMsg && (
140
- <span className={classes.backupError}>
141
- <Icon type="error-circle-filled" size={13} />{' '}
142
- <i data-tooltip-id="htmlToolTip" data-tooltip-place="top" data-tooltip-html={item.errorMsg}>
143
- <u>Error</u> Occurred.
144
- </i>{' '}
145
- Retrying...
146
- </span>
147
- )}
136
+ <div key={item.id} className={classes.backupProgress}>
137
+ <div className={classes.backupIcon}>
138
+ <Icon type="loading" size={24} />
148
139
  </div>
149
- <div className={classes.backupStart}>
150
- <div>
151
- <span className={classes.progressMessage} onClick={() => setShowProgressDetails(true)}>
152
- {progressMessage}
140
+ <div className={classes.backupLeft}>
141
+ <div className={classes.backupId}>
142
+ {type === 'restore' ? 'Restoring ' : ''}backup-{id}
143
+ <span className={classes.backupTime} title={formatDateTime(started)}>
144
+ <Icon type="clock" size={12} /> Started {started ? timeAgo(new Date(started)) : 'a few seconds ago'}
153
145
  </span>
154
- {/* <Icon type="clock" size={12} /> Started {started ? timeAgo(new Date(started)) : 'a few seconds ago'} */}
146
+ {item.errorMsg && (
147
+ <span className={classes.backupError}>
148
+ <Icon type="error-circle-filled" size={13} />{' '}
149
+ <i data-tooltip-id="htmlToolTip" data-tooltip-place="top" data-tooltip-html={item.errorMsg}>
150
+ <u>Error</u> Occurred.
151
+ </i>{' '}
152
+ Retrying...
153
+ </span>
154
+ )}
155
155
  </div>
156
- <button onClick={() => setShowCancelModal(true)} title={`Cancel ${type}`}>
157
- <Icon type="close" size={12} /> Cancel
158
- </button>
159
- </div>
160
- </div>
161
- <div className={classes.backupRight}>
162
- <div className={classes.ProgressStats}>
163
- <div className={classes.ProgressStatsLeft}>
164
- <span>
165
- {filesProcessed} / {totalFiles} Files
166
- </span>
167
- <i></i>
168
- <span>
169
- {formatBytes(bytesProcessed)} / {formatBytes(totalBytes)}
170
- </span>
156
+ <div className={classes.backupStart}>
157
+ <div>
158
+ <span className={classes.progressMessage} onClick={() => setShowProgressDetails(true)}>
159
+ {progressMessage}
160
+ </span>
161
+ {/* <Icon type="clock" size={12} /> Started {started ? timeAgo(new Date(started)) : 'a few seconds ago'} */}
162
+ </div>
163
+ <button onClick={() => setShowCancelModal(true)} title={`Cancel ${type}`}>
164
+ <Icon type="close" size={12} /> Cancel
165
+ </button>
171
166
  </div>
172
- <div className={classes.ProgressStatsRight}>{type !== 'restore' && <>Remaining: {formatSeconds(seconds_remaining)}</>}</div>
173
167
  </div>
174
- <div className={classes.progressBar}>
175
- <div
176
- className={`${classes.progressBarFill} ${progressPercent > 3 ? classes.progressBarFilled : ''}`}
177
- style={{ width: progressPercent + '%' }}
178
- >
179
- <span>{progressPercent}%</span>
168
+ <div className={classes.backupRight}>
169
+ <div className={classes.ProgressStats}>
170
+ <div className={classes.ProgressStatsLeft}>
171
+ <span>
172
+ {filesProcessed} / {totalFiles} Files
173
+ </span>
174
+ <i></i>
175
+ <span>
176
+ {formatBytes(bytesProcessed)} / {formatBytes(totalBytes)}
177
+ </span>
178
+ </div>
179
+ <div className={classes.ProgressStatsRight}>{type !== 'restore' && <>Remaining: {formatSeconds(seconds_remaining)}</>}</div>
180
+ </div>
181
+ <div className={classes.progressBar}>
182
+ <div
183
+ className={`${classes.progressBarFill} ${progressPercent > 3 ? classes.progressBarFilled : ''}`}
184
+ style={{ width: progressPercent + '%' }}
185
+ >
186
+ <span>{progressPercent}%</span>
187
+ </div>
180
188
  </div>
181
189
  </div>
190
+
191
+ {showCancelModal && (
192
+ <ActionModal
193
+ title={`Cancel ${type}`}
194
+ message={<>{`Are you sure you want to cancel the ${type} process?`}</>}
195
+ closeModal={() => setShowCancelModal(false)}
196
+ width="400px"
197
+ primaryAction={{
198
+ title: `Yes, Cancel ${type}`,
199
+ type: 'danger',
200
+ isPending: type === 'restore' ? cancelRestoreMutation.isPending : cancelBackupMutation.isPending,
201
+ action: () => cancel(),
202
+ }}
203
+ />
204
+ )}
205
+ {showProgressDetails && (
206
+ <BackupEvents
207
+ id={id}
208
+ type={type}
209
+ planId={planId}
210
+ sourceId={sourceId}
211
+ sourceType={sourceType}
212
+ progressData={progressData}
213
+ inProgress={true}
214
+ close={() => setShowProgressDetails(false)}
215
+ />
216
+ )}
182
217
  </div>
183
- {showCancelModal && (
184
- <ActionModal
185
- title={`Cancel ${type}`}
186
- message={<>{`Are you sure you want to cancel the ${type} process?`}</>}
187
- closeModal={() => setShowCancelModal(false)}
188
- width="400px"
189
- primaryAction={{
190
- title: `Yes, Cancel ${type}`,
191
- type: 'danger',
192
- isPending: type === 'restore' ? cancelRestoreMutation.isPending : cancelBackupMutation.isPending,
193
- action: () => cancel(),
194
- }}
195
- />
196
- )}
197
- {showProgressDetails && (
198
- <BackupEvents
199
- id={id}
200
- type={type}
201
- planId={planId}
202
- sourceId={sourceId}
203
- sourceType={sourceType}
204
- progressData={progressData}
205
- inProgress={true}
206
- close={() => setShowProgressDetails(false)}
207
- />
218
+ {/* Mirrors Progress */}
219
+ {progressData?.mirrors && Object.keys(progressData.mirrors).length > 0 && (
220
+ <div className={classes.mirrorsSection}>
221
+ <div className={classes.mirrorsTitle}>Replicating</div>
222
+ <div className={classes.mirrorsList}>
223
+ {Object.entries(progressData.mirrors as Record<string, ReplicationProgressData>).map(([storageId, mirror]) => {
224
+ const mirrorResticData = mirror.events
225
+ ?.slice()
226
+ .reverse()
227
+ .find((e: any) => e.resticData)?.resticData;
228
+ const mirrorPercent = mirrorResticData?.percent_done ? Math.round(mirrorResticData.percent_done * 100) : 0;
229
+ const mirrorMessage = generateMirrorProgressMessage(mirror);
230
+
231
+ let mirrorStatusClass = classes.mirrorPending;
232
+ if (mirror.status === 'running')
233
+ mirrorStatusClass = mirrorPercent > 0 ? classes.mirrorRunning : classes.mirrorRunningIndeterminate;
234
+ if (mirror.status === 'completed') mirrorStatusClass = classes.mirrorCompleted;
235
+ if (mirror.status === 'failed') mirrorStatusClass = classes.mirrorFailed;
236
+ if (mirror.status === 'retrying') mirrorStatusClass = classes.mirrorRunningIndeterminate;
237
+
238
+ return (
239
+ <div key={storageId} className={classes.mirrorItem}>
240
+ <div className={classes.mirrorStorageIcon}>
241
+ {(mirror.status === 'running' || mirror.status === 'pending' || mirror.status === 'retrying') && (
242
+ <Icon type="loading" size={12} />
243
+ )}
244
+ <img src={`/providers/${mirror.storageType}.png`} />
245
+ </div>
246
+ <div className={classes.mirrorProgress}>
247
+ <div className={classes.mirrorInfo}>
248
+ <span className={classes.mirrorName}>{mirror.storageName}</span>
249
+ <span className={`${classes.mirrorStatus} ${mirrorStatusClass}`}>
250
+ {mirror.status === 'pending' && 'Pending'}
251
+ {mirror.status === 'running' && (mirrorPercent > 0 ? `${mirrorPercent}% — ${mirrorMessage}` : mirrorMessage)}
252
+ {mirror.status === 'completed' && (
253
+ <>
254
+ <Icon type="check" size={10} /> Completed
255
+ </>
256
+ )}
257
+ {mirror.status === 'failed' && (
258
+ <span data-tooltip-id="htmlToolTip" data-tooltip-html={mirror.error || 'Replication failed'}>
259
+ <Icon type="error" size={12} /> Failed
260
+ </span>
261
+ )}
262
+ {mirror.status === 'retrying' && mirrorMessage}
263
+ </span>
264
+ </div>
265
+ <div className={classes.mirrorBar}>
266
+ <div
267
+ className={`${classes.mirrorBarFill} ${mirrorStatusClass}`}
268
+ style={{
269
+ width:
270
+ mirror.status === 'completed'
271
+ ? '100%'
272
+ : mirror.status === 'failed'
273
+ ? '100%'
274
+ : mirrorPercent > 0
275
+ ? `${mirrorPercent}%`
276
+ : undefined,
277
+ }}
278
+ />
279
+ </div>
280
+ </div>
281
+ </div>
282
+ );
283
+ })}
284
+ </div>
285
+ </div>
208
286
  )}
209
287
  </div>
210
288
  );