@plutonhq/core-frontend 0.1.25 → 0.1.27

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 (91) hide show
  1. package/dist-lib/@types/backups.d.ts +4 -1
  2. package/dist-lib/@types/backups.d.ts.map +1 -1
  3. package/dist-lib/@types/plans.d.ts +2 -0
  4. package/dist-lib/@types/plans.d.ts.map +1 -1
  5. package/dist-lib/@types/restores.d.ts +2 -0
  6. package/dist-lib/@types/restores.d.ts.map +1 -1
  7. package/dist-lib/components/Device/DeviceInfo/DeviceInfo.module.scss.js +26 -26
  8. package/dist-lib/components/Plan/BackupEvents/BackupEvents.d.ts.map +1 -1
  9. package/dist-lib/components/Plan/BackupEvents/BackupEvents.js +27 -27
  10. package/dist-lib/components/Plan/BackupEvents/BackupEvents.js.map +1 -1
  11. package/dist-lib/components/Plan/PlanBackups/PlanBackups.d.ts.map +1 -1
  12. package/dist-lib/components/Plan/PlanBackups/PlanBackups.js +26 -26
  13. package/dist-lib/components/Plan/PlanBackups/PlanBackups.js.map +1 -1
  14. package/dist-lib/components/Plan/PlanForm/PlanForm.d.ts.map +1 -1
  15. package/dist-lib/components/Plan/PlanForm/PlanForm.js +64 -80
  16. package/dist-lib/components/Plan/PlanForm/PlanForm.js.map +1 -1
  17. package/dist-lib/components/Plan/PlanHistory/PlanHistory.js +1 -1
  18. package/dist-lib/components/Plan/PlanHistory/PlanHistory.js.map +1 -1
  19. package/dist-lib/components/Plan/PlanPruneModal/PlanPruneModal.d.ts.map +1 -1
  20. package/dist-lib/components/Plan/PlanPruneModal/PlanPruneModal.js +62 -26
  21. package/dist-lib/components/Plan/PlanPruneModal/PlanPruneModal.js.map +1 -1
  22. package/dist-lib/components/Plan/PlanSettings/PlanPruneSettings.d.ts.map +1 -1
  23. package/dist-lib/components/Plan/PlanSettings/PlanPruneSettings.js +138 -62
  24. package/dist-lib/components/Plan/PlanSettings/PlanPruneSettings.js.map +1 -1
  25. package/dist-lib/components/Plan/PlanSettings/PlanSettings.module.scss.js +42 -42
  26. package/dist-lib/components/Plan/PlanSettings/PlanSourceSettings.d.ts.map +1 -1
  27. package/dist-lib/components/Plan/PlanSettings/PlanSourceSettings.js +39 -30
  28. package/dist-lib/components/Plan/PlanSettings/PlanSourceSettings.js.map +1 -1
  29. package/dist-lib/components/Plan/PlanStats/PlanStats.d.ts.map +1 -1
  30. package/dist-lib/components/Plan/PlanStats/PlanStats.js +25 -25
  31. package/dist-lib/components/Plan/PlanStats/PlanStats.js.map +1 -1
  32. package/dist-lib/components/Plan/PlanStats/PlanStats.module.scss.js +1 -1
  33. package/dist-lib/components/common/Icon/Icon.d.ts.map +1 -1
  34. package/dist-lib/components/common/Icon/Icon.js +12 -1
  35. package/dist-lib/components/common/Icon/Icon.js.map +1 -1
  36. package/dist-lib/components/common/StatusLabel/StatusLabel.d.ts +1 -1
  37. package/dist-lib/components/common/StatusLabel/StatusLabel.d.ts.map +1 -1
  38. package/dist-lib/components/common/StatusLabel/StatusLabel.js +17 -12
  39. package/dist-lib/components/common/StatusLabel/StatusLabel.js.map +1 -1
  40. package/dist-lib/components/common/form/IntervalField/IntervalField.d.ts.map +1 -1
  41. package/dist-lib/components/common/form/IntervalField/IntervalField.js +66 -46
  42. package/dist-lib/components/common/form/IntervalField/IntervalField.js.map +1 -1
  43. package/dist-lib/components/common/form/NumberInput/NumberInput.module.scss.js +4 -4
  44. package/dist-lib/components/common/form/StoragePicker/StoragePicker.d.ts.map +1 -1
  45. package/dist-lib/components/common/form/StoragePicker/StoragePicker.js +49 -47
  46. package/dist-lib/components/common/form/StoragePicker/StoragePicker.js.map +1 -1
  47. package/dist-lib/components/common/form/TagsInput/TagsInput.d.ts +2 -1
  48. package/dist-lib/components/common/form/TagsInput/TagsInput.d.ts.map +1 -1
  49. package/dist-lib/components/common/form/TagsInput/TagsInput.js +15 -15
  50. package/dist-lib/components/common/form/TagsInput/TagsInput.js.map +1 -1
  51. package/dist-lib/components/common/form/TimePicker/TimePicker.module.scss.js +13 -13
  52. package/dist-lib/hooks/usePlanSingleActions.js +7 -7
  53. package/dist-lib/hooks/usePlanSingleActions.js.map +1 -1
  54. package/dist-lib/styles/core-frontend.css +1 -1
  55. package/dist-lib/styles/global.scss +6 -0
  56. package/dist-lib/utils/constants.js +1 -1
  57. package/dist-lib/utils/constants.js.map +1 -1
  58. package/dist-lib/utils/helpers.d.ts +2 -0
  59. package/dist-lib/utils/helpers.d.ts.map +1 -1
  60. package/dist-lib/utils/helpers.js +55 -33
  61. package/dist-lib/utils/helpers.js.map +1 -1
  62. package/dist-lib/utils/plans.js +6 -6
  63. package/dist-lib/utils/plans.js.map +1 -1
  64. package/dist-lib/utils.js +34 -33
  65. package/package.json +1 -1
  66. package/src/@types/backups.ts +4 -1
  67. package/src/@types/plans.ts +2 -0
  68. package/src/@types/restores.ts +2 -0
  69. package/src/components/Device/DeviceInfo/DeviceInfo.module.scss +1 -0
  70. package/src/components/Plan/BackupEvents/BackupEvents.tsx +5 -3
  71. package/src/components/Plan/PlanBackups/PlanBackups.tsx +5 -2
  72. package/src/components/Plan/PlanForm/PlanForm.tsx +1 -19
  73. package/src/components/Plan/PlanHistory/PlanHistory.tsx +1 -1
  74. package/src/components/Plan/PlanPruneModal/PlanPruneModal.tsx +54 -11
  75. package/src/components/Plan/PlanSettings/PlanPruneSettings.tsx +145 -61
  76. package/src/components/Plan/PlanSettings/PlanSettings.module.scss +5 -0
  77. package/src/components/Plan/PlanSettings/PlanSourceSettings.tsx +15 -1
  78. package/src/components/Plan/PlanStats/PlanStats.module.scss +3 -0
  79. package/src/components/Plan/PlanStats/PlanStats.tsx +2 -8
  80. package/src/components/common/Icon/Icon.tsx +12 -0
  81. package/src/components/common/StatusLabel/StatusLabel.tsx +7 -1
  82. package/src/components/common/form/IntervalField/IntervalField.tsx +21 -1
  83. package/src/components/common/form/NumberInput/NumberInput.module.scss +1 -0
  84. package/src/components/common/form/StoragePicker/StoragePicker.tsx +8 -1
  85. package/src/components/common/form/TagsInput/TagsInput.tsx +3 -2
  86. package/src/components/common/form/TimePicker/TimePicker.module.scss +1 -0
  87. package/src/hooks/usePlanSingleActions.tsx +2 -2
  88. package/src/styles/global.scss +6 -0
  89. package/src/utils/constants.ts +1 -1
  90. package/src/utils/helpers.ts +25 -0
  91. package/src/utils/plans.ts +3 -3
@@ -5,7 +5,6 @@ import StoragePicker from '../../common/form/StoragePicker/StoragePicker';
5
5
  import PlanStrategySettings from '../PlanSettings/PlanStrategySettings';
6
6
  import PlanSourceSettings from '../PlanSettings/PlanSourceSettings';
7
7
  import { NewPlanSettings } from '../../../@types/plans';
8
- import NumberInput from '../../common/form/NumberInput/NumberInput';
9
8
  import classes from '../AddPlan/AddPlan.module.scss';
10
9
  import PFClasses from './PlanForm.module.scss';
11
10
  import { useGetSettings } from '../../../services/settings';
@@ -254,30 +253,13 @@ const PlanForm = ({
254
253
  <div className={PFClasses.planStep}>
255
254
  <div className={classes.field} style={{ width: '150px' }}>
256
255
  <IntervalField
257
- label="Backup Interval*"
256
+ label="Backup Interval"
258
257
  fieldValue={planSettings.settings.interval}
259
258
  onUpdate={(intervalSettings) =>
260
259
  onPlanSettingsChange({ ...planSettings, settings: { ...planSettings.settings, interval: intervalSettings } })
261
260
  }
262
261
  />
263
262
  </div>
264
-
265
- <div className={classes.field} style={{ width: '150px' }}>
266
- <NumberInput
267
- label="Backups to Keep"
268
- fieldValue={planSettings.settings.prune.snapCount}
269
- onUpdate={(val) =>
270
- onPlanSettingsChange({
271
- ...planSettings,
272
- settings: { ...planSettings.settings, prune: { ...planSettings.settings.prune, snapCount: val } },
273
- })
274
- }
275
- placeholder="5"
276
- min={1}
277
- inline={false}
278
- hint="Number of Active Restorable Backups/Snapshots to Keep regardless of the Removal Policy"
279
- />
280
- </div>
281
263
  <PlanPruneSettings
282
264
  plan={planSettings}
283
265
  onUpdate={(pruneSettings) =>
@@ -52,7 +52,7 @@ const PlanHistory = ({ planId, history, itemsCount = 10 }: PlanHistoryProps) =>
52
52
  <div><b>Changes</b>: ${backupChanges(h.changes)}</div>
53
53
  <div><b>Started</b>: ${formatDateTime(h.started)}</div>
54
54
  <div><b>Ended</b>: ${h.ended ? formatDateTime(h.ended) : ' - '}</div>
55
- <div><b>Duration</b>: ${duration ? formatDuration(duration) : '0ms'} </div>
55
+ <div><b>Duration</b>: ${h.status !== 'started' && duration ? formatDuration(duration) : '-'} </div>
56
56
  `}
57
57
  data-tooltip-place="top"
58
58
  ></div>
@@ -15,6 +15,56 @@ interface PlanPruneModalProps {
15
15
  const PlanPruneModal = ({ planId, method, prune, snapshotsCount, taskPending, close }: PlanPruneModalProps) => {
16
16
  const pruneMutation = usePrunePlan();
17
17
 
18
+ const getBackupMessage = () => {
19
+ switch (prune.policy) {
20
+ case 'keepLast':
21
+ return (
22
+ <>
23
+ Your plan is set to keep the last {prune.snapCount} backups.{' '}
24
+ {prune.snapCount < snapshotsCount
25
+ ? `Running prune now will remove ${snapshotsCount - prune.snapCount} older snapshots, freeing up some storage space.`
26
+ : 'There are no excess snapshots to clean up.'}
27
+ </>
28
+ );
29
+ case 'forgetByAge':
30
+ return (
31
+ <>
32
+ Your plan is set to remove backups older than {planIntervalAgeName(prune.forgetAge || '3m')}, while always keeping at least{' '}
33
+ {prune.snapCount || 1} backup{(prune.snapCount || 1) > 1 ? 's' : ''}. This action will remove any backups that exceed the retention
34
+ policy.
35
+ </>
36
+ );
37
+ case 'custom':
38
+ return (
39
+ <>
40
+ Your plan uses an advanced retention policy
41
+ {prune.keepDailySnaps ? `, keeping daily backups for ${prune.keepDailySnaps} days` : ''}
42
+ {prune.keepWeeklySnaps ? `, weekly backups for ${prune.keepWeeklySnaps} weeks` : ''}
43
+ {prune.keepMonthlySnaps ? `, monthly backups for ${prune.keepMonthlySnaps} months` : ''}, while always keeping at least{' '}
44
+ {prune.snapCount || 1} backup{(prune.snapCount || 1) > 1 ? 's' : ''}. This action will remove any backups that exceed the retention
45
+ policy.
46
+ </>
47
+ );
48
+ case 'disable':
49
+ return <>Pruning is disabled for this plan. No backups will be removed.</>;
50
+ default:
51
+ return (
52
+ <>
53
+ Your plan is set to maintain {prune.snapCount} recent backups.{' '}
54
+ {prune.snapCount < snapshotsCount
55
+ ? `Running prune now will remove ${snapshotsCount - prune.snapCount} older snapshots, freeing up some storage space.`
56
+ : 'There are no excess snapshots to clean up.'}
57
+ </>
58
+ );
59
+ }
60
+ };
61
+
62
+ const canPrune = () => {
63
+ if (prune.policy === 'disable') return false;
64
+ if (prune.policy === 'keepLast' && prune.snapCount >= snapshotsCount) return false;
65
+ return true;
66
+ };
67
+
18
68
  return (
19
69
  <ActionModal
20
70
  title={method === 'sync' ? 'Clean Up Old Revisions' : 'Clean Up Old Backups'}
@@ -23,25 +73,18 @@ const PlanPruneModal = ({ planId, method, prune, snapshotsCount, taskPending, cl
23
73
  {method === 'sync' ? (
24
74
  <>
25
75
  Your plan is set to maintain{' '}
26
- {prune.policy === 'forgetByAge' ? `file revisions newer than ${planIntervalAgeName(prune.forgetAge || '3m')}.` : ''}{' '}
27
- {prune.policy === 'forgetByFileCount' ? `Only keep the last ${prune.snapCount} versions of each file.` : ''} This action will
28
- remove any excess file revisions older than the retention policy.
76
+ {prune.policy === 'forgetByAge' ? `file revisions newer than ${planIntervalAgeName(prune.forgetAge || '3m')}.` : ''} This action
77
+ will remove any excess file revisions older than the retention policy.
29
78
  </>
30
79
  ) : (
31
- <>
32
- Your plan is set to maintain {prune.snapCount} recent backups(snapshots).{' '}
33
- {prune.snapCount < snapshotsCount
34
- ? `Running prune now will remove ${' '}
35
- ${snapshotsCount - prune.snapCount} older snapshots, freeing up some storage space`
36
- : 'There are no excess snapshots to clean up.'}
37
- </>
80
+ getBackupMessage()
38
81
  )}
39
82
  </>
40
83
  }
41
84
  closeModal={() => !pruneMutation.isPending && close()}
42
85
  width="500px"
43
86
  primaryAction={{
44
- title: method === 'sync' ? `Yes, Remove Old Revisions` : prune.snapCount < snapshotsCount ? 'Yes, Remove Old Backups' : '',
87
+ title: method === 'sync' ? 'Yes, Remove Old Revisions' : canPrune() ? 'Yes, Remove Old Backups' : '',
45
88
  type: 'default',
46
89
  isPending: pruneMutation.isPending,
47
90
  action: () =>
@@ -1,9 +1,9 @@
1
1
  import classes from './PlanSettings.module.scss';
2
2
  import Select from '../../common/form/Select/Select';
3
- import Input from '../../common/form/Input/Input';
4
3
  import Tristate from '../../common/form/Tristate/Tristate';
5
4
  import { NewPlanSettings } from '../../../@types/plans';
6
5
  import NumberInput from '../../common/form/NumberInput/NumberInput';
6
+ import Toggle from '../../common/form/Toggle/Toggle';
7
7
 
8
8
  interface PlanPruneSettingsProps {
9
9
  plan: NewPlanSettings;
@@ -11,7 +11,7 @@ interface PlanPruneSettingsProps {
11
11
  }
12
12
 
13
13
  const PlanPruneSettings = ({ plan, onUpdate }: PlanPruneSettingsProps) => {
14
- const pruneSettings = plan.settings?.prune;
14
+ const pruneSettings = plan.settings?.prune || {};
15
15
 
16
16
  return (
17
17
  <>
@@ -19,83 +19,167 @@ const PlanPruneSettings = ({ plan, onUpdate }: PlanPruneSettingsProps) => {
19
19
  <label className={classes.label}>Backup Retention Policy</label>
20
20
  <Select
21
21
  options={[
22
- { label: 'Remove by Age', value: 'forgetByAge' },
23
- { label: 'Remove by Date', value: 'forgetByDate' },
24
- { label: 'Custom Prune Policy', value: 'custom' },
22
+ { label: 'Keep Number of Backups', value: 'keepLast' },
23
+ { label: 'Remove Backups By Age', value: 'forgetByAge' },
24
+ { label: 'Advanced Policy', value: 'custom' },
25
+ { label: 'Keep All Backups (Disable Pruning)', value: 'disable' },
25
26
  ]}
26
- fieldValue={pruneSettings.policy}
27
- onUpdate={(val: string) => onUpdate({ ...pruneSettings, policy: val })}
27
+ fieldValue={pruneSettings.policy || 'keepLast'}
28
+ onUpdate={(val: string) => {
29
+ // Reset snapCount to a default safe value if switching contexts, or retain if preferred
30
+ onUpdate({ ...pruneSettings, policy: val, snapCount: val === 'keepLast' ? 5 : pruneSettings.snapCount });
31
+ }}
28
32
  />
29
33
  </div>
30
- {pruneSettings.policy === 'forgetByAge' && (
34
+
35
+ {/* OPTION 1: KEEP LAST N */}
36
+ {pruneSettings.policy === 'keepLast' && (
31
37
  <div className={classes.field}>
32
- <label className={classes.label}>Remove Backups Older Than</label>
33
- <div className={classes.forgetByAgeField}>
34
- <NumberInput
35
- fieldValue={pruneSettings.forgetAge ? parseInt(pruneSettings.forgetAge.replace(/\D/g, ''), 10) : 3}
36
- onUpdate={(val) =>
37
- onUpdate({
38
- ...pruneSettings,
39
- forgetAge: (pruneSettings.forgetAge || '3m').replace(/\d+/g, val.toString()),
40
- })
41
- }
42
- placeholder="5"
43
- min={1}
44
- full={true}
45
- />
46
- <Tristate
47
- fieldValue={(pruneSettings.forgetAge || '3m').replace(/\d/g, '')}
48
- options={[
49
- { label: 'Days', value: 'd' },
50
- { label: 'Weeks', value: 'w' },
51
- { label: 'Months', value: 'm' },
52
- ]}
53
- onUpdate={(val: string) =>
54
- onUpdate({
55
- ...pruneSettings,
56
- forgetAge: (pruneSettings.forgetAge || '3m').replace(/\D/g, val),
57
- })
58
- }
59
- />
60
- </div>
61
- </div>
62
- )}
63
- {pruneSettings.policy === 'forgetByDate' && (
64
- <div className={classes.field} style={{ width: '200px' }}>
65
- <label className={classes.label}>Remove Backups Older Than</label>
66
- <Input type="date" fieldValue={pruneSettings.forgetDate || ''} onUpdate={(val) => onUpdate({ ...pruneSettings, forgetDate: val })} />
38
+ <NumberInput
39
+ label="Number of Backups to Keep"
40
+ fieldValue={pruneSettings.snapCount || 5}
41
+ onUpdate={(val) => onUpdate({ ...pruneSettings, snapCount: val })}
42
+ placeholder="5"
43
+ min={1}
44
+ hint="Only the most recent 5 backups will be kept."
45
+ inline={false}
46
+ />
67
47
  </div>
68
48
  )}
69
- {pruneSettings.policy === 'custom' && (
49
+
50
+ {/* OPTION 2: REMOVE BY AGE */}
51
+ {pruneSettings.policy === 'forgetByAge' && (
70
52
  <>
71
53
  <div className={classes.field}>
72
- <label className={classes.label}>Custom Policy Settings</label>
73
- <div className={classes.customPolicyOption}>
74
- <span>Keep Daily Backups for </span>
54
+ <label className={classes.label}>Remove Backups Older Than</label>
55
+ <div className={classes.forgetByAgeField}>
75
56
  <NumberInput
76
- fieldValue={pruneSettings.keepDailySnaps || ''}
77
- onUpdate={(val) => onUpdate({ ...pruneSettings, keepDailySnaps: val })}
57
+ fieldValue={pruneSettings.forgetAge ? parseInt(pruneSettings.forgetAge.replace(/\D/g, ''), 10) : 3}
58
+ onUpdate={(val) =>
59
+ onUpdate({
60
+ ...pruneSettings,
61
+ forgetAge: (pruneSettings.forgetAge || '3m').replace(/\d+/g, val.toString()),
62
+ })
63
+ }
64
+ placeholder="3"
65
+ min={1}
66
+ full={true}
67
+ />
68
+ <Tristate
69
+ fieldValue={(pruneSettings.forgetAge || '3m').replace(/\d/g, '')}
70
+ options={[
71
+ { label: 'Days', value: 'd' },
72
+ { label: 'Weeks', value: 'w' },
73
+ { label: 'Months', value: 'm' },
74
+ { label: 'Years', value: 'y' },
75
+ ]}
76
+ onUpdate={(val: string) =>
77
+ onUpdate({
78
+ ...pruneSettings,
79
+ forgetAge: (pruneSettings.forgetAge || '3m').replace(/\D/g, val),
80
+ })
81
+ }
78
82
  />
79
- <span>Days</span>
80
83
  </div>
81
- <div className={classes.customPolicyOption}>
82
- <span>Keep Weekly Backups for </span>
84
+ </div>
85
+
86
+ {/* Fail-safe keeping minimum N backups so they aren't left with 0 backups if backups fail for a month */}
87
+ <div className={classes.field}>
88
+ <label className={classes.label}>Minimum numbers of backups to keep</label>
89
+ <div>
83
90
  <NumberInput
84
- fieldValue={pruneSettings.keepWeeklySnaps || ''}
85
- onUpdate={(val) => onUpdate({ ...pruneSettings, keepWeeklySnaps: val })}
91
+ fieldValue={pruneSettings.snapCount || 1}
92
+ onUpdate={(val) => onUpdate({ ...pruneSettings, snapCount: val })}
93
+ min={1}
94
+ placeholder="3"
95
+ full={true}
86
96
  />
87
- <span>Weeks</span>
88
97
  </div>
89
- <div className={classes.customPolicyOption}>
90
- <span>Keep Monthly Backups for </span>
98
+ </div>
99
+ </>
100
+ )}
101
+
102
+ {/* OPTION 3: ADVANCED */}
103
+ {pruneSettings.policy === 'custom' && (
104
+ <div className={classes.field}>
105
+ <label className={classes.label}>Advanced Policy Settings</label>
106
+ <small className={classes.helperText} style={{ marginBottom: '15px', display: 'block', color: 'var(--text-muted)' }}>
107
+ A backup is kept if it matches <strong>ANY</strong> of the checked rules below.
108
+ </small>
109
+
110
+ {/* Keep Daily */}
111
+ <div className={classes.customPolicyOption}>
112
+ <Toggle
113
+ fieldValue={!!pruneSettings.keepDailySnaps}
114
+ onUpdate={(val) => onUpdate({ ...pruneSettings, keepDailySnaps: val ? 7 : undefined })}
115
+ customClasses={classes.removeRemoteToggle}
116
+ inline={true}
117
+ />
118
+ <span>Keep the latest daily backup for</span>
119
+ <NumberInput
120
+ fieldValue={pruneSettings.keepDailySnaps || ''}
121
+ onUpdate={(val) => onUpdate({ ...pruneSettings, keepDailySnaps: val })}
122
+ min={1}
123
+ />
124
+ <span>Days</span>
125
+ </div>
126
+
127
+ {/* Keep Weekly */}
128
+ <div className={classes.customPolicyOption}>
129
+ <Toggle
130
+ fieldValue={!!pruneSettings.keepWeeklySnaps}
131
+ onUpdate={(val) => onUpdate({ ...pruneSettings, keepWeeklySnaps: val ? 4 : undefined })}
132
+ customClasses={classes.removeRemoteToggle}
133
+ inline={true}
134
+ />
135
+ <span>Keep the latest weekly backup for</span>
136
+ <NumberInput
137
+ fieldValue={pruneSettings.keepWeeklySnaps || ''}
138
+ onUpdate={(val) => onUpdate({ ...pruneSettings, keepWeeklySnaps: val })}
139
+ min={1}
140
+ />
141
+ <span>Weeks</span>
142
+ </div>
143
+
144
+ {/* Keep Monthly */}
145
+ <div className={classes.customPolicyOption}>
146
+ <Toggle
147
+ fieldValue={!!pruneSettings.keepMonthlySnaps}
148
+ onUpdate={(val) => onUpdate({ ...pruneSettings, keepMonthlySnaps: val ? 12 : undefined })}
149
+ customClasses={classes.removeRemoteToggle}
150
+ inline={true}
151
+ />
152
+ <span>Keep the latest monthly backup for</span>
153
+ <NumberInput
154
+ fieldValue={pruneSettings.keepMonthlySnaps || ''}
155
+ onUpdate={(val) => onUpdate({ ...pruneSettings, keepMonthlySnaps: val })}
156
+ min={1}
157
+ />
158
+ <span>Months</span>
159
+ </div>
160
+ {/* Fail-safe keeping minimum N backups so they aren't left with 0 backups if backups fail for a month */}
161
+ <div className={classes.field} style={{ marginTop: '15px' }}>
162
+ <label className={classes.label}>Minimum numbers of backups to keep</label>
163
+ <div>
91
164
  <NumberInput
92
- fieldValue={pruneSettings.keepMonthlySnaps || ''}
93
- onUpdate={(val) => onUpdate({ ...pruneSettings, keepMonthlySnaps: val })}
165
+ fieldValue={pruneSettings.snapCount || 1}
166
+ onUpdate={(val) => onUpdate({ ...pruneSettings, snapCount: val })}
167
+ min={1}
168
+ placeholder="3"
169
+ full={true}
94
170
  />
95
- <span>Months</span>
96
171
  </div>
97
172
  </div>
98
- </>
173
+ </div>
174
+ )}
175
+
176
+ {/* OPTION 4: DISABLE */}
177
+ {pruneSettings.policy === 'disable' && (
178
+ <div className={classes.field}>
179
+ <small className={classes.helperText}>
180
+ Pruning is disabled. Old backups will never be deleted. Make sure you have enough storage!
181
+ </small>
182
+ </div>
99
183
  )}
100
184
  </>
101
185
  );
@@ -113,10 +113,15 @@
113
113
  flex-direction: row;
114
114
  align-items: center;
115
115
  margin-bottom: 10px;
116
+ gap: 10px;
116
117
  & > div {
117
118
  width: 100px;
118
119
  margin: 0 10px;
119
120
  }
121
+ div[class*='_toggleField'] {
122
+ width: auto;
123
+ margin: 0;
124
+ }
120
125
  }
121
126
 
122
127
  .typeFieldActive {
@@ -1,3 +1,4 @@
1
+ import { useEffect } from 'react';
1
2
  import classes from './PlanSettings.module.scss';
2
3
  import PathPicker from '../../common/PathPicker/PathPicker';
3
4
  import { NewPlanSettings } from '../../../@types/plans';
@@ -27,6 +28,19 @@ const PlanSourceSettings = ({ plan, onUpdate, error, isEditing }: PlanSourceSett
27
28
  );
28
29
  }
29
30
 
31
+ // When the device changes, reset the sourceConfig paths to prevent invalid paths from being submitted
32
+ useEffect(() => {
33
+ if (!isEditing) {
34
+ onUpdate({
35
+ ...plan,
36
+ sourceConfig: {
37
+ includes: [],
38
+ excludes: [],
39
+ },
40
+ });
41
+ }
42
+ }, [isEditing, deviceId]);
43
+
30
44
  return (
31
45
  <>
32
46
  <div className={classes.field}>
@@ -47,7 +61,7 @@ const PlanSourceSettings = ({ plan, onUpdate, error, isEditing }: PlanSourceSett
47
61
  onUpdate={(paths) => onUpdate({ ...plan, sourceConfig: { ...paths } })}
48
62
  deviceId={deviceId}
49
63
  single={plan.method === 'sync'}
50
- disallowChange={plan.method === 'sync'}
64
+ disallowChange={plan.method === 'sync' && isEditing}
51
65
  />
52
66
  </div>
53
67
  </>
@@ -1,5 +1,6 @@
1
1
  .planStats {
2
2
  display: flex;
3
+ justify-content: space-between;
3
4
  gap: 20px;
4
5
  .widgetTitle {
5
6
  padding: 2px 6px;
@@ -42,6 +43,7 @@
42
43
  gap: 5px;
43
44
  font-size: 0.75rem;
44
45
  font-weight: 500;
46
+ min-width: 80px;
45
47
  img {
46
48
  width: auto;
47
49
  max-height: 18px;
@@ -55,6 +57,7 @@
55
57
  border: 1px solid var(--line-color);
56
58
  border-radius: 50%;
57
59
  width: 30px;
60
+ min-width: 30px;
58
61
  height: 30px;
59
62
  box-sizing: border-box;
60
63
  padding-top: 4px;
@@ -1,6 +1,6 @@
1
1
  import Icon from '../../common/Icon/Icon';
2
2
  import { Plan } from '../../../@types/plans';
3
- import { formatBytes, formatNumberToK } from '../../../utils/helpers';
3
+ import { formatBytes, formatNumberToK, formatIntervalDisplay } from '../../../utils/helpers';
4
4
  import PlanHistory from '../PlanHistory/PlanHistory';
5
5
  import classes from './PlanStats.module.scss';
6
6
  import PlanStorageInfo from '../PlanStorageInfo/PlanStorageInfo';
@@ -45,13 +45,7 @@ const PlanStats = ({ plan, isSync, lastBackupItem }: PlanStatsProps) => {
45
45
  <div
46
46
  data-tooltip-id="htmlToolTip"
47
47
  data-tooltip-place="top"
48
- data-tooltip-html={
49
- isActive
50
- ? isSync
51
- ? `Syncing changes every ${interval.minutes} minutes`
52
- : `Copying changes ${['hours', 'minutes', 'days'].includes(interval.type) ? interval[interval.type as 'hours' | 'minutes' | 'days'] + interval.type : interval.type}`
53
- : 'Plan is Not Active'
54
- }
48
+ data-tooltip-html={isActive ? `${isSync ? 'Syncing' : 'Copying'} changes ${formatIntervalDisplay(interval)}` : 'Plan is Not Active'}
55
49
  >
56
50
  <Icon type={isActive ? (isSync ? 'reload' : 'copy') : 'pause'} size={16} />
57
51
  <div className={classes.sourceArrow}>→</div>
@@ -732,6 +732,18 @@ const Icon = ({ type, color = 'currentColor', size = 16, title = '', classes = '
732
732
  </g>
733
733
  </IconWrapper>
734
734
  )}
735
+ {type === 'folder-exclude' && (
736
+ <IconWrapper size={size} viewBox="0 0 24 24">
737
+ <path
738
+ fill="none"
739
+ stroke={color}
740
+ strokeLinecap="round"
741
+ strokeLinejoin="round"
742
+ strokeWidth={2}
743
+ d="M13.5 19H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h4l3 3h7a2 2 0 0 1 2 2v4m1 9l-5-5m0 5l5-5"
744
+ ></path>
745
+ </IconWrapper>
746
+ )}
735
747
 
736
748
  {type === 'folders' && (
737
749
  <IconWrapper size={size} viewBox="0 0 24 24">
@@ -1,7 +1,7 @@
1
1
  import Icon from '../Icon/Icon';
2
2
 
3
3
  interface StatusLabelProps {
4
- status: 'completed' | 'cancelled' | 'failed' | 'started';
4
+ status: 'completed' | 'cancelled' | 'failed' | 'started' | 'retrying' | 'initializing';
5
5
  hasError: boolean;
6
6
  }
7
7
 
@@ -30,6 +30,12 @@ const StatusLabel = ({ status, hasError }: StatusLabelProps) => {
30
30
  <Icon type={'loading'} size={15} /> In Progress
31
31
  </div>
32
32
  );
33
+ } else if (status === 'initializing') {
34
+ return (
35
+ <div>
36
+ <Icon type={'loading'} size={15} /> Initializing
37
+ </div>
38
+ );
33
39
  }
34
40
  };
35
41
 
@@ -1,3 +1,4 @@
1
+ import { NumberInput } from '../../..';
1
2
  import { PlanInterval } from '../../../../@types/plans';
2
3
  import Icon from '../../Icon/Icon';
3
4
  import FormField from '../FormField/FormField';
@@ -44,6 +45,7 @@ const IntervalField = ({
44
45
  { label: 'Daily', value: 'daily' },
45
46
  { label: 'Weekly', value: 'weekly' },
46
47
  { label: 'Monthly', value: 'monthly' },
48
+ { label: 'Every x Minutes', value: 'minutes' },
47
49
  { label: 'Every x Hours', value: 'hours' },
48
50
  { label: 'Every x Days', value: 'days' },
49
51
  ]}
@@ -84,7 +86,7 @@ const IntervalField = ({
84
86
  </div>
85
87
  </>
86
88
  )}
87
- {fieldValue.type !== 'hourly' && fieldValue.type !== 'hours' && (
89
+ {fieldValue.type !== 'hourly' && fieldValue.type !== 'hours' && fieldValue.type !== 'minutes' && (
88
90
  <>
89
91
  <span>at</span>
90
92
  <TimePicker fieldValue={fieldValue.time || '10:00AM'} onUpdate={(val) => onUpdate({ ...fieldValue, time: val })} />
@@ -123,6 +125,24 @@ const IntervalField = ({
123
125
  />
124
126
  </>
125
127
  )}
128
+ {fieldValue.type === 'minutes' && (
129
+ <>
130
+ <span>Every</span>
131
+ <NumberInput
132
+ min={5}
133
+ inline={true}
134
+ fieldValue={fieldValue.minutes || 5}
135
+ onUpdate={(minutes) =>
136
+ onUpdate({
137
+ ...fieldValue,
138
+ minutes: minutes,
139
+ })
140
+ }
141
+ placeholder="1"
142
+ />{' '}
143
+ Minutes
144
+ </>
145
+ )}
126
146
  {fieldValue.type === 'hours' && (
127
147
  <>
128
148
  <span>Every</span>
@@ -92,6 +92,7 @@
92
92
  width: 100%;
93
93
  .input {
94
94
  width: 100%;
95
+ min-width: 120px;
95
96
  }
96
97
  }
97
98
  &.fieldHasError {
@@ -70,6 +70,13 @@ const StoragePicker = ({ onUpdate, storagePath = '', storageId, disabled = false
70
70
  }
71
71
  }, [selectedStorage, path]);
72
72
 
73
+ useEffect(() => {
74
+ if (!disabled) {
75
+ setSelectedStorage(null);
76
+ setPath('');
77
+ }
78
+ }, [deviceId, disabled]);
79
+
73
80
  // console.log('Storage path :', path, !disabled && isLocalStorage && !path);
74
81
 
75
82
  return (
@@ -98,7 +105,7 @@ const StoragePicker = ({ onUpdate, storagePath = '', storageId, disabled = false
98
105
  <Input
99
106
  disabled={disabled}
100
107
  fieldValue={path}
101
- onUpdate={(val) => setPath(val.startsWith('/') ? val.slice(1) : val)} //if the val starts with a slash remove it
108
+ onUpdate={(val) => setPath(!isLocalStorage && val.startsWith('/') ? val.slice(1) : val)} //if the val starts with a slash remove it (only for remote storages, local paths need the leading slash)
102
109
  placeholder={isLocalStorage ? 'Select a folder' : hasBucketName ? 'subfolder' : `folder-or-bucket/subfolder`}
103
110
  full={true}
104
111
  required={!disabled && isLocalStorage}
@@ -10,11 +10,12 @@ type TagsInputProps = {
10
10
  icon?: string;
11
11
  type?: string;
12
12
  inline?: boolean;
13
+ hint?: string;
13
14
  fieldValue: string[];
14
15
  onUpdate: (f: string[]) => void;
15
16
  };
16
17
 
17
- const TagsInput = ({ label, description, customClasses = '', fieldValue = [], onUpdate, icon, type = 'tag', inline }: TagsInputProps) => {
18
+ const TagsInput = ({ label, description, customClasses = '', hint = '', fieldValue = [], onUpdate, icon, type = 'tag', inline }: TagsInputProps) => {
18
19
  const [tags, setTags] = useState<string[]>(() => (Array.isArray(fieldValue) ? fieldValue : []));
19
20
  const [newTag, setNewTag] = useState('');
20
21
 
@@ -56,7 +57,7 @@ const TagsInput = ({ label, description, customClasses = '', fieldValue = [], on
56
57
  };
57
58
 
58
59
  return (
59
- <FormField type={'tags'} label={label} description={description} inline={inline} classes={`${classes.tagField} ${customClasses}`}>
60
+ <FormField type={'tags'} label={label} hint={hint} description={description} inline={inline} classes={`${classes.tagField} ${customClasses}`}>
60
61
  <div className={classes.tagBox}>
61
62
  {tags.map((t, i) => (
62
63
  <div className={classes.tag} key={t + i}>
@@ -28,6 +28,7 @@
28
28
 
29
29
  .timeSelect {
30
30
  position: absolute;
31
+ z-index: 33;
31
32
  background-color: var(--field-bg);
32
33
  border: 1px solid var(--field-line-color);
33
34
  padding: 10px;
@@ -50,8 +50,8 @@ export const usePlanSingleActions = (): {
50
50
 
51
51
  const sortedHistory = [...(plan.backups || [])].sort((a, b) => b.started - a.started);
52
52
  const activeBackups = sortedHistory.filter((s) => s.inProgress);
53
- const activeRestores = (plan.restores || []).filter((s) => s.status === 'started');
54
- const lastBackupItem = sortedHistory[0];
53
+ const activeRestores = (plan.restores || []).filter((s) => s.inProgress);
54
+ const lastBackupItem = isSync ? sortedHistory.filter((s) => s.status === 'completed')[0] : sortedHistory[0];
55
55
  const actionInProgress = activeBackups.length > 0 || activeRestores.length > 0;
56
56
  const taskPending = pauseMutation.isPending || resumeMutation.isPending || performBackupMutation.isPending;
57
57