From 324c4cccdca787f27a57e1730ade9c8b2b83ede3 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 5 Feb 2026 13:24:48 +0000 Subject: [PATCH] Update storage page and routes --- frontend-modern/src/App.tsx | 7 +- .../src/components/Storage/Storage.tsx | 133 +++++++++++++++++- 2 files changed, 137 insertions(+), 3 deletions(-) diff --git a/frontend-modern/src/App.tsx b/frontend-modern/src/App.tsx index b2f406184..f0ee1a78b 100644 --- a/frontend-modern/src/App.tsx +++ b/frontend-modern/src/App.tsx @@ -65,8 +65,10 @@ const Dashboard = lazy(() => ); const StorageComponent = lazy(() => import('./components/Storage/Storage')); const Backups = lazy(() => import('./components/Backups/Backups')); +const UnifiedBackups = lazy(() => import('./components/Backups/UnifiedBackups')); const Replication = lazy(() => import('./components/Replication/Replication')); const MailGateway = lazy(() => import('./components/PMG/MailGateway')); +const Services = lazy(() => import('./components/Services/Services')); const CephPage = lazy(() => import('./pages/Ceph')); const AlertsPage = lazy(() => import('./pages/Alerts').then((module) => ({ default: module.Alerts })), @@ -949,8 +951,9 @@ function App() { - } /> - } /> + + + diff --git a/frontend-modern/src/components/Storage/Storage.tsx b/frontend-modern/src/components/Storage/Storage.tsx index 2bacdcfb0..2a1551f27 100644 --- a/frontend-modern/src/components/Storage/Storage.tsx +++ b/frontend-modern/src/components/Storage/Storage.tsx @@ -207,6 +207,64 @@ const Storage: Component = () => { return map; }); + const cephSummaryStats = createMemo(() => { + const clusters = visibleCephClusters(); + const totals = clusters.reduce( + (acc, cluster) => { + acc.total += Math.max(0, cluster.totalBytes || 0); + acc.used += Math.max(0, cluster.usedBytes || 0); + acc.available += Math.max(0, cluster.availableBytes || 0); + return acc; + }, + { total: 0, used: 0, available: 0 }, + ); + const usagePercent = totals.total > 0 ? (totals.used / totals.total) * 100 : 0; + return { + clusters, + totalBytes: totals.total, + usedBytes: totals.used, + availableBytes: totals.available, + usagePercent, + }; + }); + + const pbsDatastoreStorage = createMemo(() => { + const instances = state.pbs || []; + const datastores: StorageType[] = []; + + instances.forEach((instance) => { + (instance.datastores || []).forEach((store) => { + const total = Number.isFinite(store.total) ? store.total : 0; + const used = Number.isFinite(store.used) ? store.used : 0; + const free = Number.isFinite(store.free) ? store.free : Math.max(total - used, 0); + const usage = total > 0 ? (used / total) * 100 : 0; + const instanceLabel = instance.name || instance.host || instance.id || 'PBS'; + const datastoreName = store.name || 'PBS Datastore'; + + datastores.push({ + id: `pbs-${instance.id || instanceLabel}-${datastoreName}`, + name: datastoreName, + node: instanceLabel, + instance: instance.id || instanceLabel, + type: 'pbs', + status: store.status || instance.status || 'unknown', + total, + used, + free, + usage, + content: 'backup', + shared: false, + enabled: true, + active: true, + nodes: [instanceLabel], + pbsNames: [datastoreName], + }); + }); + }); + + return datastores; + }); + const sortKeyOptions: { value: StorageSortKey; label: string }[] = [ { value: 'name', label: 'Name' }, @@ -220,7 +278,7 @@ const Storage: Component = () => { // Filter storage - in storage view, filter out 0 capacity and deduplicate const filteredStorage = createMemo(() => { - let storage = state.storage || []; + let storage = [...(state.storage || []), ...pbsDatastoreStorage()]; // In storage view, deduplicate identical storage and filter out 0 capacity if (viewMode() === 'storage') { @@ -582,6 +640,79 @@ const Storage: Component = () => { searchTerm={searchTerm()} /> + 0 + } + > + +
+
+
+ Ceph Summary +
+
+ {cephSummaryStats().clusters.length} cluster + {cephSummaryStats().clusters.length !== 1 ? 's' : ''} detected +
+
+
+
+ {formatBytes(cephSummaryStats().totalBytes)} +
+
+ {formatPercent(cephSummaryStats().usagePercent)} used +
+
+
+
+ + {(cluster) => ( +
+
+
+
+ {cluster.name || 'Ceph Cluster'} +
+ +
+ {cluster.healthMessage} +
+
+
+ + {getCephHealthLabel(cluster.health)} + +
+
+ +
+
+ {formatBytes(cluster.usedBytes)} used + {formatBytes(cluster.availableBytes)} free +
+
+ )} +
+
+
+
+ {/* Tab Toggle */}