@plutonhq/core-frontend 0.1.23 → 0.1.25
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/index.js +4 -1
- package/dist-lib/@types/index.js.map +1 -1
- package/dist-lib/@types/settings.d.ts +14 -0
- package/dist-lib/@types/settings.d.ts.map +1 -1
- package/dist-lib/@types/settings.js +34 -0
- package/dist-lib/@types/settings.js.map +1 -0
- package/dist-lib/components/Plan/Backups/Backups.d.ts.map +1 -1
- package/dist-lib/components/Plan/Backups/Backups.js +189 -159
- package/dist-lib/components/Plan/Backups/Backups.js.map +1 -1
- package/dist-lib/components/Plan/PlanSettings/PlanNotificationSettings.d.ts.map +1 -1
- package/dist-lib/components/Plan/PlanSettings/PlanNotificationSettings.js +148 -90
- package/dist-lib/components/Plan/PlanSettings/PlanNotificationSettings.js.map +1 -1
- package/dist-lib/components/Plan/SnapshotViewer/SnapshotViewer.d.ts +32 -0
- package/dist-lib/components/Plan/SnapshotViewer/SnapshotViewer.d.ts.map +1 -0
- package/dist-lib/components/Plan/SnapshotViewer/SnapshotViewer.js +252 -0
- package/dist-lib/components/Plan/SnapshotViewer/SnapshotViewer.js.map +1 -0
- package/dist-lib/components/Plan/SnapshotViewer/SnapshotViewerFile.d.ts +23 -0
- package/dist-lib/components/Plan/SnapshotViewer/SnapshotViewerFile.d.ts.map +1 -0
- package/dist-lib/components/Plan/SnapshotViewer/SnapshotViewerFile.js +72 -0
- package/dist-lib/components/Plan/SnapshotViewer/SnapshotViewerFile.js.map +1 -0
- package/dist-lib/components/Restore/RestoreFileSelector/RestoreFileSelector.d.ts.map +1 -1
- package/dist-lib/components/Restore/RestoreFileSelector/RestoreFileSelector.js +188 -198
- package/dist-lib/components/Restore/RestoreFileSelector/RestoreFileSelector.js.map +1 -1
- package/dist-lib/components/Restore/RestoreFileSelector/RestoreFileSelector.module.scss.js +20 -64
- package/dist-lib/components/Restore/RestoreFileSelector/RestoreFileSelector.module.scss.js.map +1 -1
- package/dist-lib/components/Restore/RestoredFileBrowser/RestoredFileBrowser.d.ts.map +1 -1
- package/dist-lib/components/Restore/RestoredFileBrowser/RestoredFileBrowser.js +125 -159
- package/dist-lib/components/Restore/RestoredFileBrowser/RestoredFileBrowser.js.map +1 -1
- package/dist-lib/components/Settings/IntegrationSettings/IntegrationSettings.d.ts.map +1 -1
- package/dist-lib/components/Settings/IntegrationSettings/IntegrationSettings.js +52 -47
- package/dist-lib/components/Settings/IntegrationSettings/IntegrationSettings.js.map +1 -1
- package/dist-lib/components/Settings/IntegrationSettings/IntegrationSettings.module.scss.js +12 -6
- package/dist-lib/components/Settings/IntegrationSettings/IntegrationSettings.module.scss.js.map +1 -1
- package/dist-lib/components/Settings/IntegrationSettings/NtfySettings.d.ts +9 -0
- package/dist-lib/components/Settings/IntegrationSettings/NtfySettings.d.ts.map +1 -0
- package/dist-lib/components/Settings/IntegrationSettings/NtfySettings.js +79 -0
- package/dist-lib/components/Settings/IntegrationSettings/NtfySettings.js.map +1 -0
- package/dist-lib/components/Settings/IntegrationSettings/SMTPSettings.d.ts +4 -3
- package/dist-lib/components/Settings/IntegrationSettings/SMTPSettings.d.ts.map +1 -1
- package/dist-lib/components/Settings/IntegrationSettings/SMTPSettings.js +37 -35
- package/dist-lib/components/Settings/IntegrationSettings/SMTPSettings.js.map +1 -1
- package/dist-lib/components/Settings/IntegrationSettings/ValidateEmailIntegration.d.ts +10 -0
- package/dist-lib/components/Settings/IntegrationSettings/ValidateEmailIntegration.d.ts.map +1 -0
- package/dist-lib/components/Settings/IntegrationSettings/ValidateEmailIntegration.js +49 -0
- package/dist-lib/components/Settings/IntegrationSettings/ValidateEmailIntegration.js.map +1 -0
- package/dist-lib/components/Storage/EditStorage/EditStorage.js +10 -10
- package/dist-lib/components/Storage/EditStorage/EditStorage.js.map +1 -1
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowser.module.scss.js +74 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowser.module.scss.js.map +1 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserDirectories.d.ts +17 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserDirectories.d.ts.map +1 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserDirectories.js +57 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserDirectories.js.map +1 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserFileList.d.ts +18 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserFileList.d.ts.map +1 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserFileList.js +24 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserFileList.js.map +1 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserFileRow.d.ts +18 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserFileRow.d.ts.map +1 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserFileRow.js +37 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserFileRow.js.map +1 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserGoUpRow.d.ts +9 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserGoUpRow.d.ts.map +1 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserGoUpRow.js +15 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserGoUpRow.js.map +1 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserToolbar.d.ts +11 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserToolbar.d.ts.map +1 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserToolbar.js +18 -0
- package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserToolbar.js.map +1 -0
- package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotDatabase.d.ts +12 -0
- package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotDatabase.d.ts.map +1 -0
- package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotDatabase.js +70 -0
- package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotDatabase.js.map +1 -0
- package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotNavigation.d.ts +22 -0
- package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotNavigation.d.ts.map +1 -0
- package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotNavigation.js +79 -0
- package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotNavigation.js.map +1 -0
- package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotSort.d.ts +6 -0
- package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotSort.d.ts.map +1 -0
- package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotSort.js +18 -0
- package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotSort.js.map +1 -0
- package/dist-lib/components/common/SnapshotBrowser/index.d.ts +11 -0
- package/dist-lib/components/common/SnapshotBrowser/index.d.ts.map +1 -0
- package/dist-lib/components/index.d.ts +12 -0
- package/dist-lib/components/index.d.ts.map +1 -1
- package/dist-lib/components.js +152 -128
- package/dist-lib/components.js.map +1 -1
- package/dist-lib/hooks/usePlanSingleActions.d.ts.map +1 -1
- package/dist-lib/hooks/usePlanSingleActions.js +21 -21
- package/dist-lib/hooks/usePlanSingleActions.js.map +1 -1
- package/dist-lib/services/backups.d.ts +4 -0
- package/dist-lib/services/backups.d.ts.map +1 -1
- package/dist-lib/services/backups.js +34 -25
- package/dist-lib/services/backups.js.map +1 -1
- package/dist-lib/services/settings.d.ts +3 -2
- package/dist-lib/services/settings.d.ts.map +1 -1
- package/dist-lib/services/settings.js +0 -1
- package/dist-lib/services/settings.js.map +1 -1
- package/dist-lib/services.js +113 -112
- package/dist-lib/styles/core-frontend.css +1 -1
- package/dist-lib/utils/index.d.ts +1 -0
- package/dist-lib/utils/index.d.ts.map +1 -1
- package/dist-lib/utils/snapshotDatabase.d.ts +16 -0
- package/dist-lib/utils/snapshotDatabase.d.ts.map +1 -0
- package/dist-lib/utils/snapshotDatabase.js +105 -0
- package/dist-lib/utils/snapshotDatabase.js.map +1 -0
- package/dist-lib/utils.js +24 -22
- package/dist-lib/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/@types/settings.ts +43 -0
- package/src/components/Plan/Backups/Backups.tsx +40 -1
- package/src/components/Plan/PlanSettings/PlanNotificationSettings.tsx +65 -0
- package/src/components/Plan/SnapshotViewer/SnapshotViewer.tsx +344 -0
- package/src/components/Plan/SnapshotViewer/SnapshotViewerFile.tsx +89 -0
- package/src/components/Restore/RestoreFileSelector/RestoreFileSelector.tsx +82 -145
- package/src/components/Restore/RestoredFileBrowser/RestoredFileBrowser.tsx +71 -156
- package/src/components/Settings/IntegrationSettings/IntegrationSettings.module.scss +16 -0
- package/src/components/Settings/IntegrationSettings/IntegrationSettings.tsx +45 -42
- package/src/components/Settings/IntegrationSettings/NtfySettings.tsx +106 -0
- package/src/components/Settings/IntegrationSettings/SMTPSettings.tsx +28 -19
- package/src/components/Settings/IntegrationSettings/ValidateEmailIntegration.tsx +58 -0
- package/src/components/Storage/EditStorage/EditStorage.tsx +1 -1
- package/src/components/common/SnapshotBrowser/SnapshotBrowser.module.scss +376 -0
- package/src/components/common/SnapshotBrowser/SnapshotBrowserDirectories.tsx +84 -0
- package/src/components/common/SnapshotBrowser/SnapshotBrowserFileList.tsx +52 -0
- package/src/components/common/SnapshotBrowser/SnapshotBrowserFileRow.tsx +44 -0
- package/src/components/common/SnapshotBrowser/SnapshotBrowserGoUpRow.tsx +22 -0
- package/src/components/common/SnapshotBrowser/SnapshotBrowserToolbar.tsx +29 -0
- package/src/components/common/SnapshotBrowser/hooks/useSnapshotDatabase.ts +130 -0
- package/src/components/common/SnapshotBrowser/hooks/useSnapshotNavigation.ts +154 -0
- package/src/components/common/SnapshotBrowser/hooks/useSnapshotSort.ts +24 -0
- package/src/components/common/SnapshotBrowser/index.ts +13 -0
- package/src/components/index.ts +15 -0
- package/src/hooks/usePlanSingleActions.tsx +5 -3
- package/src/services/backups.ts +12 -0
- package/src/services/settings.ts +2 -2
- package/src/utils/index.ts +1 -0
- package/src/utils/snapshotDatabase.ts +201 -0
- /package/dist-lib/providers/{azureBlob.png → azureblob.png} +0 -0
- /package/dist-lib/providers/{azureFiles.png → azurefiles.png} +0 -0
- /package/dist-lib/providers/{files.png → filescom.png} +0 -0
- /package/dist-lib/providers/{oracle.png → oracleobjectstorage.png} +0 -0
- /package/dist-lib/providers/{proton.png → protondrive.png} +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { useState, useMemo } from 'react';
|
|
2
|
-
import { FixedSizeList as List } from 'react-window';
|
|
1
|
+
import { useState, useMemo, useEffect } from 'react';
|
|
3
2
|
import Icon from '../../common/Icon/Icon';
|
|
4
3
|
import { RestoreFileItem } from '../../../@types/restores';
|
|
5
4
|
import { calculateDirectorySizes, formatBytes, formatDateTime, formatNumberToK, isMobile, sortFileItems } from '../../../utils/helpers';
|
|
6
5
|
import FileIcon from '../../common/FileIcon/FileIcon';
|
|
7
6
|
import classes from './RestoreFileSelector.module.scss';
|
|
8
7
|
import { getParentPath, getPathSeparator, normalizePath, splitPath } from '../../../utils/restore';
|
|
8
|
+
import { SnapshotBrowserToolbar, SnapshotBrowserDirectories, SnapshotBrowserFileList, SnapshotBrowserGoUpRow } from '../../common/SnapshotBrowser';
|
|
9
|
+
import { useSnapshotNavigation } from '../../common/SnapshotBrowser/hooks/useSnapshotNavigation';
|
|
10
|
+
import sbClasses from '../../common/SnapshotBrowser/SnapshotBrowser.module.scss';
|
|
9
11
|
|
|
10
12
|
interface RestoreFileSelectorProps {
|
|
11
13
|
selected: {
|
|
@@ -23,9 +25,9 @@ interface RestoreFileSelectorProps {
|
|
|
23
25
|
|
|
24
26
|
const isMobileDevice = isMobile();
|
|
25
27
|
const ITEM_HEIGHT = isMobileDevice ? 65 : 45;
|
|
28
|
+
const GRID_COLUMNS = '1fr 180px minmax(80px, auto)';
|
|
26
29
|
|
|
27
30
|
const RestoreFileSelector = ({ selected, files, isLoading, errorFetching, showChange, onSelect, fileSelectCondition }: RestoreFileSelectorProps) => {
|
|
28
|
-
const [selectedFolder, setSelectedFolder] = useState<string>('');
|
|
29
31
|
const [search, setSearch] = useState('');
|
|
30
32
|
const [sortField, setSortField] = useState<'name' | 'modifiedAt' | 'size'>('name');
|
|
31
33
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
|
@@ -152,13 +154,23 @@ const RestoreFileSelector = ({ selected, files, isLoading, errorFetching, showCh
|
|
|
152
154
|
return aParts.length - bParts.length;
|
|
153
155
|
});
|
|
154
156
|
|
|
155
|
-
// Set the first directory as selected by default
|
|
156
|
-
console.log('SelectedFolder :', sortedDirs[0]);
|
|
157
|
-
setSelectedFolder(sortedDirs[0] || '');
|
|
158
|
-
|
|
159
157
|
return sortedDirs;
|
|
160
158
|
}, [files]);
|
|
161
159
|
|
|
160
|
+
const { selectedFolder, setSelectedFolder, hasSubdirectories, isVisible, expandParentFolders, toggleFolder } = useSnapshotNavigation(
|
|
161
|
+
directories,
|
|
162
|
+
expandedFolders,
|
|
163
|
+
setExpandedFolders,
|
|
164
|
+
{ splitPath, getPathSeparator, hasLeadingSeparator: false },
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Set the first directory as selected by default
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (directories.length > 0 && selectedFolder === '') {
|
|
170
|
+
setSelectedFolder(directories[0]);
|
|
171
|
+
}
|
|
172
|
+
}, [directories, selectedFolder, setSelectedFolder]);
|
|
173
|
+
|
|
162
174
|
const summary = useMemo(() => {
|
|
163
175
|
let selectedFilesCount = 0;
|
|
164
176
|
let selectedBytes = 0;
|
|
@@ -182,11 +194,6 @@ const RestoreFileSelector = ({ selected, files, isLoading, errorFetching, showCh
|
|
|
182
194
|
};
|
|
183
195
|
}, [files, selectedFiles]);
|
|
184
196
|
|
|
185
|
-
const hasSubdirectories = (dir: string) => {
|
|
186
|
-
const separator = getPathSeparator(dir);
|
|
187
|
-
return directories.some((d) => d !== dir && d.startsWith(dir + separator));
|
|
188
|
-
};
|
|
189
|
-
|
|
190
197
|
const handleSort = (field: 'name' | 'modifiedAt' | 'size') => {
|
|
191
198
|
if (sortField === field) {
|
|
192
199
|
// Toggle direction if clicking the same field
|
|
@@ -198,44 +205,6 @@ const RestoreFileSelector = ({ selected, files, isLoading, errorFetching, showCh
|
|
|
198
205
|
}
|
|
199
206
|
};
|
|
200
207
|
|
|
201
|
-
const expandParentFolders = (dirPath: string) => {
|
|
202
|
-
const newExpanded = new Set(expandedFolders);
|
|
203
|
-
const separator = getPathSeparator(dirPath);
|
|
204
|
-
const parts = splitPath(dirPath);
|
|
205
|
-
let currentPath = '';
|
|
206
|
-
|
|
207
|
-
// Expand all parent directories
|
|
208
|
-
parts.forEach((part) => {
|
|
209
|
-
currentPath = currentPath ? `${currentPath}${separator}${part}` : part;
|
|
210
|
-
newExpanded.add(currentPath);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
setExpandedFolders(newExpanded);
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
const toggleFolder = (dir: string) => {
|
|
217
|
-
const newExpanded = new Set(expandedFolders);
|
|
218
|
-
if (expandedFolders.has(dir)) {
|
|
219
|
-
newExpanded.delete(dir);
|
|
220
|
-
} else {
|
|
221
|
-
newExpanded.add(dir);
|
|
222
|
-
}
|
|
223
|
-
setExpandedFolders(newExpanded);
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const isVisible = (dir: string) => {
|
|
227
|
-
const separator = getPathSeparator(dir);
|
|
228
|
-
const parts = splitPath(dir);
|
|
229
|
-
const parentParts = parts.slice(0, -1);
|
|
230
|
-
let parentPath = '';
|
|
231
|
-
|
|
232
|
-
// Check if all parent folders are expanded
|
|
233
|
-
return parentParts.every((part) => {
|
|
234
|
-
parentPath = parentPath ? `${parentPath}${separator}${part}` : part;
|
|
235
|
-
return expandedFolders.has(parentPath);
|
|
236
|
-
});
|
|
237
|
-
};
|
|
238
|
-
|
|
239
208
|
const onFileSelect = (path: string, isDirectory: boolean) => {
|
|
240
209
|
const newSelected = { ...selectedFiles };
|
|
241
210
|
const isCurrentlySelected = isPathSelected(path);
|
|
@@ -343,19 +312,17 @@ const RestoreFileSelector = ({ selected, files, isLoading, errorFetching, showCh
|
|
|
343
312
|
// Parent directory navigation
|
|
344
313
|
if (item === null) {
|
|
345
314
|
return (
|
|
346
|
-
<
|
|
315
|
+
<SnapshotBrowserGoUpRow
|
|
347
316
|
style={style}
|
|
348
|
-
|
|
349
|
-
onClick={() => {
|
|
317
|
+
onGoUp={() => {
|
|
350
318
|
const parentPath = getParentPath(selectedFolder);
|
|
351
319
|
const normalizedParentPath = normalizePath(parentPath);
|
|
352
320
|
if (!normalizedParentPath || normalizedParentPath === '') return;
|
|
353
321
|
expandParentFolders(normalizedParentPath);
|
|
354
322
|
setSelectedFolder(normalizedParentPath);
|
|
355
323
|
}}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
</div>
|
|
324
|
+
gridTemplateColumns={GRID_COLUMNS}
|
|
325
|
+
/>
|
|
359
326
|
);
|
|
360
327
|
}
|
|
361
328
|
|
|
@@ -369,9 +336,9 @@ const RestoreFileSelector = ({ selected, files, isLoading, errorFetching, showCh
|
|
|
369
336
|
|
|
370
337
|
return (
|
|
371
338
|
<div
|
|
372
|
-
style={style}
|
|
339
|
+
style={{ ...style, gridTemplateColumns: GRID_COLUMNS }}
|
|
373
340
|
key={file.path}
|
|
374
|
-
className={`${
|
|
341
|
+
className={`${sbClasses.snapshotFile} ${isDirectory ? sbClasses.fileIsDir : ''} ${showChange && file.changeType === 'modified' ? classes.fileModified : ''} ${showChange && file.changeType === 'removed' ? classes.fileRemoved : ''}`}
|
|
375
342
|
onClick={() => {
|
|
376
343
|
if (isDirectory) {
|
|
377
344
|
expandParentFolders(normalizedPath);
|
|
@@ -379,7 +346,7 @@ const RestoreFileSelector = ({ selected, files, isLoading, errorFetching, showCh
|
|
|
379
346
|
}
|
|
380
347
|
}}
|
|
381
348
|
>
|
|
382
|
-
<div className={
|
|
349
|
+
<div className={sbClasses.fileName}>
|
|
383
350
|
<button
|
|
384
351
|
className={`${classes.selectButton} ${isSelected ? classes.selected : ''} ${!canBeSelected ? classes.notSelectable : ''}`}
|
|
385
352
|
onClick={(e) => {
|
|
@@ -406,10 +373,12 @@ const RestoreFileSelector = ({ selected, files, isLoading, errorFetching, showCh
|
|
|
406
373
|
<Icon type="loading" size={24} /> Loading Snapshot Content..
|
|
407
374
|
</div>
|
|
408
375
|
)}
|
|
409
|
-
<div className={
|
|
410
|
-
<
|
|
411
|
-
|
|
412
|
-
|
|
376
|
+
<div className={sbClasses.snapshotBrowser}>
|
|
377
|
+
<SnapshotBrowserToolbar
|
|
378
|
+
search={search}
|
|
379
|
+
onSearchChange={setSearch}
|
|
380
|
+
leftContent={
|
|
381
|
+
<div className={sbClasses.stats}>
|
|
413
382
|
<strong>Summary: </strong>
|
|
414
383
|
{formatNumberToK(summary.selectedFiles)}/{formatNumberToK(summary.totalFiles)} Items {' • '}
|
|
415
384
|
{formatBytes(summary.selectedBytes)}/{formatBytes(summary.totalBytes)}
|
|
@@ -423,91 +392,59 @@ const RestoreFileSelector = ({ selected, files, isLoading, errorFetching, showCh
|
|
|
423
392
|
</div>
|
|
424
393
|
)}
|
|
425
394
|
</div>
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
{
|
|
439
|
-
const parts = splitPath(dir);
|
|
440
|
-
const dirName = parts[parts.length - 1];
|
|
441
|
-
const depth = parts.length - 1;
|
|
442
|
-
const isExpanded = expandedFolders.has(dir);
|
|
443
|
-
const hasChildren = hasSubdirectories(dir);
|
|
395
|
+
}
|
|
396
|
+
/>
|
|
397
|
+
|
|
398
|
+
<div className={sbClasses.browserContent}>
|
|
399
|
+
<SnapshotBrowserDirectories
|
|
400
|
+
directories={directories}
|
|
401
|
+
selectedFolder={selectedFolder}
|
|
402
|
+
expandedFolders={expandedFolders}
|
|
403
|
+
onDirectoryClick={(dir) => setSelectedFolder(dir)}
|
|
404
|
+
onToggleFolder={toggleFolder}
|
|
405
|
+
isVisible={isVisible}
|
|
406
|
+
hasSubdirectories={hasSubdirectories}
|
|
407
|
+
renderDirectoryExtra={(dir) => {
|
|
444
408
|
const isSelected = isPathSelected(files.find((f) => f.isDirectory && normalizePath(f.path) === dir)?.path || dir);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
<Icon type={'fm-directory'} size={14} />
|
|
470
|
-
<button
|
|
471
|
-
className={`${classes.selectButton} ${isSelected ? classes.selected : ''}`}
|
|
472
|
-
onClick={(e) => {
|
|
473
|
-
e.stopPropagation();
|
|
474
|
-
const originalPath = files.find((f) => f.isDirectory && normalizePath(f.path) === dir)?.path || dir;
|
|
475
|
-
onFileSelect(originalPath, true);
|
|
476
|
-
}}
|
|
477
|
-
>
|
|
478
|
-
<Icon type={isSelected ? 'check-circle-filled' : 'check-circle'} size={13} />
|
|
479
|
-
</button>
|
|
480
|
-
{dirName}
|
|
481
|
-
</div>
|
|
409
|
+
return (
|
|
410
|
+
<button
|
|
411
|
+
className={`${classes.selectButton} ${isSelected ? classes.selected : ''}`}
|
|
412
|
+
onClick={(e) => {
|
|
413
|
+
e.stopPropagation();
|
|
414
|
+
const originalPath = files.find((f) => f.isDirectory && normalizePath(f.path) === dir)?.path || dir;
|
|
415
|
+
onFileSelect(originalPath, true);
|
|
416
|
+
}}
|
|
417
|
+
>
|
|
418
|
+
<Icon type={isSelected ? 'check-circle-filled' : 'check-circle'} size={13} />
|
|
419
|
+
</button>
|
|
420
|
+
);
|
|
421
|
+
}}
|
|
422
|
+
/>
|
|
423
|
+
|
|
424
|
+
<div className={`${sbClasses.content} styled__scrollbar`}>
|
|
425
|
+
<SnapshotBrowserFileList
|
|
426
|
+
files={directChildren}
|
|
427
|
+
height={window.innerHeight - 370}
|
|
428
|
+
itemSize={ITEM_HEIGHT}
|
|
429
|
+
headerContent={
|
|
430
|
+
<>
|
|
431
|
+
<div onClick={() => handleSort('name')} className={sortField === 'name' ? classes.activeSort : ''}>
|
|
432
|
+
Name {sortField === 'name' && (sortDirection === 'asc' ? '↑' : '↓')}
|
|
482
433
|
</div>
|
|
483
|
-
|
|
434
|
+
<div onClick={() => handleSort('modifiedAt')} className={sortField === 'modifiedAt' ? classes.activeSort : ''}>
|
|
435
|
+
Last Modified {sortField === 'modifiedAt' && (sortDirection === 'asc' ? '↑' : '↓')}
|
|
436
|
+
</div>
|
|
437
|
+
<div onClick={() => handleSort('size')} className={sortField === 'size' ? classes.activeSort : ''}>
|
|
438
|
+
Size {sortField === 'size' && (sortDirection === 'asc' ? '↑' : '↓')}
|
|
439
|
+
</div>
|
|
440
|
+
</>
|
|
484
441
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
<div className={classes.
|
|
491
|
-
<div className={classes.header}>
|
|
492
|
-
<div onClick={() => handleSort('name')} className={sortField === 'name' ? classes.activeSort : ''}>
|
|
493
|
-
Name {sortField === 'name' && (sortDirection === 'asc' ? '↑' : '↓')}
|
|
494
|
-
</div>
|
|
495
|
-
<div onClick={() => handleSort('modifiedAt')} className={sortField === 'modifiedAt' ? classes.activeSort : ''}>
|
|
496
|
-
Last Modified {sortField === 'modifiedAt' && (sortDirection === 'asc' ? '↑' : '↓')}
|
|
497
|
-
</div>
|
|
498
|
-
<div onClick={() => handleSort('size')} className={sortField === 'size' ? classes.activeSort : ''}>
|
|
499
|
-
Size {sortField === 'size' && (sortDirection === 'asc' ? '↑' : '↓')}
|
|
500
|
-
</div>
|
|
501
|
-
</div>
|
|
502
|
-
{selectedFolder ? (
|
|
503
|
-
<List height={window.innerHeight - 370} itemCount={directChildren.length} itemSize={ITEM_HEIGHT} width="100%">
|
|
504
|
-
{Row}
|
|
505
|
-
</List>
|
|
506
|
-
) : (
|
|
507
|
-
<div className={classes.fileListEmpty}>Select a folder from the left to browse it's content</div>
|
|
508
|
-
)}
|
|
509
|
-
{errorFetching && <div className={classes.error}>Failed to load files. Please try again.</div>}
|
|
510
|
-
</div>
|
|
442
|
+
renderRow={Row}
|
|
443
|
+
selectedFolder={selectedFolder || null}
|
|
444
|
+
gridTemplateColumns={GRID_COLUMNS}
|
|
445
|
+
emptyMessage="Select a folder from the left to browse it's content"
|
|
446
|
+
/>
|
|
447
|
+
{errorFetching && <div className={classes.error}>Failed to load files. Please try again.</div>}
|
|
511
448
|
</div>
|
|
512
449
|
</div>
|
|
513
450
|
</div>
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { useState, useMemo, useEffect } from 'react';
|
|
2
|
-
import { FixedSizeList as List } from 'react-window';
|
|
3
2
|
import Icon from '../../common/Icon/Icon';
|
|
3
|
+
import FileIcon from '../../common/FileIcon/FileIcon';
|
|
4
4
|
import classes from './RestoredFileBrowser.module.scss';
|
|
5
5
|
import { RestoredFileItem, RestoredItemsStats } from '../../../@types/restores';
|
|
6
6
|
import { formatBytes, formatNumberToK, isMobile } from '../../../utils/helpers';
|
|
7
7
|
import { getParentPath, getPathSeparator, normalizePath, splitPath } from '../../../utils/restore';
|
|
8
|
-
import
|
|
8
|
+
import { SnapshotBrowserToolbar, SnapshotBrowserDirectories, SnapshotBrowserFileList, SnapshotBrowserGoUpRow } from '../../common/SnapshotBrowser';
|
|
9
|
+
import { useSnapshotNavigation } from '../../common/SnapshotBrowser/hooks/useSnapshotNavigation';
|
|
10
|
+
import sbClasses from '../../common/SnapshotBrowser/SnapshotBrowser.module.scss';
|
|
9
11
|
|
|
10
12
|
interface RestoredFileBrowserProps {
|
|
11
13
|
files: RestoredFileItem[];
|
|
@@ -15,9 +17,9 @@ interface RestoredFileBrowserProps {
|
|
|
15
17
|
|
|
16
18
|
const isMobileDevice = isMobile();
|
|
17
19
|
const ITEM_HEIGHT = isMobileDevice ? 65 : 45;
|
|
20
|
+
const GRID_COLUMNS = '1fr 100px minmax(80px, auto)';
|
|
18
21
|
|
|
19
22
|
const RestoredFileBrowser = ({ files, stats, isPreview = false }: RestoredFileBrowserProps) => {
|
|
20
|
-
const [selectedFolder, setSelectedFolder] = useState<string>('');
|
|
21
23
|
const [search, setSearch] = useState('');
|
|
22
24
|
const [filters, setFilters] = useState({
|
|
23
25
|
unchanged: true,
|
|
@@ -34,7 +36,6 @@ const RestoredFileBrowser = ({ files, stats, isPreview = false }: RestoredFileBr
|
|
|
34
36
|
|
|
35
37
|
parts.forEach((part, index) => {
|
|
36
38
|
currentPath = currentPath ? `${currentPath}${separator}${part}` : part;
|
|
37
|
-
// Only expand folders up to 5 levels deep (index < 5)
|
|
38
39
|
if (index < 3) {
|
|
39
40
|
allPaths.add(currentPath);
|
|
40
41
|
}
|
|
@@ -54,7 +55,6 @@ const RestoredFileBrowser = ({ files, stats, isPreview = false }: RestoredFileBr
|
|
|
54
55
|
})
|
|
55
56
|
.forEach((file) => {
|
|
56
57
|
const dirPath = getParentPath(file.path);
|
|
57
|
-
// Use the normalized path as key
|
|
58
58
|
const normalizedDirPath = normalizePath(dirPath);
|
|
59
59
|
|
|
60
60
|
if (normalizedDirPath) {
|
|
@@ -70,7 +70,6 @@ const RestoredFileBrowser = ({ files, stats, isPreview = false }: RestoredFileBr
|
|
|
70
70
|
|
|
71
71
|
const directories = useMemo(() => {
|
|
72
72
|
const dirs = new Set<string>();
|
|
73
|
-
// Derive directories from all files, not filtered fileSystem, to keep tree stable during search
|
|
74
73
|
files.forEach((file) => {
|
|
75
74
|
const dirPath = getParentPath(file.path);
|
|
76
75
|
const separator = getPathSeparator(dirPath);
|
|
@@ -87,62 +86,22 @@ const RestoredFileBrowser = ({ files, stats, isPreview = false }: RestoredFileBr
|
|
|
87
86
|
return Array.from(dirs);
|
|
88
87
|
}, [files]);
|
|
89
88
|
|
|
90
|
-
|
|
89
|
+
const { selectedFolder, setSelectedFolder, hasSubdirectories, isVisible, expandParentFolders, toggleFolder } = useSnapshotNavigation(
|
|
90
|
+
directories,
|
|
91
|
+
expandedFolders,
|
|
92
|
+
setExpandedFolders,
|
|
93
|
+
{ splitPath, getPathSeparator, hasLeadingSeparator: false },
|
|
94
|
+
);
|
|
95
|
+
|
|
91
96
|
useEffect(() => {
|
|
92
97
|
if (directories.length > 0 && selectedFolder === '') {
|
|
93
98
|
setSelectedFolder(directories[0]);
|
|
94
99
|
}
|
|
95
|
-
}, [directories, selectedFolder]);
|
|
96
|
-
|
|
97
|
-
const hasSubdirectories = (dir: string) => {
|
|
98
|
-
const separator = getPathSeparator(dir);
|
|
99
|
-
return directories.some((d) => d !== dir && d.startsWith(dir + separator));
|
|
100
|
-
};
|
|
100
|
+
}, [directories, selectedFolder, setSelectedFolder]);
|
|
101
101
|
|
|
102
102
|
const hasUpdatedContent = (dir: string) => {
|
|
103
|
-
return Object.entries(fileSystem).some(([path,
|
|
104
|
-
return path.startsWith(dir) &&
|
|
105
|
-
});
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const expandParentDirectories = (dir: string) => {
|
|
109
|
-
const newExpanded = new Set(expandedFolders);
|
|
110
|
-
const separator = getPathSeparator(dir);
|
|
111
|
-
const parts = splitPath(dir);
|
|
112
|
-
let currentPath = '';
|
|
113
|
-
|
|
114
|
-
// Expand all parent directories
|
|
115
|
-
parts.forEach((part, index) => {
|
|
116
|
-
currentPath = currentPath ? `${currentPath}${separator}${part}` : part;
|
|
117
|
-
if (index < parts.length - 1) {
|
|
118
|
-
// Don't expand the target directory itself
|
|
119
|
-
newExpanded.add(currentPath);
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
setExpandedFolders(newExpanded);
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const toggleFolder = (dir: string) => {
|
|
127
|
-
const newExpanded = new Set(expandedFolders);
|
|
128
|
-
if (expandedFolders.has(dir)) {
|
|
129
|
-
newExpanded.delete(dir);
|
|
130
|
-
} else {
|
|
131
|
-
newExpanded.add(dir);
|
|
132
|
-
}
|
|
133
|
-
setExpandedFolders(newExpanded);
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const isVisible = (dir: string) => {
|
|
137
|
-
const separator = getPathSeparator(dir);
|
|
138
|
-
const parts = splitPath(dir);
|
|
139
|
-
const parentParts = parts.slice(0, -1);
|
|
140
|
-
let parentPath = '';
|
|
141
|
-
|
|
142
|
-
// Check if all parent folders are expanded
|
|
143
|
-
return parentParts.every((part) => {
|
|
144
|
-
parentPath = parentPath ? `${parentPath}${separator}${part}` : part;
|
|
145
|
-
return expandedFolders.has(parentPath);
|
|
103
|
+
return Object.entries(fileSystem).some(([path, dirFiles]) => {
|
|
104
|
+
return path.startsWith(dir) && dirFiles.some((f) => f.action === 'restored' || f.action === 'updated');
|
|
146
105
|
});
|
|
147
106
|
};
|
|
148
107
|
|
|
@@ -155,11 +114,9 @@ const RestoredFileBrowser = ({ files, stats, isPreview = false }: RestoredFileBr
|
|
|
155
114
|
const isDirectoryA = directories.includes(normalizedPathA);
|
|
156
115
|
const isDirectoryB = directories.includes(normalizedPathB);
|
|
157
116
|
|
|
158
|
-
// First sort by type: directories first, then files
|
|
159
117
|
if (isDirectoryA && !isDirectoryB) return -1;
|
|
160
118
|
if (!isDirectoryA && isDirectoryB) return 1;
|
|
161
119
|
|
|
162
|
-
// If both are same type, sort by status priority
|
|
163
120
|
const priority = { restored: 0, updated: 1, unchanged: 2 };
|
|
164
121
|
return priority[a.action] - priority[b.action];
|
|
165
122
|
});
|
|
@@ -169,25 +126,19 @@ const RestoredFileBrowser = ({ files, stats, isPreview = false }: RestoredFileBr
|
|
|
169
126
|
const totalItems = sortedFiles.length + (showGoUpButton ? 1 : 0);
|
|
170
127
|
|
|
171
128
|
const FileRow = ({ index, style }: { index: number; style: React.CSSProperties }) => {
|
|
172
|
-
// If showing go up button and this is the first item
|
|
173
129
|
if (showGoUpButton && index === 0) {
|
|
174
130
|
return (
|
|
175
|
-
<
|
|
131
|
+
<SnapshotBrowserGoUpRow
|
|
176
132
|
style={style}
|
|
177
|
-
|
|
178
|
-
onClick={() => {
|
|
133
|
+
onGoUp={() => {
|
|
179
134
|
const parentPath = getParentPath(selectedFolder);
|
|
180
135
|
const normalizedParentPath = normalizePath(parentPath);
|
|
181
|
-
console.log('normalizedParentPath :', normalizedParentPath);
|
|
182
136
|
if (!normalizedParentPath || normalizedParentPath === '') return;
|
|
183
|
-
|
|
137
|
+
expandParentFolders(normalizedParentPath);
|
|
184
138
|
setSelectedFolder(normalizedParentPath);
|
|
185
139
|
}}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
<div className={classes.status}></div>
|
|
189
|
-
<div></div>
|
|
190
|
-
</div>
|
|
140
|
+
gridTemplateColumns={GRID_COLUMNS}
|
|
141
|
+
/>
|
|
191
142
|
);
|
|
192
143
|
}
|
|
193
144
|
|
|
@@ -202,22 +153,22 @@ const RestoredFileBrowser = ({ files, stats, isPreview = false }: RestoredFileBr
|
|
|
202
153
|
const isDirectory = directories.includes(normalizedPath);
|
|
203
154
|
const hasUpdates = hasUpdatedContent(normalizedPath);
|
|
204
155
|
const fileAction = file.action;
|
|
205
|
-
const isRestored = file.action === 'restored'
|
|
156
|
+
const isRestored = file.action === 'restored';
|
|
206
157
|
const fileActionLabel = isRestored ? 'New' : file.action;
|
|
207
158
|
|
|
208
159
|
return (
|
|
209
160
|
<div
|
|
210
|
-
style={style}
|
|
211
|
-
className={`${
|
|
161
|
+
style={{ ...style, gridTemplateColumns: GRID_COLUMNS }}
|
|
162
|
+
className={`${sbClasses.snapshotFile} ${isDirectory ? sbClasses.fileIsDir : ''}`}
|
|
212
163
|
onClick={() => {
|
|
213
164
|
if (isDirectory) {
|
|
214
165
|
setSelectedFolder(normalizedPath);
|
|
215
|
-
|
|
166
|
+
expandParentFolders(normalizedPath);
|
|
216
167
|
}
|
|
217
168
|
}}
|
|
218
169
|
>
|
|
219
|
-
<div className={
|
|
220
|
-
{isDirectory ? <Icon type={
|
|
170
|
+
<div className={sbClasses.fileName}>
|
|
171
|
+
{isDirectory ? <Icon type={'fm-directory'} size={16} /> : <FileIcon filename={fileName || ''} />}{' '}
|
|
221
172
|
{isRestored && <span className={classes.newFileIndicator} />} {fileName}
|
|
222
173
|
{hasUpdates && <i />}
|
|
223
174
|
</div>
|
|
@@ -230,17 +181,19 @@ const RestoredFileBrowser = ({ files, stats, isPreview = false }: RestoredFileBr
|
|
|
230
181
|
};
|
|
231
182
|
|
|
232
183
|
return (
|
|
233
|
-
<div className={
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
184
|
+
<div className={sbClasses.snapshotBrowser}>
|
|
185
|
+
<SnapshotBrowserToolbar
|
|
186
|
+
search={search}
|
|
187
|
+
onSearchChange={setSearch}
|
|
188
|
+
leftContent={
|
|
189
|
+
stats && (
|
|
190
|
+
<div className={sbClasses.stats}>
|
|
238
191
|
<strong>Summary: </strong> {formatNumberToK(stats.total_files)} Items {' • '}
|
|
239
192
|
{formatBytes(stats.bytes_restored)}/{formatBytes(stats.total_bytes)}
|
|
240
193
|
</div>
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
rightContent={
|
|
244
197
|
<div className={classes.filters}>
|
|
245
198
|
{(Object.keys(filters) as Array<keyof typeof filters>).map((action) => (
|
|
246
199
|
<label key={action}>
|
|
@@ -249,81 +202,43 @@ const RestoredFileBrowser = ({ files, stats, isPreview = false }: RestoredFileBr
|
|
|
249
202
|
</label>
|
|
250
203
|
))}
|
|
251
204
|
</div>
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
205
|
+
}
|
|
206
|
+
/>
|
|
207
|
+
|
|
208
|
+
<div className={sbClasses.browserContent}>
|
|
209
|
+
<SnapshotBrowserDirectories
|
|
210
|
+
directories={directories}
|
|
211
|
+
selectedFolder={selectedFolder}
|
|
212
|
+
expandedFolders={expandedFolders}
|
|
213
|
+
onDirectoryClick={(dir) => {
|
|
214
|
+
setSelectedFolder(dir);
|
|
215
|
+
expandParentFolders(dir);
|
|
216
|
+
}}
|
|
217
|
+
onToggleFolder={toggleFolder}
|
|
218
|
+
isVisible={isVisible}
|
|
219
|
+
hasSubdirectories={hasSubdirectories}
|
|
220
|
+
renderDirectoryExtra={(dir) => {
|
|
267
221
|
const hasUpdates = hasUpdatedContent(dir);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
>
|
|
282
|
-
|
|
283
|
-
<button
|
|
284
|
-
className={classes.toggleButton}
|
|
285
|
-
onClick={(e) => {
|
|
286
|
-
e.stopPropagation();
|
|
287
|
-
toggleFolder(dir);
|
|
288
|
-
}}
|
|
289
|
-
>
|
|
290
|
-
{isExpanded ? '-' : '+'}
|
|
291
|
-
</button>
|
|
292
|
-
) : (
|
|
293
|
-
<span className={`${classes.togglePlaceholder}`} />
|
|
294
|
-
)}
|
|
295
|
-
<span className={classes.dirName}>
|
|
296
|
-
<Icon type={'fm-directory'} size={14} /> {dirName}
|
|
297
|
-
{hasUpdates && <span className={classes.notification} />}
|
|
298
|
-
</span>
|
|
299
|
-
</div>
|
|
300
|
-
);
|
|
222
|
+
return hasUpdates ? <span className={classes.notification} /> : null;
|
|
223
|
+
}}
|
|
224
|
+
/>
|
|
225
|
+
|
|
226
|
+
<div className={sbClasses.content}>
|
|
227
|
+
<SnapshotBrowserFileList
|
|
228
|
+
files={Array(totalItems)}
|
|
229
|
+
height={window.innerHeight - (isPreview ? 370 : 250)}
|
|
230
|
+
itemSize={ITEM_HEIGHT}
|
|
231
|
+
headerContent={
|
|
232
|
+
<>
|
|
233
|
+
<div>Name</div>
|
|
234
|
+
<div>Status</div>
|
|
235
|
+
<div>Size</div>
|
|
236
|
+
</>
|
|
301
237
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
<div className={classes.content}>
|
|
307
|
-
<div className={classes.fileList}>
|
|
308
|
-
<div className={classes.header}>
|
|
309
|
-
<div>Name</div>
|
|
310
|
-
<div>Status</div>
|
|
311
|
-
<div>Size</div>
|
|
312
|
-
</div>
|
|
313
|
-
{selectedFolder && totalItems > 0 ? (
|
|
314
|
-
<List
|
|
315
|
-
height={window.innerHeight - (isPreview ? 370 : 250)}
|
|
316
|
-
itemCount={totalItems}
|
|
317
|
-
itemSize={ITEM_HEIGHT}
|
|
318
|
-
width="100%"
|
|
319
|
-
className={`${classes.fileListVirtualized} styled__scrollbar`}
|
|
320
|
-
>
|
|
321
|
-
{FileRow}
|
|
322
|
-
</List>
|
|
323
|
-
) : (
|
|
324
|
-
<div className={classes.fileListEmpty}>Select a folder from the left to browse its content</div>
|
|
325
|
-
)}
|
|
326
|
-
</div>
|
|
238
|
+
renderRow={FileRow}
|
|
239
|
+
selectedFolder={selectedFolder || null}
|
|
240
|
+
gridTemplateColumns={GRID_COLUMNS}
|
|
241
|
+
/>
|
|
327
242
|
</div>
|
|
328
243
|
</div>
|
|
329
244
|
</div>
|