@plutonhq/core-frontend 0.1.32 → 0.1.34

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 (135) hide show
  1. package/dist-lib/@types/plans.d.ts +4 -0
  2. package/dist-lib/@types/plans.d.ts.map +1 -1
  3. package/dist-lib/components/Device/DeviceResticSettings/DeviceResticSettings.js +4 -4
  4. package/dist-lib/components/Device/DeviceResticSettings/DeviceResticSettings.js.map +1 -1
  5. package/dist-lib/components/Plan/AddPlan/AddPlan.d.ts.map +1 -1
  6. package/dist-lib/components/Plan/AddPlan/AddPlan.js +29 -24
  7. package/dist-lib/components/Plan/AddPlan/AddPlan.js.map +1 -1
  8. package/dist-lib/components/Plan/BackupProgress/BackupProgress.js +34 -33
  9. package/dist-lib/components/Plan/BackupProgress/BackupProgress.js.map +1 -1
  10. package/dist-lib/components/Plan/FilterPlans/FilterPlans.d.ts +9 -0
  11. package/dist-lib/components/Plan/FilterPlans/FilterPlans.d.ts.map +1 -0
  12. package/dist-lib/components/Plan/FilterPlans/FilterPlans.js +117 -0
  13. package/dist-lib/components/Plan/FilterPlans/FilterPlans.js.map +1 -0
  14. package/dist-lib/components/Plan/FilterPlans/FilterPlans.module.scss.js +20 -0
  15. package/dist-lib/components/Plan/FilterPlans/FilterPlans.module.scss.js.map +1 -0
  16. package/dist-lib/components/Plan/PlanForm/PlanForm.d.ts +4 -2
  17. package/dist-lib/components/Plan/PlanForm/PlanForm.d.ts.map +1 -1
  18. package/dist-lib/components/Plan/PlanForm/PlanForm.js +33 -29
  19. package/dist-lib/components/Plan/PlanForm/PlanForm.js.map +1 -1
  20. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.d.ts +2 -1
  21. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.d.ts.map +1 -1
  22. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.js +85 -57
  23. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.js.map +1 -1
  24. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.module.scss.js +11 -9
  25. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.module.scss.js.map +1 -1
  26. package/dist-lib/components/Plan/PlanItems/PlanItem.js +1 -1
  27. package/dist-lib/components/Plan/PlanItems/PlanItem.js.map +1 -1
  28. package/dist-lib/components/Plan/PlanRepair/PlanRepair.d.ts +9 -0
  29. package/dist-lib/components/Plan/PlanRepair/PlanRepair.d.ts.map +1 -0
  30. package/dist-lib/components/Plan/PlanRepair/PlanRepair.js +262 -0
  31. package/dist-lib/components/Plan/PlanRepair/PlanRepair.js.map +1 -0
  32. package/dist-lib/components/Plan/PlanRepair/PlanRepair.module.scss.js +14 -0
  33. package/dist-lib/components/Plan/PlanRepair/PlanRepair.module.scss.js.map +1 -0
  34. package/dist-lib/components/Plan/PlanSettings/PlanAdvancedSettings.d.ts +4 -2
  35. package/dist-lib/components/Plan/PlanSettings/PlanAdvancedSettings.d.ts.map +1 -1
  36. package/dist-lib/components/Plan/PlanSettings/PlanAdvancedSettings.js +24 -22
  37. package/dist-lib/components/Plan/PlanSettings/PlanAdvancedSettings.js.map +1 -1
  38. package/dist-lib/components/Plan/PlanSettings/PlanGeneralSettings.d.ts +4 -2
  39. package/dist-lib/components/Plan/PlanSettings/PlanGeneralSettings.d.ts.map +1 -1
  40. package/dist-lib/components/Plan/PlanSettings/PlanGeneralSettings.js +39 -28
  41. package/dist-lib/components/Plan/PlanSettings/PlanGeneralSettings.js.map +1 -1
  42. package/dist-lib/components/Plan/PlanSettings/PlanPerformanceSettings.js +2 -2
  43. package/dist-lib/components/Plan/PlanSettings/PlanPerformanceSettings.js.map +1 -1
  44. package/dist-lib/components/Plan/PlanSettings/PlanSettings.module.scss.js +66 -64
  45. package/dist-lib/components/Plan/PlanSettings/PlanSettings.module.scss.js.map +1 -1
  46. package/dist-lib/components/Plan/PlanSizeChart/PlanSizeChart.d.ts +7 -0
  47. package/dist-lib/components/Plan/PlanSizeChart/PlanSizeChart.d.ts.map +1 -0
  48. package/dist-lib/components/Plan/PlanSizeChart/PlanSizeChart.js +116 -0
  49. package/dist-lib/components/Plan/PlanSizeChart/PlanSizeChart.js.map +1 -0
  50. package/dist-lib/components/Plan/PlanSizeChart/PlanSizeChart.module.scss.js +20 -0
  51. package/dist-lib/components/Plan/PlanSizeChart/PlanSizeChart.module.scss.js.map +1 -0
  52. package/dist-lib/components/Plan/PlanStats/PlanStats.d.ts.map +1 -1
  53. package/dist-lib/components/Plan/PlanStats/PlanStats.js +29 -30
  54. package/dist-lib/components/Plan/PlanStats/PlanStats.js.map +1 -1
  55. package/dist-lib/components/Plan/PlanStats/PlanStats.module.scss.js +16 -14
  56. package/dist-lib/components/Plan/PlanStats/PlanStats.module.scss.js.map +1 -1
  57. package/dist-lib/components/common/FileManager/FileManager.module.scss.js +18 -16
  58. package/dist-lib/components/common/FileManager/FileManager.module.scss.js.map +1 -1
  59. package/dist-lib/components/common/Icon/Icon.d.ts.map +1 -1
  60. package/dist-lib/components/common/Icon/Icon.js +395 -378
  61. package/dist-lib/components/common/Icon/Icon.js.map +1 -1
  62. package/dist-lib/components/common/SortItems/SortItems.d.ts +2 -1
  63. package/dist-lib/components/common/SortItems/SortItems.d.ts.map +1 -1
  64. package/dist-lib/components/common/SortItems/SortItems.js +14 -14
  65. package/dist-lib/components/common/SortItems/SortItems.js.map +1 -1
  66. package/dist-lib/components/common/SortItems/SortItems.module.scss.js +1 -1
  67. package/dist-lib/components/common/form/MultiSelect/MultiSelect.module.scss.js +16 -16
  68. package/dist-lib/components/index.d.ts +2 -0
  69. package/dist-lib/components/index.d.ts.map +1 -1
  70. package/dist-lib/components.js +199 -195
  71. package/dist-lib/components.js.map +1 -1
  72. package/dist-lib/hooks/usePlanSingleActions.d.ts.map +1 -1
  73. package/dist-lib/hooks/usePlanSingleActions.js +22 -19
  74. package/dist-lib/hooks/usePlanSingleActions.js.map +1 -1
  75. package/dist-lib/node_modules/.pnpm/@kurkle_color@0.3.4/node_modules/@kurkle/color/dist/color.esm.js +449 -0
  76. package/dist-lib/node_modules/.pnpm/@kurkle_color@0.3.4/node_modules/@kurkle/color/dist/color.esm.js.map +1 -0
  77. package/dist-lib/node_modules/.pnpm/chart.js@4.5.1/node_modules/chart.js/dist/chart.js +5219 -0
  78. package/dist-lib/node_modules/.pnpm/chart.js@4.5.1/node_modules/chart.js/dist/chart.js.map +1 -0
  79. package/dist-lib/node_modules/.pnpm/chart.js@4.5.1/node_modules/chart.js/dist/chunks/helpers.dataset.js +1691 -0
  80. package/dist-lib/node_modules/.pnpm/chart.js@4.5.1/node_modules/chart.js/dist/chunks/helpers.dataset.js.map +1 -0
  81. package/dist-lib/node_modules/.pnpm/react-chartjs-2@5.3.1_chart.js@4.5.1_react@18.3.1/node_modules/react-chartjs-2/dist/index.js +93 -0
  82. package/dist-lib/node_modules/.pnpm/react-chartjs-2@5.3.1_chart.js@4.5.1_react@18.3.1/node_modules/react-chartjs-2/dist/index.js.map +1 -0
  83. package/dist-lib/routes/Login/Login.d.ts.map +1 -1
  84. package/dist-lib/routes/Login/Login.js +45 -36
  85. package/dist-lib/routes/Login/Login.js.map +1 -1
  86. package/dist-lib/routes/PlanSingle/PlanSingle.d.ts.map +1 -1
  87. package/dist-lib/routes/PlanSingle/PlanSingle.js +131 -118
  88. package/dist-lib/routes/PlanSingle/PlanSingle.js.map +1 -1
  89. package/dist-lib/routes/Plans/Plans.d.ts.map +1 -1
  90. package/dist-lib/routes/Plans/Plans.js +77 -51
  91. package/dist-lib/routes/Plans/Plans.js.map +1 -1
  92. package/dist-lib/services/plans.d.ts +33 -5
  93. package/dist-lib/services/plans.d.ts.map +1 -1
  94. package/dist-lib/services/plans.js +92 -67
  95. package/dist-lib/services/plans.js.map +1 -1
  96. package/dist-lib/services.js +93 -91
  97. package/dist-lib/styles/core-frontend.css +1 -1
  98. package/dist-lib/utils/helpers.d.ts +2 -0
  99. package/dist-lib/utils/helpers.d.ts.map +1 -1
  100. package/dist-lib/utils/helpers.js +68 -42
  101. package/dist-lib/utils/helpers.js.map +1 -1
  102. package/dist-lib/utils.js +36 -34
  103. package/package.json +3 -1
  104. package/src/@types/plans.ts +5 -0
  105. package/src/components/Device/DeviceResticSettings/DeviceResticSettings.tsx +2 -2
  106. package/src/components/Plan/AddPlan/AddPlan.tsx +22 -16
  107. package/src/components/Plan/BackupProgress/BackupProgress.tsx +1 -1
  108. package/src/components/Plan/FilterPlans/FilterPlans.module.scss +65 -0
  109. package/src/components/Plan/FilterPlans/FilterPlans.tsx +126 -0
  110. package/src/components/Plan/PlanForm/PlanForm.tsx +7 -1
  111. package/src/components/Plan/PlanIntegrity/PlanIntegrity.module.scss +19 -0
  112. package/src/components/Plan/PlanIntegrity/PlanIntegrity.tsx +40 -3
  113. package/src/components/Plan/PlanItems/PlanItem.tsx +1 -1
  114. package/src/components/Plan/PlanRepair/PlanRepair.module.scss +53 -0
  115. package/src/components/Plan/PlanRepair/PlanRepair.tsx +243 -0
  116. package/src/components/Plan/PlanSettings/PlanAdvancedSettings.tsx +6 -2
  117. package/src/components/Plan/PlanSettings/PlanGeneralSettings.tsx +14 -2
  118. package/src/components/Plan/PlanSettings/PlanPerformanceSettings.tsx +2 -2
  119. package/src/components/Plan/PlanSettings/PlanSettings.module.scss +8 -0
  120. package/src/components/Plan/PlanSizeChart/PlanSizeChart.module.scss +76 -0
  121. package/src/components/Plan/PlanSizeChart/PlanSizeChart.tsx +166 -0
  122. package/src/components/Plan/PlanStats/PlanStats.module.scss +16 -2
  123. package/src/components/Plan/PlanStats/PlanStats.tsx +8 -11
  124. package/src/components/common/FileManager/FileManager.module.scss +7 -0
  125. package/src/components/common/Icon/Icon.tsx +21 -0
  126. package/src/components/common/SortItems/SortItems.module.scss +3 -2
  127. package/src/components/common/SortItems/SortItems.tsx +6 -3
  128. package/src/components/common/form/MultiSelect/MultiSelect.module.scss +1 -0
  129. package/src/components/index.ts +2 -0
  130. package/src/hooks/usePlanSingleActions.tsx +26 -23
  131. package/src/routes/Login/Login.tsx +8 -2
  132. package/src/routes/PlanSingle/PlanSingle.tsx +17 -0
  133. package/src/routes/Plans/Plans.tsx +70 -35
  134. package/src/services/plans.ts +40 -4
  135. package/src/utils/helpers.ts +25 -0
@@ -1,4 +1,4 @@
1
- import { NewPlanSettings } from '../../../@types/plans';
1
+ import { NewPlanSettings, PlanAddRunSettings } from '../../../@types/plans';
2
2
  import NumberInput from '../../common/form/NumberInput/NumberInput';
3
3
  import Toggle from '../../common/form/Toggle/Toggle';
4
4
  import classes from './PlanSettings.module.scss';
@@ -7,9 +7,11 @@ interface PlanGeneralSettingsProps {
7
7
  settings: NewPlanSettings['settings'];
8
8
  onUpdate: (settings: NewPlanSettings['settings']) => void;
9
9
  isEditing: boolean;
10
+ runSettings?: PlanAddRunSettings;
11
+ setRunSettings?: (runSettings: PlanAddRunSettings) => void;
10
12
  }
11
13
 
12
- const PlanGeneralSettings = ({ settings, onUpdate, isEditing }: PlanGeneralSettingsProps) => {
14
+ const PlanGeneralSettings = ({ settings, onUpdate, isEditing, runSettings, setRunSettings }: PlanGeneralSettingsProps) => {
13
15
  const { encryption, compression, retries, retryDelay } = settings;
14
16
  return (
15
17
  <>
@@ -48,6 +50,16 @@ const PlanGeneralSettings = ({ settings, onUpdate, isEditing }: PlanGeneralSetti
48
50
  inline={false}
49
51
  />
50
52
  </div>
53
+ {!isEditing && setRunSettings && (
54
+ <div className={`${classes.field} ${classes.runNowField}`}>
55
+ <label className={classes.label}>Run Backup Now</label>
56
+ <Toggle
57
+ fieldValue={runSettings?.runNow ?? true}
58
+ onUpdate={(val: boolean) => setRunSettings && setRunSettings({ ...runSettings, runNow: val })}
59
+ description={'Run backup immediately after creating the plan'}
60
+ />
61
+ </div>
62
+ )}
51
63
  </>
52
64
  );
53
65
  };
@@ -34,8 +34,8 @@ const PlanPerformanceSettings = ({ plan, onUpdate }: PlanPerformanceSettingsProp
34
34
  <div className={classes.field}>
35
35
  <NumberInput
36
36
  label="Packet Size (MB)"
37
- fieldValue={perfSettings?.packSize ? parseInt(perfSettings.packSize.replace('MiB', ''), 10) : ''}
38
- onUpdate={(val) => onUpdate({ ...perfSettings, packSize: val + 'MiB' })}
37
+ fieldValue={perfSettings?.packSize ? parseInt(perfSettings.packSize, 10) : ''}
38
+ onUpdate={(val) => onUpdate({ ...perfSettings, packSize: val ? val.toString() : '' })}
39
39
  min={0}
40
40
  max={4000}
41
41
  />
@@ -26,6 +26,14 @@
26
26
  }
27
27
  }
28
28
 
29
+ .runNowField {
30
+ padding: 12px;
31
+ border: 1px solid var(--primary-color-mid);
32
+ border-radius: 6px;
33
+ box-sizing: border-box;
34
+ background: var(--background-color);
35
+ }
36
+
29
37
  .label {
30
38
  font-weight: 600;
31
39
  display: block;
@@ -0,0 +1,76 @@
1
+ .chartWrap {
2
+ position: relative;
3
+ width: 100%;
4
+ height: 100%;
5
+ min-height: 60px;
6
+ }
7
+
8
+ .chartCanvas {
9
+ position: absolute;
10
+ inset: 0;
11
+ }
12
+
13
+ .rangeSelector {
14
+ position: absolute;
15
+ top: -28px;
16
+ right: 0;
17
+ z-index: 2;
18
+ }
19
+
20
+ .rangeBtn {
21
+ background: var(--content-background-color);
22
+ color: var(--content-text-color-light);
23
+ border-radius: 4px;
24
+ font-size: 0.6875rem;
25
+ font-weight: 600;
26
+ padding: 2px 8px;
27
+ cursor: pointer;
28
+ line-height: 1.4;
29
+
30
+ &:hover {
31
+ color: var(--primary-color);
32
+ border-color: var(--primary-color);
33
+ }
34
+ }
35
+
36
+ .rangeMenu {
37
+ position: absolute;
38
+ top: calc(100% + 4px);
39
+ right: 0;
40
+ margin: 0;
41
+ padding: 4px 0;
42
+ list-style: none;
43
+ background: var(--content-background-color);
44
+ border: 1px solid var(--line-color);
45
+ border-radius: 4px;
46
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
47
+ min-width: 80px;
48
+
49
+ li {
50
+ padding: 4px 10px;
51
+ font-size: 0.75rem;
52
+ cursor: pointer;
53
+ color: var(--content-text-color);
54
+
55
+ &:hover {
56
+ background: var(--primary-color-light);
57
+ color: var(--primary-color);
58
+ }
59
+
60
+ &.active {
61
+ color: var(--primary-color);
62
+ font-weight: 600;
63
+ }
64
+ }
65
+ }
66
+
67
+ .empty {
68
+ display: flex;
69
+ align-items: center;
70
+ justify-content: center;
71
+ gap: 6px;
72
+ height: 100%;
73
+ font-size: 0.75rem;
74
+ color: var(--content-text-color-light);
75
+ opacity: 0.7;
76
+ }
@@ -0,0 +1,166 @@
1
+ import { useMemo, useRef, useState, useEffect } from 'react';
2
+ import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Filler, Tooltip, Legend, ChartOptions } from 'chart.js';
3
+ import { Line } from 'react-chartjs-2';
4
+ import Icon from '../../common/Icon/Icon';
5
+ import { Backup } from '../../../@types/backups';
6
+ import { formatBytes, formatNumberToK, isDarkMode } from '../../../utils/helpers';
7
+ import classes from './PlanSizeChart.module.scss';
8
+
9
+ ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Filler, Tooltip, Legend);
10
+
11
+ type RangeKey = '7d' | '14d' | '1m' | '3m' | '6m';
12
+
13
+ const RANGE_OPTIONS: { key: RangeKey; label: string; labelFull: string; days: number }[] = [
14
+ { key: '7d', label: '7d', labelFull: '7 days', days: 7 },
15
+ { key: '14d', label: '14d', labelFull: '14 days', days: 14 },
16
+ { key: '1m', label: '1m', labelFull: '1 month', days: 30 },
17
+ { key: '3m', label: '3m', labelFull: '3 months', days: 90 },
18
+ { key: '6m', label: '6m', labelFull: '6 months', days: 180 },
19
+ ];
20
+
21
+ interface PlanSizeChartProps {
22
+ backups: Backup[];
23
+ }
24
+
25
+ const PlanSizeChart = ({ backups }: PlanSizeChartProps) => {
26
+ const [range, setRange] = useState<RangeKey>('3m');
27
+ const [open, setOpen] = useState(false);
28
+ const dropdownRef = useRef<HTMLDivElement>(null);
29
+
30
+ useEffect(() => {
31
+ if (!open) return;
32
+ const onClick = (e: MouseEvent) => {
33
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
34
+ setOpen(false);
35
+ }
36
+ };
37
+ document.addEventListener('mousedown', onClick);
38
+ return () => document.removeEventListener('mousedown', onClick);
39
+ }, [open]);
40
+
41
+ const activeRange = RANGE_OPTIONS.find((r) => r.key === range) || RANGE_OPTIONS[3];
42
+
43
+ const filtered = useMemo(() => {
44
+ const cutoff = Date.now() - activeRange.days * 24 * 60 * 60 * 1000;
45
+ // if there is only one backup duplicate the first backup to make 2 items
46
+ // so that the graph is never empty and shows the size even if there is only one backup in the selected range
47
+ const theBackups = backups && backups.length === 1 ? [backups[0], backups[0]] : backups || [];
48
+ return [...theBackups]
49
+ .filter((b) => {
50
+ const t = new Date(b.started).getTime();
51
+ return !isNaN(t) && t >= cutoff;
52
+ })
53
+ .sort((a, b) => new Date(a.started).getTime() - new Date(b.started).getTime());
54
+ }, [backups, activeRange.days]);
55
+
56
+ const labels = filtered.map((b) => new Date(b.started).toLocaleString());
57
+ const sizeData = filtered.map((b) => b.totalSize || 0);
58
+ const filesData = filtered.map((b) => b.totalFiles || 0);
59
+
60
+ const data = {
61
+ labels,
62
+ datasets: [
63
+ {
64
+ label: 'Size',
65
+ data: sizeData,
66
+ yAxisID: 'ySize',
67
+ borderColor: 'transparent',
68
+ backgroundColor: 'rgba(87, 132, 255, 0.12)',
69
+ fill: true,
70
+ tension: 0.4,
71
+ borderWidth: 1.5,
72
+ pointRadius: 0,
73
+ pointHoverRadius: 4,
74
+ pointHoverBackgroundColor: 'rgba(87, 90, 255, 1)',
75
+ },
76
+ {
77
+ label: 'Files',
78
+ data: filesData,
79
+ yAxisID: 'yFiles',
80
+ borderColor: '#9a9bff',
81
+ backgroundColor: 'transparent',
82
+ borderDash: [3, 3],
83
+ fill: false,
84
+ tension: 0.4,
85
+ borderWidth: 1.2,
86
+ pointRadius: 0,
87
+ pointHoverRadius: 4,
88
+ pointHoverBackgroundColor: 'rgba(87, 90, 255, 1)',
89
+ },
90
+ ],
91
+ };
92
+
93
+ const options: ChartOptions<'line'> = {
94
+ responsive: true,
95
+ animation: false,
96
+ maintainAspectRatio: false,
97
+ interaction: { mode: 'index', intersect: false },
98
+ plugins: {
99
+ legend: { display: false },
100
+ tooltip: {
101
+ displayColors: false,
102
+ backgroundColor: isDarkMode ? 'rgba(0, 0, 0, 0.9)' : 'rgba(255, 255, 255, 1)',
103
+ titleColor: isDarkMode ? '#fff' : '#666',
104
+ bodyColor: isDarkMode ? '#ccc' : '#888',
105
+ padding: 8,
106
+ titleFont: { size: 11 },
107
+ bodyFont: { size: 11 },
108
+ callbacks: {
109
+ title: (items) => {
110
+ const idx = items[0]?.dataIndex ?? 0;
111
+ const b = filtered[idx];
112
+ return b ? new Date(b.started).toLocaleString() : '';
113
+ },
114
+ label: (ctx) => {
115
+ if (ctx.dataset.label === 'Size') return `Size: ${formatBytes(ctx.parsed.y || 0)}`;
116
+ return `Files: ${formatNumberToK(ctx.parsed.y || 0)}`;
117
+ },
118
+ },
119
+ },
120
+ },
121
+ scales: {
122
+ x: { display: false },
123
+ ySize: { display: false, beginAtZero: true },
124
+ yFiles: { display: false, beginAtZero: true, position: 'right' },
125
+ },
126
+ };
127
+
128
+ return (
129
+ <div className={classes.chartWrap}>
130
+ <div className={classes.rangeSelector} ref={dropdownRef}>
131
+ <button type="button" className={classes.rangeBtn} onClick={() => setOpen((v) => !v)}>
132
+ {activeRange.label}
133
+ </button>
134
+ {open && (
135
+ <ul className={classes.rangeMenu}>
136
+ {RANGE_OPTIONS.slice()
137
+ .reverse()
138
+ .map((opt) => (
139
+ <li
140
+ key={opt.key}
141
+ className={opt.key === range ? classes.active : ''}
142
+ onClick={() => {
143
+ setRange(opt.key);
144
+ setOpen(false);
145
+ }}
146
+ >
147
+ {opt.labelFull}
148
+ </li>
149
+ ))}
150
+ </ul>
151
+ )}
152
+ </div>
153
+ {filtered.length === 0 ? (
154
+ <div className={classes.empty}>
155
+ <Icon type="folders" size={16} /> No data in range
156
+ </div>
157
+ ) : (
158
+ <div className={classes.chartCanvas}>
159
+ <Line data={data} options={options} />
160
+ </div>
161
+ )}
162
+ </div>
163
+ );
164
+ };
165
+
166
+ export default PlanSizeChart;
@@ -16,6 +16,15 @@
16
16
  span {
17
17
  color: var(--icon-color);
18
18
  }
19
+ .widgetSubTitle {
20
+ margin-left: 4px;
21
+ i {
22
+ font-style: normal;
23
+ font-family: sans-serif;
24
+ font-size: 9px;
25
+ padding: 0 2px;
26
+ }
27
+ }
19
28
  }
20
29
 
21
30
  .sources,
@@ -83,12 +92,17 @@
83
92
  }
84
93
  .snapshots {
85
94
  width: 24%;
95
+ padding: 20px 0 0 0;
96
+ display: flex;
97
+ flex-direction: column;
98
+ justify-content: space-between;
86
99
  .snapshotsContent {
87
100
  display: flex;
88
101
  justify-content: space-around;
89
102
  align-items: center;
90
- height: 100%;
91
- margin-top: 8px;
103
+ height: 70px;
104
+ margin-top: 16px;
105
+ position: relative;
92
106
 
93
107
  & > div {
94
108
  text-align: center;
@@ -1,9 +1,11 @@
1
1
  import Icon from '../../common/Icon/Icon';
2
2
  import { Plan } from '../../../@types/plans';
3
- import { formatBytes, formatNumberToK, formatIntervalDisplay } from '../../../utils/helpers';
3
+ import { formatIntervalDisplay } from '../../../utils/helpers';
4
4
  import PlanHistory from '../PlanHistory/PlanHistory';
5
+ import PlanSizeChart from '../PlanSizeChart/PlanSizeChart';
5
6
  import classes from './PlanStats.module.scss';
6
7
  import PlanStorageInfo from '../PlanStorageInfo/PlanStorageInfo';
8
+ import { formatBytes, formatNumberToK } from '../../../utils/helpers';
7
9
 
8
10
  interface PlanStatsProps {
9
11
  plan: Plan;
@@ -61,18 +63,13 @@ const PlanStats = ({ plan, isSync, lastBackupItem }: PlanStatsProps) => {
61
63
  </div>
62
64
  <div className={classes.snapshots}>
63
65
  <div className={classes.widgetTitle}>
64
- <Icon type="folders" size={12} /> Source Stats
66
+ <Icon type="folders" size={12} /> Stats{' '}
67
+ <span className={classes.widgetSubTitle}>
68
+ {formatNumberToK(totalFiles)} files <i>|</i> {formatBytes(totalSize)}
69
+ </span>
65
70
  </div>
66
71
  <div className={classes.snapshotsContent}>
67
- <div>
68
- <span>{totalFiles ? formatNumberToK(totalFiles) : 0}</span>
69
- <span>Files</span>
70
- </div>
71
- <div></div>
72
- <div>
73
- <span>{totalSize ? formatBytes(totalSize) : '0.00B'}</span>
74
- <span>Size</span>
75
- </div>
72
+ <PlanSizeChart backups={plan.backups} />
76
73
  </div>
77
74
  </div>
78
75
  <div className={classes.health}>
@@ -163,6 +163,13 @@
163
163
  max-width: 70px;
164
164
  }
165
165
 
166
+ .fileDate {
167
+ max-width: 150px;
168
+ white-space: nowrap;
169
+ overflow: hidden;
170
+ text-overflow: ellipsis;
171
+ }
172
+
166
173
  .selected {
167
174
  background: #e6f7ff;
168
175
  }
@@ -978,6 +978,18 @@ const Icon = ({ type, color = 'currentColor', size = 16, title = '', classes = '
978
978
  ></path>
979
979
  </IconWrapper>
980
980
  )}
981
+ {type === 'repair' && (
982
+ <IconWrapper size={size} viewBox="0 0 24 24">
983
+ <g fill="none" fillRule="evenodd">
984
+ <path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path>
985
+ <path
986
+ fill={color}
987
+ d="M7.527 2.657a7.001 7.001 0 0 1 8.26 9.347l4.599 3.893a3.3 3.3 0 1 1-4.651 4.65l-3.891-4.597a7.001 7.001 0 0 1-9.35-8.26a1.01 1.01 0 0 1 1.72-.432l3.045 3.307l2.297-.845l.847-2.3l-3.309-3.04a1.01 1.01 0 0 1 .433-1.723"
988
+ ></path>
989
+ </g>
990
+ </IconWrapper>
991
+ )}
992
+
981
993
  {type === 'dots-vertical' && (
982
994
  <IconWrapper size={size} viewBox="0 0 24 24">
983
995
  <path
@@ -1151,6 +1163,15 @@ const Icon = ({ type, color = 'currentColor', size = 16, title = '', classes = '
1151
1163
  ></path>
1152
1164
  </IconWrapper>
1153
1165
  )}
1166
+ {type === 'filter' && (
1167
+ <IconWrapper size={size} viewBox="0 0 24 24">
1168
+ <path
1169
+ fill={color}
1170
+ d="M15 19.88c.04.3-.06.62-.29.83a.996.996 0 0 1-1.41 0L9.29 16.7a.99.99 0 0 1-.29-.83v-5.12L4.21 4.62a1 1 0 0 1 .17-1.4c.19-.14.4-.22.62-.22h14c.22 0 .43.08.62.22a1 1 0 0 1 .17 1.4L15 10.75zM7.04 5L11 10.06v5.52l2 2v-7.53L16.96 5z"
1171
+ ></path>
1172
+ </IconWrapper>
1173
+ )}
1174
+
1154
1175
  {type === 'notification' && (
1155
1176
  <IconWrapper size={size} viewBox="0 0 24 24">
1156
1177
  <g fill="none" stroke={color} strokeWidth={1.5}>
@@ -7,6 +7,7 @@
7
7
  span {
8
8
  opacity: 1;
9
9
  color: inherit;
10
+ font-weight: 600;
10
11
  }
11
12
  }
12
13
 
@@ -18,11 +19,11 @@
18
19
  background: var(--field-bg);
19
20
  width: calc(100% - 2px);
20
21
  border-radius: 0 0 4px 4px;
21
- max-height: 200px;
22
22
  overflow: auto;
23
23
  font-size: 0.75rem;
24
24
  right: 0;
25
- width: 200px;
25
+ width: 210px;
26
+ max-height: 320px;
26
27
  border-radius: 6px;
27
28
  ul {
28
29
  margin: 0;
@@ -2,18 +2,19 @@ import { useState } from 'react';
2
2
  import Icon from '../Icon/Icon';
3
3
  import classes from './SortItems.module.scss';
4
4
  type SortItemsProps = {
5
+ id: string;
5
6
  onSort: (s: string) => void;
6
7
  options: { label: string; value: string }[];
7
8
  };
8
9
 
9
- const SortItems = ({ options, onSort }: SortItemsProps) => {
10
+ const SortItems = ({ id, options, onSort }: SortItemsProps) => {
10
11
  const [showDropDown, setshowDropDown] = useState(false);
11
- const [selected, setsSelected] = useState('');
12
+ const [selected, setsSelected] = useState(localStorage.getItem(id) || '');
12
13
  const selectedLabel = options.find((item) => item.value === selected);
13
14
  return (
14
15
  <div className={classes.sortItems}>
15
16
  <button
16
- className={selected ? classes.sortActive : ''}
17
+ className={selected || showDropDown ? classes.sortActive : ''}
17
18
  onClick={() => setshowDropDown(!showDropDown)}
18
19
  data-tooltip-id="appTooltip"
19
20
  data-tooltip-content="Sort"
@@ -34,6 +35,7 @@ const SortItems = ({ options, onSort }: SortItemsProps) => {
34
35
  onClick={() => {
35
36
  setsSelected(item.value);
36
37
  onSort(item.value);
38
+ localStorage.setItem(id, item.value);
37
39
  setshowDropDown(false);
38
40
  }}
39
41
  className={`${selected && item.value === selected ? classes.selectedItem : ''}`}
@@ -46,6 +48,7 @@ const SortItems = ({ options, onSort }: SortItemsProps) => {
46
48
  onClick={() => {
47
49
  setsSelected('');
48
50
  onSort('');
51
+ localStorage.removeItem(id);
49
52
  setshowDropDown(false);
50
53
  }}
51
54
  >
@@ -73,6 +73,7 @@
73
73
  .dropBtn {
74
74
  background: transparent;
75
75
  color: var(--icon-color);
76
+ padding: 0;
76
77
  }
77
78
  }
78
79
 
@@ -27,6 +27,7 @@ export { default as PathPicker } from './common/PathPicker/PathPicker';
27
27
  export { default as SearchItems } from './common/SearchItems/SearchItems';
28
28
  export { default as SidePanel } from './common/SidePanel/SidePanel';
29
29
  export { default as SortItems } from './common/SortItems/SortItems';
30
+ export { default as FilterPlans } from './Plan/FilterPlans/FilterPlans';
30
31
  export { default as StatusLabel } from './common/StatusLabel/StatusLabel';
31
32
  export { default as Tabs, TabList, TabPanel, Tab } from './common/Tabs/Tabs';
32
33
  export { default as TagsFilter } from './common/TagsFilter/TagsFilter';
@@ -90,6 +91,7 @@ export { default as PlanTypeSettings } from './Plan/PlanSettings/PlanTypeSetting
90
91
  export { default as PlanStats } from './Plan/PlanStats/PlanStats';
91
92
  export { default as PlanUnlockModal } from './Plan/PlanUnlockModal/PlanUnlockModal';
92
93
  export { default as PlanIntegrity } from './Plan/PlanIntegrity/PlanIntegrity';
94
+ export { default as PlanRepair } from './Plan/PlanRepair/PlanRepair';
93
95
  export { default as SnapshotViewer } from './Plan/SnapshotViewer/SnapshotViewer';
94
96
  export { default as SnapshotViewerFile } from './Plan/SnapshotViewer/SnapshotViewerFile';
95
97
 
@@ -97,30 +97,33 @@ export const usePlanSingleActions = (): {
97
97
 
98
98
  const toastId = toast.loading(`Starting ${isSync ? 'Sync' : 'Backup'}...`);
99
99
 
100
- performBackupMutation.mutate(plan.id, {
101
- onSuccess: (data) => {
102
- const msg = data?.message || `${isSync ? 'Sync' : 'Backup'} initiated successfully! 🚀`;
103
- const notStarted = !isSync && data?.message && data?.message.includes('reached the concurrency limit');
104
- toast.update(toastId, {
105
- render: isSync ? msg : notStarted ? data?.message : 'Backup initiated successfully!',
106
- type: 'success',
107
- isLoading: false,
108
- autoClose: 3000,
109
- });
110
- if (!isSync && !notStarted) {
111
- navigate(`/plan/${plan.id}?pendingbackup=1`);
112
- }
113
- },
114
- onError: (error: any) => {
115
- toast.update(toastId, {
116
- render: `${isSync ? 'Sync' : 'Backup'} failed to start. ${error?.message || 'Unknown Error.'}`,
117
- type: 'error',
118
- isLoading: false,
119
- autoClose: false,
120
- closeButton: true,
121
- });
100
+ performBackupMutation.mutate(
101
+ { id: plan.id },
102
+ {
103
+ onSuccess: (data) => {
104
+ const msg = data?.message || `${isSync ? 'Sync' : 'Backup'} initiated successfully! 🚀`;
105
+ const notStarted = !isSync && data?.message && data?.message.includes('reached the concurrency limit');
106
+ toast.update(toastId, {
107
+ render: isSync ? msg : notStarted ? data?.message : 'Backup initiated successfully!',
108
+ type: 'success',
109
+ isLoading: false,
110
+ autoClose: 3000,
111
+ });
112
+ if (!isSync && !notStarted) {
113
+ navigate(`/plan/${plan.id}?pendingbackup=1`);
114
+ }
115
+ },
116
+ onError: (error: any) => {
117
+ toast.update(toastId, {
118
+ render: `${isSync ? 'Sync' : 'Backup'} failed to start. ${error?.message || 'Unknown Error.'}`,
119
+ type: 'error',
120
+ isLoading: false,
121
+ autoClose: false,
122
+ closeButton: true,
123
+ });
124
+ },
122
125
  },
123
- });
126
+ );
124
127
  };
125
128
 
126
129
  return {
@@ -65,7 +65,13 @@ const Login = () => {
65
65
  </h3>
66
66
  </div>
67
67
  <div className={classes.container}>
68
- <div className="loginForm">
68
+ <form
69
+ className="loginForm"
70
+ onSubmit={(e) => {
71
+ e.preventDefault();
72
+ handleLogin();
73
+ }}
74
+ >
69
75
  <div className={classes.loginInput}>
70
76
  <Icon type="user" classes={classes.loginInputIcon} />
71
77
  <input
@@ -90,7 +96,7 @@ const Login = () => {
90
96
  {loginMutation.isPending ? 'Logging in...' : 'Login'}
91
97
  </button>
92
98
  {error && error.msg && <div className={classes.loginErrorMsg}>{error.msg}</div>}
93
- </div>
99
+ </form>
94
100
  </div>
95
101
  </div>
96
102
  );
@@ -15,10 +15,13 @@ import PlanRemoveModal from '../../components/Plan/PlanRemoveModal/PlanRemoveMod
15
15
  import PlanProgress from '../../components/Plan/PlanProgress/PlanProgress';
16
16
  import PlanBackups from '../../components/Plan/PlanBackups/PlanBackups';
17
17
  import PlanIntegrity from '../../components/Plan/PlanIntegrity/PlanIntegrity';
18
+ import PlanRepair from '../../components/Plan/PlanRepair/PlanRepair';
19
+ import { PlanVerifiedResult } from '../..';
18
20
 
19
21
  const PlanSingle = () => {
20
22
  const [showMoreOptions, setShowMoreOptions] = useState(false);
21
23
  const [showIntegrityModal, setShowIntegrityModal] = useState(false);
24
+ const [showRepoRepair, setShowRepoRepair] = useState('');
22
25
  const { id } = useParams();
23
26
 
24
27
  const EditPlanModal = useComponentOverride('EditPlan', EditPlan);
@@ -198,6 +201,20 @@ const PlanSingle = () => {
198
201
  storage={plan.storage}
199
202
  replicationStorages={plan.settings.replication?.enabled ? plan.settings.replication.storages : []}
200
203
  onClose={() => setShowIntegrityModal(false)}
204
+ onRepairOpen={(replicationId) => {
205
+ setShowIntegrityModal(false);
206
+ setShowRepoRepair(replicationId);
207
+ }}
208
+ />
209
+ )}
210
+ {showRepoRepair && !isSync && (
211
+ <PlanRepair
212
+ planId={id}
213
+ errorType={
214
+ (plan.verified?.result[showRepoRepair as keyof typeof plan.verified.result] as PlanVerifiedResult)?.errorType || 'unknown'
215
+ }
216
+ onClose={() => setShowRepoRepair('')}
217
+ onOpenIntegrity={() => setShowIntegrityModal(true)}
201
218
  />
202
219
  )}
203
220
  {showDeleteModal && (