@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
@@ -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}
@@ -55,7 +55,7 @@ const GeneralSettings = ({ settings, onUpdate }: GeneralSettingsProps) => {
55
55
  { label: 'Light', value: 'light' },
56
56
  ]}
57
57
  onUpdate={(val: string) => handleThemeChange(val as 'auto' | 'light' | 'dark')}
58
- inline={true}
58
+ inline={false}
59
59
  />
60
60
  </div>
61
61
  </div>
@@ -19,12 +19,19 @@ const IntegrationSettings = ({ settingsID, settings, onUpdate }: IntegrationSett
19
19
 
20
20
  const validationMutation = useValidateIntegration();
21
21
 
22
- console.log('smtp :', smtp);
22
+ const onIntegrationUpdate = (key: string, intSettings: Record<string, any>) => {
23
+ console.log('onIntegrationUpdate :', key, intSettings);
24
+ onUpdate({ ...integrationSettings, [key]: { ...integrationSettings[key], ...intSettings } });
25
+ };
23
26
 
24
27
  return (
25
28
  <div className={classes.integrations}>
26
29
  <div>
27
- <SMTPSettings settings={smtp} onUpdate={onUpdate} showTestModal={setShowEmailTestModal} />
30
+ <SMTPSettings
31
+ settings={smtp}
32
+ onUpdate={(iSettings) => onIntegrationUpdate('smtp', iSettings)}
33
+ showTestModal={(type) => setShowEmailTestModal(type)}
34
+ />
28
35
  </div>
29
36
  {showEmailTestModal && (
30
37
  <ActionModal
@@ -125,7 +125,16 @@ const Icon = ({ type, color = 'currentColor', size = 16, title = '', classes = '
125
125
  </g>
126
126
  </IconWrapper>
127
127
  )}
128
-
128
+ {type === 'mirrors' && (
129
+ <IconWrapper size={size} viewBox="0 0 24 24">
130
+ <path
131
+ fill={color}
132
+ d="M21.53 5.15a.99.99 0 0 0-.97-.04l-10 5a1 1 0 0 0-.55.89v10a1 1 0 0 0 1 1c.15 0 .31-.04.45-.11l10-5a1 1 0 0 0 .55-.89V6c0-.35-.18-.67-.47-.85ZM20 15.38l-8 4v-7.76l8-4z"
133
+ />
134
+ <path fill={color} d="m16.55 3.11l-10 5A1 1 0 0 0 6 9v10h2V9.62l9.45-4.72l-.89-1.79Z" />
135
+ <path fill={color} d="m12.55 1.11l-10 5A1 1 0 0 0 2 7v10h2V7.62l9.45-4.73l-.89-1.79Z" />
136
+ </IconWrapper>
137
+ )}
129
138
  {type === 'devices' && (
130
139
  <IconWrapper size={size} viewBox="0 0 24 24">
131
140
  <path
@@ -56,6 +56,9 @@
56
56
  }
57
57
 
58
58
  @media only screen and (max-width: 768px) {
59
+ .header {
60
+ max-height: none;
61
+ }
59
62
  .headerTitle {
60
63
  font-size: 1rem;
61
64
  }
@@ -89,6 +89,12 @@ export { default as PlanTypeSettings } from './Plan/PlanSettings/PlanTypeSetting
89
89
  export { default as PlanStats } from './Plan/PlanStats/PlanStats';
90
90
  export { default as PlanUnlockModal } from './Plan/PlanUnlockModal/PlanUnlockModal';
91
91
 
92
+ // Mirror/Replication components
93
+ export { default as MirrorStatusBadge } from './Plan/Mirrors/MirrorStatusBadge';
94
+ export { default as MirrorStorageSelector } from './Plan/Mirrors/MirrorStorageSelector';
95
+ export { default as MirrorStorageSelectorModal } from './Plan/Mirrors/MirrorStorageSelectorModal';
96
+ export { default as PlanReplicationSettings } from './Plan/PlanSettings/PlanReplicationSettings';
97
+
92
98
  // Restore components
93
99
  export { default as PlanRestores } from './Plan/Restores/Restores';
94
100
  export { default as RestoreChangeViewer } from './Restore/RestoreChangeViewer/RestoreChangeViewer';
@@ -1,9 +1,20 @@
1
- import { useEffect } from 'react';
1
+ import { useEffect, useRef } from 'react';
2
2
  import { useRegisterSW } from 'virtual:pwa-register/react';
3
3
 
4
+ const VERSION_STORAGE_KEY = 'pluton_app_version';
5
+
4
6
  /**
5
- * This hook handles the PWA auto-update logic in the background.
6
- * It's completely silent and requires no user interaction.
7
+ * Handles two PWA update scenarios:
8
+ *
9
+ * 1. **Workbox precache update** — When a new frontend build is deployed, Workbox detects
10
+ * that the precache manifest changed and sets `needRefresh`. We immediately activate
11
+ * the new service worker so the page reloads with fresh assets.
12
+ *
13
+ * 2. **Backend version mismatch** — Compares the server's `X-App-Version` header
14
+ * (stored on `window.plutonVersion` by `validateAuth`) against the last known version
15
+ * in localStorage. On mismatch (e.g. docker/binary upgrade), unregisters all service
16
+ * workers, clears all caches, and hard-reloads. This catches cases where the SW update
17
+ * check hasn't fired yet (long-lived tabs, cached sw.js, etc.).
7
18
  */
8
19
  export function usePwaAutoUpdate() {
9
20
  const {
@@ -12,22 +23,51 @@ export function usePwaAutoUpdate() {
12
23
  updateServiceWorker,
13
24
  } = useRegisterSW();
14
25
 
26
+ const versionCheckedRef = useRef(false);
27
+
28
+ // Workbox detected a new service worker with updated assets
15
29
  useEffect(() => {
16
- // This effect triggers when a new service worker is ready to take over.
17
30
  if (needRefresh) {
18
- // This immediately tells the new service worker to take control,
19
- // which will force a hard reload of the page.
20
31
  updateServiceWorker(true);
21
32
  }
22
-
23
- // This effect triggers when the app's assets are fully cached and ready for offline use.
24
- // We won't show a toast, but we'll log it for debugging purposes.
25
33
  if (offlineReady) {
26
34
  console.log('PWA is ready for offline use.');
27
- // Reset the state to prevent this log from firing on every render.
28
35
  setOfflineReady(false);
29
36
  }
30
37
  }, [needRefresh, offlineReady, setOfflineReady, updateServiceWorker]);
31
38
 
32
- // This hook doesn't need to return anything as it performs its job in the background.
39
+ // Backend version mismatch check
40
+ useEffect(() => {
41
+ if (versionCheckedRef.current) return;
42
+
43
+ const serverVersion = (window as any).plutonVersion;
44
+ if (!serverVersion || serverVersion === 'unknown') return;
45
+
46
+ const storedVersion = localStorage.getItem(VERSION_STORAGE_KEY);
47
+
48
+ if (storedVersion && storedVersion !== serverVersion) {
49
+ versionCheckedRef.current = true;
50
+ clearCachesAndReload(serverVersion);
51
+ } else if (!storedVersion) {
52
+ localStorage.setItem(VERSION_STORAGE_KEY, serverVersion);
53
+ }
54
+ });
55
+ }
56
+
57
+ async function clearCachesAndReload(newVersion: string) {
58
+ try {
59
+ if ('serviceWorker' in navigator) {
60
+ const registrations = await navigator.serviceWorker.getRegistrations();
61
+ await Promise.all(registrations.map((r) => r.unregister()));
62
+ }
63
+ if ('caches' in window) {
64
+ const cacheNames = await caches.keys();
65
+ await Promise.all(cacheNames.map((name) => caches.delete(name)));
66
+ }
67
+ } catch (e) {
68
+ console.error('Failed to clear caches:', e);
69
+ }
70
+
71
+ localStorage.setItem(VERSION_STORAGE_KEY, newVersion);
72
+ window.location.reload();
33
73
  }
package/src/router.tsx CHANGED
@@ -3,7 +3,6 @@ import Plans from './routes/Plans/Plans';
3
3
  import Login from './routes/Login/Login';
4
4
  import Setup from './routes/Setup/Setup';
5
5
  import { useAuth } from './services/users';
6
- import { useSetupStatus } from './services/settings';
7
6
  import App from './components/App/App/App';
8
7
  import Settings from './routes/Settings/Settings';
9
8
  import Storages from './routes/Storages/Storages';
@@ -14,14 +13,10 @@ import Footer from './components/App/Footer/Footer';
14
13
  import NotFoundRoute from './routes/NotFoundRoute/NotFoundRoute';
15
14
  import Icon from './components/common/Icon/Icon';
16
15
 
17
- function ProtectedLayout() {
18
- // Check setup status FIRST - this endpoint doesn't require auth
19
- // and tells us if we're in binary mode and if setup is pending
20
- const { data: setupStatus, isLoading: setupLoading } = useSetupStatus();
21
- const { data: authData, isError: authError, isLoading: authLoading } = useAuth();
16
+ function PublicRoute({ children }: { children: React.ReactNode }) {
17
+ const { data: authData, isLoading: authLoading } = useAuth();
22
18
 
23
- // Show loading spinner while checking setup status
24
- if (setupLoading) {
19
+ if (authLoading) {
25
20
  return (
26
21
  <div className="loadingScreen">
27
22
  <Icon size={60} type="loading" />
@@ -29,16 +24,16 @@ function ProtectedLayout() {
29
24
  );
30
25
  }
31
26
 
32
- // If setup status check failed, we can still proceed with auth check
33
- // (might be a network issue or old backend without setup endpoint)
34
-
35
- // For binary installations with pending setup, redirect to setup page
36
- // This check happens BEFORE auth check so unauthenticated users can access setup
37
- if (setupStatus?.data?.isBinary && setupStatus?.data?.setupPending) {
38
- return <Navigate to="/setup" replace />;
27
+ if (authData) {
28
+ return <Navigate to="/" replace />;
39
29
  }
40
30
 
41
- // Now check auth (setup is either complete or not a binary installation)
31
+ return <>{children}</>;
32
+ }
33
+
34
+ function ProtectedLayout() {
35
+ const { data: authData, isError: authError, isLoading: authLoading } = useAuth();
36
+
42
37
  if (authLoading) {
43
38
  return (
44
39
  <div className="loadingScreen">
@@ -47,6 +42,13 @@ function ProtectedLayout() {
47
42
  );
48
43
  }
49
44
 
45
+ // Check setup-pending from auth response headers (set by versionMiddleware on all responses)
46
+ const installType = (window as any).plutonInstallType;
47
+ const setupPending = (window as any).plutonSetupPending;
48
+ if (installType === 'binary' && setupPending) {
49
+ return <Navigate to="/setup" replace />;
50
+ }
51
+
50
52
  // If auth failed, redirect to login
51
53
  if (authError) {
52
54
  return <Navigate to="/login" replace />;
@@ -74,7 +76,14 @@ export function AppRoutes() {
74
76
  <Route path={'plan/:id'} element={<PlanSingle />} />
75
77
  <Route path="*" element={<NotFoundRoute />} />
76
78
  </Route>
77
- <Route path="login" element={<Login />} />
79
+ <Route
80
+ path="login"
81
+ element={
82
+ <PublicRoute>
83
+ <Login />
84
+ </PublicRoute>
85
+ }
86
+ />
78
87
  <Route path="setup" element={<Setup />} />
79
88
  </Routes>
80
89
  );
@@ -6,10 +6,9 @@ import { useGetDevice, useGetSystemMetrics } from '../../services/devices';
6
6
  import DeviceInfo from '../../components/Device/DeviceInfo/DeviceInfo';
7
7
  import EditDevice from '../../components/Device/EditDevice/EditDevice';
8
8
  import classes from './DeviceSingle.module.scss';
9
- // import { compareVersions } from '../../utils/helpers';
10
9
  import NotFound from '../../components/common/NotFound/NotFound';
11
10
  import DeviceBackups from '../../components/Device/DeviceBackups/DeviceBackups';
12
- import { DevicePlan } from '../../@types/devices';
11
+ import { DevicePlan, DeviceStorage } from '../../@types/devices';
13
12
 
14
13
  const DeviceSingle = () => {
15
14
  const { id } = useParams();
@@ -27,6 +26,7 @@ const DeviceSingle = () => {
27
26
  const device = data?.result?.device;
28
27
  const metrics = metricsData?.result && metricsData?.result?.system ? metricsData.result : device?.metrics;
29
28
  const devicePlans: DevicePlan[] = data?.result?.plans || [];
29
+ const deviceStorages: DeviceStorage[] = data?.result?.storages || [];
30
30
  const deviceName = device?.name;
31
31
  const isPending = device?.status && device?.status === 'pending';
32
32
 
@@ -73,7 +73,7 @@ const DeviceSingle = () => {
73
73
  </div>
74
74
  ) : (
75
75
  <>
76
- <DeviceBackups plans={devicePlans} />
76
+ <DeviceBackups plans={devicePlans} storages={deviceStorages} />
77
77
  {metrics?.system && <DeviceInfo metrics={metrics} isRefetching={false} />}
78
78
  </>
79
79
  )}
@@ -5,14 +5,12 @@ import { API_URL } from '../utils/constants';
5
5
  const notifiedBackupProgress = new Set<string>();
6
6
 
7
7
  // Generate Download
8
- export async function generateBackupDownload({ backupId }: { backupId: string; planId: string }) {
9
- // const header = new Headers({ 'Content-Type': 'application/json', Accept: 'application/json' });
10
- const res = await fetch(`${API_URL}/backups/${backupId}/action/download`, {
8
+ export async function generateBackupDownload({ backupId, replicationId }: { backupId: string; planId: string; replicationId?: string }) {
9
+ const url = `${API_URL}/backups/${backupId}/action/download?${replicationId ? `replicationId=${replicationId}` : ''}`;
10
+ const res = await fetch(url, {
11
11
  method: 'POST',
12
12
  credentials: 'include',
13
- // headers: header,
14
13
  });
15
- // Check if response is ok
16
14
  const data = await res.json();
17
15
  if (!data.success) {
18
16
  throw new Error(data.error);
@@ -241,13 +239,12 @@ export function useUpdateBackup() {
241
239
  }
242
240
 
243
241
  // Get Snapshot Files
244
- export async function getSnapshotFiles({ backupId }: { backupId: string }) {
245
- const res = await fetch(`${API_URL}/backups/${backupId}/files`, {
242
+ export async function getSnapshotFiles({ backupId, replicationId }: { backupId: string; replicationId?: string }) {
243
+ const url = replicationId ? `${API_URL}/backups/${backupId}/files?replicationId=${replicationId}` : `${API_URL}/backups/${backupId}/files`;
244
+ const res = await fetch(url, {
246
245
  method: 'GET',
247
246
  credentials: 'include',
248
- // headers: header,
249
247
  });
250
- // Check if response is ok
251
248
  const data = await res.json();
252
249
  if (!data.success) {
253
250
  throw new Error(data.error);
@@ -263,3 +260,29 @@ export function useGetSnapshotFiles(payload: { backupId: string }) {
263
260
  retry: false,
264
261
  });
265
262
  }
263
+
264
+ // Retry Failed Replications
265
+ export async function retryFailedReplications({ backupId, replicationId }: { backupId: string; planId: string; replicationId: string }) {
266
+ const header = new Headers({ 'Content-Type': 'application/json', Accept: 'application/json' });
267
+ const res = await fetch(`${API_URL}/backups/${backupId}/action/replication-retry?replicationId=${replicationId}`, {
268
+ method: 'POST',
269
+ credentials: 'include',
270
+ headers: header,
271
+ });
272
+ const data = await res.json();
273
+ if (!data.success) {
274
+ throw new Error(data.error);
275
+ }
276
+ return data;
277
+ }
278
+
279
+ export function useRetryFailedReplications() {
280
+ const queryClient = useQueryClient();
281
+ return useMutation({
282
+ mutationFn: retryFailedReplications,
283
+ onSuccess: (res, payload) => {
284
+ console.log('res :', payload, res);
285
+ queryClient.invalidateQueries({ queryKey: ['plan', payload.planId] });
286
+ },
287
+ });
288
+ }
@@ -392,3 +392,48 @@ export function useCheckActiveBackupsOrRestore() {
392
392
  // refetchOnMount: true,
393
393
  });
394
394
  }
395
+
396
+ // Remove Replication Storage
397
+ export async function deleteReplicationStorage({
398
+ planID,
399
+ storageID,
400
+ storagePath,
401
+ removeData,
402
+ replicationId,
403
+ }: {
404
+ planID: string;
405
+ storageID: string;
406
+ storagePath: string;
407
+ removeData: boolean;
408
+ replicationId?: string;
409
+ }) {
410
+ const header = new Headers({ 'Content-Type': 'application/json', Accept: 'application/json' });
411
+ const res = await fetch(`${API_URL}/plans/${planID}/action/delete-replication-storage`, {
412
+ method: 'POST',
413
+ credentials: 'include',
414
+ headers: header,
415
+ body: JSON.stringify({ storageID, storagePath, removeData, replicationId }),
416
+ });
417
+ const data = await res.json();
418
+ if (!data.success) {
419
+ throw new Error(data.error);
420
+ }
421
+ return data;
422
+ }
423
+
424
+ export function useDeleteReplicationStorage() {
425
+ const queryClient = useQueryClient();
426
+ return useMutation({
427
+ mutationFn: deleteReplicationStorage,
428
+ onError: (error: Error) => {
429
+ console.log('error :', error?.message);
430
+ toast.error(error.message || `Error removing replication storage.`);
431
+ },
432
+ onSuccess: (res, variables) => {
433
+ console.log('# Replication Storage Removed! :', res, variables);
434
+ queryClient.invalidateQueries({ queryKey: ['plan', variables.planID] });
435
+ queryClient.invalidateQueries({ queryKey: ['plans'] });
436
+ toast.success(res?.message || `Replication storage removed successfully!`, { autoClose: 5000 });
437
+ },
438
+ });
439
+ }
@@ -89,6 +89,8 @@ export async function restoreBackup({
89
89
  includes,
90
90
  excludes,
91
91
  deleteOption,
92
+ storageId,
93
+ replicationId,
92
94
  }: {
93
95
  backupId: string;
94
96
  planId: string;
@@ -97,13 +99,15 @@ export async function restoreBackup({
97
99
  includes?: string[];
98
100
  excludes?: string[];
99
101
  deleteOption: boolean;
102
+ storageId?: string;
103
+ replicationId?: string;
100
104
  }) {
101
105
  const header = new Headers({ 'Content-Type': 'application/json', Accept: 'application/json' });
102
106
  const res = await fetch(`${API_URL}/restores/action/restore`, {
103
107
  method: 'POST',
104
108
  credentials: 'include',
105
109
  headers: header,
106
- body: JSON.stringify({ backupId, planId, target, overwrite, includes, excludes, delete: deleteOption }),
110
+ body: JSON.stringify({ backupId, planId, target, overwrite, includes, excludes, delete: deleteOption, storageId, replicationId }),
107
111
  });
108
112
  const data = await res.json();
109
113
  if (!data.success) {
@@ -132,6 +136,8 @@ export async function getDryRestoreStats({
132
136
  includes,
133
137
  excludes,
134
138
  deleteOption,
139
+ storageId,
140
+ replicationId,
135
141
  }: {
136
142
  backupId: string;
137
143
  planId: string;
@@ -140,13 +146,15 @@ export async function getDryRestoreStats({
140
146
  includes?: string[];
141
147
  excludes?: string[];
142
148
  deleteOption?: boolean;
149
+ storageId?: string;
150
+ replicationId?: string;
143
151
  }) {
144
152
  const header = new Headers({ 'Content-Type': 'application/json', Accept: 'application/json' });
145
153
  const res = await fetch(`${API_URL}/restores/action/dryrestore`, {
146
154
  method: 'POST',
147
155
  credentials: 'include',
148
156
  headers: header,
149
- body: JSON.stringify({ backupId, planId, target, overwrite, includes, excludes, delete: deleteOption }),
157
+ body: JSON.stringify({ backupId, planId, target, overwrite, includes, excludes, delete: deleteOption, storageId, replicationId }),
150
158
  });
151
159
  const data = await res.json();
152
160
  if (!data.success) {
@@ -1,5 +1,5 @@
1
1
  import { API_URL } from '../utils/constants';
2
- import { useQuery, useMutation } from '@tanstack/react-query';
2
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
3
3
  import { useNavigate } from 'react-router';
4
4
 
5
5
  interface LoginCredentials {
@@ -12,22 +12,28 @@ export type InstallType = 'docker' | 'binary' | 'dev';
12
12
  //VALIDATE USER
13
13
  export async function validateAuth() {
14
14
  const res = await fetch(`${API_URL}/user/validate`, { method: 'GET', credentials: 'include' });
15
- if (!res.ok) {
16
- throw new Error('Invalid authentication');
17
- }
15
+
16
+ // Read headers before checking status - middleware sets these on all responses
18
17
  const appVersion = res.headers.get('x-app-version');
19
18
  const serverOS = res.headers.get('x-server-os');
20
19
  const installType = (res.headers.get('x-install-type') || 'dev') as InstallType;
20
+ const setupPending = res.headers.get('x-setup-pending') === '1';
21
21
 
22
22
  (window as any).plutonVersion = appVersion || 'unknown';
23
23
  (window as any).plutonServerOS = serverOS || 'unknown';
24
24
  (window as any).plutonInstallType = installType;
25
+ (window as any).plutonSetupPending = setupPending;
26
+
27
+ if (!res.ok) {
28
+ throw new Error('Invalid authentication');
29
+ }
25
30
 
26
31
  const data = await res.json();
27
32
  return {
28
33
  ...data,
29
34
  appVersion,
30
35
  installType,
36
+ setupPending,
31
37
  };
32
38
  }
33
39
 
@@ -59,10 +65,11 @@ export async function loginUser(credentials: LoginCredentials) {
59
65
  // Add this new hook
60
66
  export function useLogin() {
61
67
  const navigate = useNavigate();
68
+ const queryClient = useQueryClient();
62
69
  return useMutation({
63
70
  mutationFn: loginUser,
64
71
  onSuccess: (res) => {
65
- console.log('res :', res);
72
+ queryClient.removeQueries({ queryKey: ['auth'] });
66
73
  if (res.totpRequired) {
67
74
  navigate('/login/verify-otp');
68
75
  } else {
@@ -87,9 +94,11 @@ export async function logoutUser() {
87
94
 
88
95
  export function useLogout() {
89
96
  const navigate = useNavigate();
97
+ const queryClient = useQueryClient();
90
98
  return useMutation({
91
99
  mutationFn: logoutUser,
92
100
  onSuccess: () => {
101
+ queryClient.removeQueries({ queryKey: ['auth'] });
93
102
  navigate('/login');
94
103
  },
95
104
  });