From ca28f42edbfebce776c8551d5b5e3d2e217b6772 Mon Sep 17 00:00:00 2001 From: Pulse Monitor Date: Thu, 28 Aug 2025 14:39:36 +0000 Subject: [PATCH] feat: add grouping toggle for flat list view adds a grouped/list toggle to the dashboard that allows users to switch between: - grouped view: vms/containers grouped by node (default) - list view: flat list of all vms/containers across cluster in list view, users can sort by any column to identify top resource consumers across the entire cluster, useful for monitoring high bandwidth/cpu/memory usage addresses #370 --- .../src/components/Dashboard/Dashboard.tsx | 73 ++++++++++++++++++- .../components/Dashboard/DashboardFilter.tsx | 33 ++++++++- 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/frontend-modern/src/components/Dashboard/Dashboard.tsx b/frontend-modern/src/components/Dashboard/Dashboard.tsx index 855047d49..4d8d54a2a 100644 --- a/frontend-modern/src/components/Dashboard/Dashboard.tsx +++ b/frontend-modern/src/components/Dashboard/Dashboard.tsx @@ -20,6 +20,7 @@ interface DashboardProps { type ViewMode = 'all' | 'vm' | 'lxc'; type StatusMode = 'all' | 'running' | 'stopped'; +type GroupingMode = 'grouped' | 'flat'; export function Dashboard(props: DashboardProps) { @@ -56,6 +57,12 @@ export function Dashboard(props: DashboardProps) { (storedStatusMode === 'all' || storedStatusMode === 'running' || storedStatusMode === 'stopped') ? storedStatusMode : 'all' ); + // Grouping mode - grouped by node or flat list + const storedGroupingMode = localStorage.getItem('dashboardGroupingMode'); + const [groupingMode, setGroupingMode] = createSignal( + (storedGroupingMode === 'grouped' || storedGroupingMode === 'flat') ? storedGroupingMode : 'grouped' + ); + const [showFilters, setShowFilters] = createSignal( localStorage.getItem('dashboardShowFilters') !== null ? localStorage.getItem('dashboardShowFilters') === 'true' @@ -91,6 +98,10 @@ export function Dashboard(props: DashboardProps) { localStorage.setItem('dashboardStatusMode', statusMode()); }); + createEffect(() => { + localStorage.setItem('dashboardGroupingMode', groupingMode()); + }); + createEffect(() => { @@ -241,10 +252,64 @@ export function Dashboard(props: DashboardProps) { return guests; }); - // Group by node + // Group by node or return flat list based on grouping mode const groupedGuests = createMemo(() => { const guests = filteredGuests(); + // If flat mode, return all guests in a single group + if (groupingMode() === 'flat') { + const groups: Record = { '': guests }; + // Sort the flat list + const key = sortKey(); + const dir = sortDirection(); + if (key) { + groups[''] = groups[''].sort((a, b) => { + let aVal: string | number | boolean | null | undefined = a[key] as string | number | boolean | null | undefined; + let bVal: string | number | boolean | null | undefined = b[key] as string | number | boolean | null | undefined; + + // Special handling for percentage-based columns + if (key === 'cpu') { + // CPU is displayed as percentage + aVal = a.cpu * 100; + bVal = b.cpu * 100; + } else if (key === 'memory') { + // Memory is displayed as percentage (use pre-calculated usage) + aVal = a.memory ? (a.memory.usage || 0) : 0; + bVal = b.memory ? (b.memory.usage || 0) : 0; + } else if (key === 'disk') { + // Disk is displayed as percentage + aVal = a.disk.total > 0 ? (a.disk.used / a.disk.total) * 100 : 0; + bVal = b.disk.total > 0 ? (b.disk.used / b.disk.total) * 100 : 0; + } + + // Handle null/undefined/empty values - put at end for both asc and desc + const aIsEmpty = aVal === null || aVal === undefined || aVal === ''; + const bIsEmpty = bVal === null || bVal === undefined || bVal === ''; + + if (aIsEmpty && bIsEmpty) return 0; + if (aIsEmpty) return 1; + if (bIsEmpty) return -1; + + // Type-specific value preparation + if (typeof aVal === 'number' && typeof bVal === 'number') { + // Numeric comparison + const comparison = aVal < bVal ? -1 : 1; + return dir === 'asc' ? comparison : -comparison; + } else { + // String comparison (case-insensitive) + const aStr = String(aVal).toLowerCase(); + const bStr = String(bVal).toLowerCase(); + + if (aStr === bStr) return 0; + const comparison = aStr < bStr ? -1 : 1; + return dir === 'asc' ? comparison : -comparison; + } + }); + } + return groups; + } + + // Original grouped by node logic const groups: Record = {}; guests.forEach(guest => { if (!groups[guest.node]) { @@ -489,6 +554,8 @@ export function Dashboard(props: DashboardProps) { setViewMode={setViewMode} statusMode={statusMode} setStatusMode={setStatusMode} + groupingMode={groupingMode} + setGroupingMode={setGroupingMode} setSortKey={setSortKey} setSortDirection={setSortDirection} searchInputRef={(el) => searchInputRef = el} @@ -631,7 +698,7 @@ export function Dashboard(props: DashboardProps) { a.localeCompare(b))} fallback={<>}> {([node, guests]) => ( <> - + ); diff --git a/frontend-modern/src/components/Dashboard/DashboardFilter.tsx b/frontend-modern/src/components/Dashboard/DashboardFilter.tsx index 80936a436..efb8a3cd3 100644 --- a/frontend-modern/src/components/Dashboard/DashboardFilter.tsx +++ b/frontend-modern/src/components/Dashboard/DashboardFilter.tsx @@ -9,6 +9,8 @@ interface DashboardFilterProps { setViewMode: (value: 'all' | 'vm' | 'lxc') => void; statusMode: () => 'all' | 'running' | 'stopped'; setStatusMode: (value: 'all' | 'running' | 'stopped') => void; + groupingMode: () => 'grouped' | 'flat'; + setGroupingMode: (value: 'grouped' | 'flat') => void; setSortKey: (value: string) => void; setSortDirection: (value: string) => void; searchInputRef?: (el: HTMLInputElement) => void; @@ -146,6 +148,34 @@ export const DashboardFilter: Component = (props) => { + {/* Grouping Mode Toggle */} +
+ + +
+ + + {/* Reset Button */} {/* Active Indicator */} - + Active