@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.
Files changed (143) hide show
  1. package/dist-lib/@types/index.js +4 -1
  2. package/dist-lib/@types/index.js.map +1 -1
  3. package/dist-lib/@types/settings.d.ts +14 -0
  4. package/dist-lib/@types/settings.d.ts.map +1 -1
  5. package/dist-lib/@types/settings.js +34 -0
  6. package/dist-lib/@types/settings.js.map +1 -0
  7. package/dist-lib/components/Plan/Backups/Backups.d.ts.map +1 -1
  8. package/dist-lib/components/Plan/Backups/Backups.js +189 -159
  9. package/dist-lib/components/Plan/Backups/Backups.js.map +1 -1
  10. package/dist-lib/components/Plan/PlanSettings/PlanNotificationSettings.d.ts.map +1 -1
  11. package/dist-lib/components/Plan/PlanSettings/PlanNotificationSettings.js +148 -90
  12. package/dist-lib/components/Plan/PlanSettings/PlanNotificationSettings.js.map +1 -1
  13. package/dist-lib/components/Plan/SnapshotViewer/SnapshotViewer.d.ts +32 -0
  14. package/dist-lib/components/Plan/SnapshotViewer/SnapshotViewer.d.ts.map +1 -0
  15. package/dist-lib/components/Plan/SnapshotViewer/SnapshotViewer.js +252 -0
  16. package/dist-lib/components/Plan/SnapshotViewer/SnapshotViewer.js.map +1 -0
  17. package/dist-lib/components/Plan/SnapshotViewer/SnapshotViewerFile.d.ts +23 -0
  18. package/dist-lib/components/Plan/SnapshotViewer/SnapshotViewerFile.d.ts.map +1 -0
  19. package/dist-lib/components/Plan/SnapshotViewer/SnapshotViewerFile.js +72 -0
  20. package/dist-lib/components/Plan/SnapshotViewer/SnapshotViewerFile.js.map +1 -0
  21. package/dist-lib/components/Restore/RestoreFileSelector/RestoreFileSelector.d.ts.map +1 -1
  22. package/dist-lib/components/Restore/RestoreFileSelector/RestoreFileSelector.js +188 -198
  23. package/dist-lib/components/Restore/RestoreFileSelector/RestoreFileSelector.js.map +1 -1
  24. package/dist-lib/components/Restore/RestoreFileSelector/RestoreFileSelector.module.scss.js +20 -64
  25. package/dist-lib/components/Restore/RestoreFileSelector/RestoreFileSelector.module.scss.js.map +1 -1
  26. package/dist-lib/components/Restore/RestoredFileBrowser/RestoredFileBrowser.d.ts.map +1 -1
  27. package/dist-lib/components/Restore/RestoredFileBrowser/RestoredFileBrowser.js +125 -159
  28. package/dist-lib/components/Restore/RestoredFileBrowser/RestoredFileBrowser.js.map +1 -1
  29. package/dist-lib/components/Settings/IntegrationSettings/IntegrationSettings.d.ts.map +1 -1
  30. package/dist-lib/components/Settings/IntegrationSettings/IntegrationSettings.js +52 -47
  31. package/dist-lib/components/Settings/IntegrationSettings/IntegrationSettings.js.map +1 -1
  32. package/dist-lib/components/Settings/IntegrationSettings/IntegrationSettings.module.scss.js +12 -6
  33. package/dist-lib/components/Settings/IntegrationSettings/IntegrationSettings.module.scss.js.map +1 -1
  34. package/dist-lib/components/Settings/IntegrationSettings/NtfySettings.d.ts +9 -0
  35. package/dist-lib/components/Settings/IntegrationSettings/NtfySettings.d.ts.map +1 -0
  36. package/dist-lib/components/Settings/IntegrationSettings/NtfySettings.js +79 -0
  37. package/dist-lib/components/Settings/IntegrationSettings/NtfySettings.js.map +1 -0
  38. package/dist-lib/components/Settings/IntegrationSettings/SMTPSettings.d.ts +4 -3
  39. package/dist-lib/components/Settings/IntegrationSettings/SMTPSettings.d.ts.map +1 -1
  40. package/dist-lib/components/Settings/IntegrationSettings/SMTPSettings.js +37 -35
  41. package/dist-lib/components/Settings/IntegrationSettings/SMTPSettings.js.map +1 -1
  42. package/dist-lib/components/Settings/IntegrationSettings/ValidateEmailIntegration.d.ts +10 -0
  43. package/dist-lib/components/Settings/IntegrationSettings/ValidateEmailIntegration.d.ts.map +1 -0
  44. package/dist-lib/components/Settings/IntegrationSettings/ValidateEmailIntegration.js +49 -0
  45. package/dist-lib/components/Settings/IntegrationSettings/ValidateEmailIntegration.js.map +1 -0
  46. package/dist-lib/components/Storage/EditStorage/EditStorage.js +10 -10
  47. package/dist-lib/components/Storage/EditStorage/EditStorage.js.map +1 -1
  48. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowser.module.scss.js +74 -0
  49. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowser.module.scss.js.map +1 -0
  50. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserDirectories.d.ts +17 -0
  51. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserDirectories.d.ts.map +1 -0
  52. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserDirectories.js +57 -0
  53. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserDirectories.js.map +1 -0
  54. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserFileList.d.ts +18 -0
  55. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserFileList.d.ts.map +1 -0
  56. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserFileList.js +24 -0
  57. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserFileList.js.map +1 -0
  58. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserFileRow.d.ts +18 -0
  59. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserFileRow.d.ts.map +1 -0
  60. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserFileRow.js +37 -0
  61. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserFileRow.js.map +1 -0
  62. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserGoUpRow.d.ts +9 -0
  63. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserGoUpRow.d.ts.map +1 -0
  64. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserGoUpRow.js +15 -0
  65. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserGoUpRow.js.map +1 -0
  66. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserToolbar.d.ts +11 -0
  67. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserToolbar.d.ts.map +1 -0
  68. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserToolbar.js +18 -0
  69. package/dist-lib/components/common/SnapshotBrowser/SnapshotBrowserToolbar.js.map +1 -0
  70. package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotDatabase.d.ts +12 -0
  71. package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotDatabase.d.ts.map +1 -0
  72. package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotDatabase.js +70 -0
  73. package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotDatabase.js.map +1 -0
  74. package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotNavigation.d.ts +22 -0
  75. package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotNavigation.d.ts.map +1 -0
  76. package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotNavigation.js +79 -0
  77. package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotNavigation.js.map +1 -0
  78. package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotSort.d.ts +6 -0
  79. package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotSort.d.ts.map +1 -0
  80. package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotSort.js +18 -0
  81. package/dist-lib/components/common/SnapshotBrowser/hooks/useSnapshotSort.js.map +1 -0
  82. package/dist-lib/components/common/SnapshotBrowser/index.d.ts +11 -0
  83. package/dist-lib/components/common/SnapshotBrowser/index.d.ts.map +1 -0
  84. package/dist-lib/components/index.d.ts +12 -0
  85. package/dist-lib/components/index.d.ts.map +1 -1
  86. package/dist-lib/components.js +152 -128
  87. package/dist-lib/components.js.map +1 -1
  88. package/dist-lib/hooks/usePlanSingleActions.d.ts.map +1 -1
  89. package/dist-lib/hooks/usePlanSingleActions.js +21 -21
  90. package/dist-lib/hooks/usePlanSingleActions.js.map +1 -1
  91. package/dist-lib/services/backups.d.ts +4 -0
  92. package/dist-lib/services/backups.d.ts.map +1 -1
  93. package/dist-lib/services/backups.js +34 -25
  94. package/dist-lib/services/backups.js.map +1 -1
  95. package/dist-lib/services/settings.d.ts +3 -2
  96. package/dist-lib/services/settings.d.ts.map +1 -1
  97. package/dist-lib/services/settings.js +0 -1
  98. package/dist-lib/services/settings.js.map +1 -1
  99. package/dist-lib/services.js +113 -112
  100. package/dist-lib/styles/core-frontend.css +1 -1
  101. package/dist-lib/utils/index.d.ts +1 -0
  102. package/dist-lib/utils/index.d.ts.map +1 -1
  103. package/dist-lib/utils/snapshotDatabase.d.ts +16 -0
  104. package/dist-lib/utils/snapshotDatabase.d.ts.map +1 -0
  105. package/dist-lib/utils/snapshotDatabase.js +105 -0
  106. package/dist-lib/utils/snapshotDatabase.js.map +1 -0
  107. package/dist-lib/utils.js +24 -22
  108. package/dist-lib/utils.js.map +1 -1
  109. package/package.json +1 -1
  110. package/src/@types/settings.ts +43 -0
  111. package/src/components/Plan/Backups/Backups.tsx +40 -1
  112. package/src/components/Plan/PlanSettings/PlanNotificationSettings.tsx +65 -0
  113. package/src/components/Plan/SnapshotViewer/SnapshotViewer.tsx +344 -0
  114. package/src/components/Plan/SnapshotViewer/SnapshotViewerFile.tsx +89 -0
  115. package/src/components/Restore/RestoreFileSelector/RestoreFileSelector.tsx +82 -145
  116. package/src/components/Restore/RestoredFileBrowser/RestoredFileBrowser.tsx +71 -156
  117. package/src/components/Settings/IntegrationSettings/IntegrationSettings.module.scss +16 -0
  118. package/src/components/Settings/IntegrationSettings/IntegrationSettings.tsx +45 -42
  119. package/src/components/Settings/IntegrationSettings/NtfySettings.tsx +106 -0
  120. package/src/components/Settings/IntegrationSettings/SMTPSettings.tsx +28 -19
  121. package/src/components/Settings/IntegrationSettings/ValidateEmailIntegration.tsx +58 -0
  122. package/src/components/Storage/EditStorage/EditStorage.tsx +1 -1
  123. package/src/components/common/SnapshotBrowser/SnapshotBrowser.module.scss +376 -0
  124. package/src/components/common/SnapshotBrowser/SnapshotBrowserDirectories.tsx +84 -0
  125. package/src/components/common/SnapshotBrowser/SnapshotBrowserFileList.tsx +52 -0
  126. package/src/components/common/SnapshotBrowser/SnapshotBrowserFileRow.tsx +44 -0
  127. package/src/components/common/SnapshotBrowser/SnapshotBrowserGoUpRow.tsx +22 -0
  128. package/src/components/common/SnapshotBrowser/SnapshotBrowserToolbar.tsx +29 -0
  129. package/src/components/common/SnapshotBrowser/hooks/useSnapshotDatabase.ts +130 -0
  130. package/src/components/common/SnapshotBrowser/hooks/useSnapshotNavigation.ts +154 -0
  131. package/src/components/common/SnapshotBrowser/hooks/useSnapshotSort.ts +24 -0
  132. package/src/components/common/SnapshotBrowser/index.ts +13 -0
  133. package/src/components/index.ts +15 -0
  134. package/src/hooks/usePlanSingleActions.tsx +5 -3
  135. package/src/services/backups.ts +12 -0
  136. package/src/services/settings.ts +2 -2
  137. package/src/utils/index.ts +1 -0
  138. package/src/utils/snapshotDatabase.ts +201 -0
  139. /package/dist-lib/providers/{azureBlob.png → azureblob.png} +0 -0
  140. /package/dist-lib/providers/{azureFiles.png → azurefiles.png} +0 -0
  141. /package/dist-lib/providers/{files.png → filescom.png} +0 -0
  142. /package/dist-lib/providers/{oracle.png → oracleobjectstorage.png} +0 -0
  143. /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
- <div
315
+ <SnapshotBrowserGoUpRow
347
316
  style={style}
348
- className={`${classes.file} ${classes.fileIsDir} ${classes.goUpButton}`}
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
- <div className={classes.fileName}>...</div>
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={`${classes.file} ${isDirectory ? classes.fileIsDir : ''} ${showChange && file.changeType === 'modified' ? classes.fileModified : ''} ${showChange && file.changeType === 'removed' ? classes.fileRemoved : ''}`}
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={classes.fileName}>
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={classes.restoredFileBrowser}>
410
- <div className={classes.toolbar}>
411
- <div className={classes.toolbarLeft}>
412
- <div className={classes.stats}>
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
- </div>
427
- <div className={classes.toolbarRight}>
428
- <div className={classes.search}>
429
- <Icon type="search" size={16} />
430
- <input type="text" placeholder="Search in current Directory..." value={search} onChange={(e) => setSearch(e.target.value)} />
431
- </div>
432
- </div>
433
- </div>
434
-
435
- <div className={classes.browserContent}>
436
- <div className={`${classes.sidebar} styled__scrollbar`}>
437
- <div className={classes.sidebarHeader}>Directories</div>
438
- {directories.map((dir) => {
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
- // Only render if parent folders are expanded or if it's a root folder
447
- if (depth === 0 || isVisible(dir)) {
448
- return (
449
- <div
450
- key={dir}
451
- className={`${classes.directory} ${selectedFolder === dir ? classes.selected : ''} ${hasChildren ? '' : classes.directoryEmpty}`}
452
- style={{ paddingLeft: `${depth * 20}px` }}
453
- onClick={() => setSelectedFolder(dir)}
454
- >
455
- {hasChildren ? (
456
- <button
457
- className={`${classes.toggleButton} ${isExpanded ? classes.active : ''}`}
458
- onClick={(e) => {
459
- e.stopPropagation();
460
- toggleFolder(dir);
461
- }}
462
- >
463
- {isExpanded ? '-' : '+'}
464
- </button>
465
- ) : (
466
- <span className={`${classes.togglePlaceholder}`} />
467
- )}
468
- <div className={classes.dirName}>
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
- return null;
486
- })}
487
- </div>
488
-
489
- <div className={`${classes.content} styled__scrollbar`}>
490
- <div className={classes.fileList}>
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 FileIcon from '../../common/FileIcon/FileIcon';
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
- // Set initial selected folder when directories change
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, files]) => {
104
- return path.startsWith(dir) && files.some((f) => f.action === 'restored' || f.action === 'updated');
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
- <div
131
+ <SnapshotBrowserGoUpRow
176
132
  style={style}
177
- className={`${classes.file} ${classes.fileIsDir} ${classes.goUpButton}`}
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
- expandParentDirectories(normalizedParentPath);
137
+ expandParentFolders(normalizedParentPath);
184
138
  setSelectedFolder(normalizedParentPath);
185
139
  }}
186
- >
187
- <div className={classes.fileName}>...</div>
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' ? true : false;
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={`${classes.file} ${isDirectory ? classes.fileIsDir : ''}`}
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
- expandParentDirectories(normalizedPath);
166
+ expandParentFolders(normalizedPath);
216
167
  }
217
168
  }}
218
169
  >
219
- <div className={classes.fileName}>
220
- {isDirectory ? <Icon type={isDirectory ? 'fm-directory' : 'fm-file'} size={16} /> : <FileIcon filename={fileName || ''} />}{' '}
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={classes.restoredFileBrowser}>
234
- <div className={classes.toolbar}>
235
- <div className={classes.toolbarLeft}>
236
- {stats && (
237
- <div className={classes.stats}>
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
- </div>
243
- <div className={classes.toolbarRight}>
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
- <div className={classes.search}>
253
- <Icon type="search" size={16} />
254
- <input type="text" placeholder="Search in current Directory..." value={search} onChange={(e) => setSearch(e.target.value)} />
255
- </div>
256
- </div>
257
- </div>
258
-
259
- <div className={classes.browserContent}>
260
- <div className={`${classes.sidebar} styled__scrollbar`}>
261
- <div className={classes.sidebarHeader}>Directories</div>
262
- {directories.map((dir) => {
263
- const parts = splitPath(dir);
264
- const dirName = parts[parts.length - 1];
265
- const depth = parts.length - 1;
266
- const isExpanded = expandedFolders.has(dir);
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
- const hasChildren = hasSubdirectories(dir);
269
-
270
- // Only render if parent folders are expanded or if it's a root folder
271
- if (depth === 0 || isVisible(dir)) {
272
- return (
273
- <div
274
- key={dir}
275
- className={`${classes.directory} ${selectedFolder === dir ? classes.selected : ''} ${hasChildren ? '' : classes.directoryEmpty}`}
276
- style={{ paddingLeft: `${depth * 20}px` }}
277
- onClick={() => {
278
- setSelectedFolder(dir);
279
- expandParentDirectories(dir);
280
- }}
281
- >
282
- {hasChildren ? (
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
- return null;
303
- })}
304
- </div>
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>