@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.
- package/dist-lib/@types/backups.d.ts +4 -1
- package/dist-lib/@types/backups.d.ts.map +1 -1
- package/dist-lib/@types/plans.d.ts +2 -0
- package/dist-lib/@types/plans.d.ts.map +1 -1
- package/dist-lib/@types/restores.d.ts +2 -0
- package/dist-lib/@types/restores.d.ts.map +1 -1
- package/dist-lib/components/Device/DeviceInfo/DeviceInfo.module.scss.js +26 -26
- package/dist-lib/components/Plan/BackupEvents/BackupEvents.d.ts.map +1 -1
- package/dist-lib/components/Plan/BackupEvents/BackupEvents.js +27 -27
- package/dist-lib/components/Plan/BackupEvents/BackupEvents.js.map +1 -1
- package/dist-lib/components/Plan/PlanBackups/PlanBackups.d.ts.map +1 -1
- package/dist-lib/components/Plan/PlanBackups/PlanBackups.js +26 -26
- package/dist-lib/components/Plan/PlanBackups/PlanBackups.js.map +1 -1
- package/dist-lib/components/Plan/PlanForm/PlanForm.d.ts.map +1 -1
- package/dist-lib/components/Plan/PlanForm/PlanForm.js +64 -80
- package/dist-lib/components/Plan/PlanForm/PlanForm.js.map +1 -1
- package/dist-lib/components/Plan/PlanHistory/PlanHistory.js +1 -1
- package/dist-lib/components/Plan/PlanHistory/PlanHistory.js.map +1 -1
- package/dist-lib/components/Plan/PlanPruneModal/PlanPruneModal.d.ts.map +1 -1
- package/dist-lib/components/Plan/PlanPruneModal/PlanPruneModal.js +62 -26
- package/dist-lib/components/Plan/PlanPruneModal/PlanPruneModal.js.map +1 -1
- package/dist-lib/components/Plan/PlanSettings/PlanPruneSettings.d.ts.map +1 -1
- package/dist-lib/components/Plan/PlanSettings/PlanPruneSettings.js +138 -62
- package/dist-lib/components/Plan/PlanSettings/PlanPruneSettings.js.map +1 -1
- package/dist-lib/components/Plan/PlanSettings/PlanSettings.module.scss.js +42 -42
- package/dist-lib/components/Plan/PlanSettings/PlanSourceSettings.d.ts.map +1 -1
- package/dist-lib/components/Plan/PlanSettings/PlanSourceSettings.js +39 -30
- package/dist-lib/components/Plan/PlanSettings/PlanSourceSettings.js.map +1 -1
- package/dist-lib/components/Plan/PlanStats/PlanStats.d.ts.map +1 -1
- package/dist-lib/components/Plan/PlanStats/PlanStats.js +25 -25
- package/dist-lib/components/Plan/PlanStats/PlanStats.js.map +1 -1
- package/dist-lib/components/Plan/PlanStats/PlanStats.module.scss.js +1 -1
- package/dist-lib/components/common/Icon/Icon.d.ts.map +1 -1
- package/dist-lib/components/common/Icon/Icon.js +12 -1
- package/dist-lib/components/common/Icon/Icon.js.map +1 -1
- package/dist-lib/components/common/StatusLabel/StatusLabel.d.ts +1 -1
- package/dist-lib/components/common/StatusLabel/StatusLabel.d.ts.map +1 -1
- package/dist-lib/components/common/StatusLabel/StatusLabel.js +17 -12
- package/dist-lib/components/common/StatusLabel/StatusLabel.js.map +1 -1
- package/dist-lib/components/common/form/IntervalField/IntervalField.d.ts.map +1 -1
- package/dist-lib/components/common/form/IntervalField/IntervalField.js +66 -46
- package/dist-lib/components/common/form/IntervalField/IntervalField.js.map +1 -1
- package/dist-lib/components/common/form/NumberInput/NumberInput.module.scss.js +4 -4
- package/dist-lib/components/common/form/StoragePicker/StoragePicker.d.ts.map +1 -1
- package/dist-lib/components/common/form/StoragePicker/StoragePicker.js +49 -47
- package/dist-lib/components/common/form/StoragePicker/StoragePicker.js.map +1 -1
- package/dist-lib/components/common/form/TagsInput/TagsInput.d.ts +2 -1
- package/dist-lib/components/common/form/TagsInput/TagsInput.d.ts.map +1 -1
- package/dist-lib/components/common/form/TagsInput/TagsInput.js +15 -15
- package/dist-lib/components/common/form/TagsInput/TagsInput.js.map +1 -1
- package/dist-lib/components/common/form/TimePicker/TimePicker.module.scss.js +13 -13
- package/dist-lib/hooks/usePlanSingleActions.js +7 -7
- package/dist-lib/hooks/usePlanSingleActions.js.map +1 -1
- package/dist-lib/styles/core-frontend.css +1 -1
- package/dist-lib/styles/global.scss +6 -0
- package/dist-lib/utils/constants.js +1 -1
- package/dist-lib/utils/constants.js.map +1 -1
- package/dist-lib/utils/helpers.d.ts +2 -0
- package/dist-lib/utils/helpers.d.ts.map +1 -1
- package/dist-lib/utils/helpers.js +55 -33
- package/dist-lib/utils/helpers.js.map +1 -1
- package/dist-lib/utils/plans.js +6 -6
- package/dist-lib/utils/plans.js.map +1 -1
- package/dist-lib/utils.js +34 -33
- package/package.json +1 -1
- package/src/@types/backups.ts +4 -1
- package/src/@types/plans.ts +2 -0
- package/src/@types/restores.ts +2 -0
- package/src/components/Device/DeviceInfo/DeviceInfo.module.scss +1 -0
- package/src/components/Plan/BackupEvents/BackupEvents.tsx +5 -3
- package/src/components/Plan/PlanBackups/PlanBackups.tsx +5 -2
- package/src/components/Plan/PlanForm/PlanForm.tsx +1 -19
- package/src/components/Plan/PlanHistory/PlanHistory.tsx +1 -1
- package/src/components/Plan/PlanPruneModal/PlanPruneModal.tsx +54 -11
- package/src/components/Plan/PlanSettings/PlanPruneSettings.tsx +145 -61
- package/src/components/Plan/PlanSettings/PlanSettings.module.scss +5 -0
- package/src/components/Plan/PlanSettings/PlanSourceSettings.tsx +15 -1
- package/src/components/Plan/PlanStats/PlanStats.module.scss +3 -0
- package/src/components/Plan/PlanStats/PlanStats.tsx +2 -8
- package/src/components/common/Icon/Icon.tsx +12 -0
- package/src/components/common/StatusLabel/StatusLabel.tsx +7 -1
- package/src/components/common/form/IntervalField/IntervalField.tsx +21 -1
- package/src/components/common/form/NumberInput/NumberInput.module.scss +1 -0
- package/src/components/common/form/StoragePicker/StoragePicker.tsx +8 -1
- package/src/components/common/form/TagsInput/TagsInput.tsx +3 -2
- package/src/components/common/form/TimePicker/TimePicker.module.scss +1 -0
- package/src/hooks/usePlanSingleActions.tsx +2 -2
- package/src/styles/global.scss +6 -0
- package/src/utils/constants.ts +1 -1
- package/src/utils/helpers.ts +25 -0
- 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) : '
|
|
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
|
-
|
|
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' ?
|
|
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: '
|
|
23
|
-
{ label: 'Remove
|
|
24
|
-
{ label: '
|
|
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) =>
|
|
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
|
-
|
|
34
|
+
|
|
35
|
+
{/* OPTION 1: KEEP LAST N */}
|
|
36
|
+
{pruneSettings.policy === 'keepLast' && (
|
|
31
37
|
<div className={classes.field}>
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
49
|
+
|
|
50
|
+
{/* OPTION 2: REMOVE BY AGE */}
|
|
51
|
+
{pruneSettings.policy === 'forgetByAge' && (
|
|
70
52
|
<>
|
|
71
53
|
<div className={classes.field}>
|
|
72
|
-
<label className={classes.label}>
|
|
73
|
-
<div className={classes.
|
|
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.
|
|
77
|
-
onUpdate={(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
|
-
|
|
82
|
-
|
|
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.
|
|
85
|
-
onUpdate={(val) => onUpdate({ ...pruneSettings,
|
|
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
|
-
|
|
90
|
-
|
|
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.
|
|
93
|
-
onUpdate={(val) => onUpdate({ ...pruneSettings,
|
|
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>
|
|
@@ -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}>
|
|
@@ -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.
|
|
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
|
|