From 6f132b7ffe93132d9c296f53ebdaf5084706341a Mon Sep 17 00:00:00 2001 From: Anso Date: Mon, 20 Apr 2026 14:53:12 -0400 Subject: [PATCH] feat(fleet): reorganize overview page for clarity and density (#712) * feat(fleet): reorganize overview page for clarity and density Scope Grid/Topology to the Overview tab (moved from above the tab bar so it no longer implies it applies to Snapshots). Move Check Updates and Refresh onto the tab bar row, right-aligned. Compact the overview toolbar: constrain the sort combobox to a fixed width so it no longer stretches full-page, and collapse the Status, Type, Severity, and Tags pill groups into a single Filters popover with an active-count badge and an inline Clear all filters action. Render the local node card in the same responsive grid as remote nodes instead of a dedicated full-width row. Visual distinction is preserved through the existing brand gradient, cyan rail, ring, and "Local" badge. * fix(fleet): stop local card from stretching when remote expands Merging the local and remote node cards into a single responsive grid meant CSS grid's default align-items: stretch made every cell in a row match the tallest one. Expanding stack details on a remote card pulled the local card up with it. Add items-start on the merged grid so each cell sizes to its own content. --- frontend/src/components/FleetView.tsx | 421 ++++++++++++++------------ 1 file changed, 224 insertions(+), 197 deletions(-) diff --git a/frontend/src/components/FleetView.tsx b/frontend/src/components/FleetView.tsx index d091c199..4ec4a628 100644 --- a/frontend/src/components/FleetView.tsx +++ b/frontend/src/components/FleetView.tsx @@ -3,7 +3,7 @@ import { Server, Cpu, MemoryStick, HardDrive, RefreshCw, ChevronDown, ChevronRight, Layers, Wifi, WifiOff, Search, ArrowUpDown, AlertTriangle, Play, Square, RotateCcw, ExternalLink, Camera, Download, Loader2, Check, - CircleCheck, CircleAlert, Globe, Monitor, X, LayoutGrid, Network, + CircleCheck, CircleAlert, Globe, Monitor, X, LayoutGrid, Network, SlidersHorizontal, } from 'lucide-react'; import { FleetMasthead } from './fleet/FleetMasthead'; import { FleetTopology } from './fleet/FleetTopology'; @@ -12,6 +12,7 @@ import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { Input } from '@/components/ui/input'; import { Combobox } from '@/components/ui/combobox'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { ScrollArea } from '@/components/ui/scroll-area'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, @@ -39,6 +40,8 @@ interface FleetPaletteEntry { function labelPaletteKey(name: string, color: LabelColor): string { return `${name.trim().toLowerCase()}|${color}`; } + +const FILTER_SECTION_LABEL_CLASS = 'text-[11px] font-medium uppercase tracking-wider text-muted-foreground'; import { MultiSelectCombobox } from '@/components/ui/multi-select-combobox'; import { formatVersion } from '@/lib/version'; import { CursorProvider, Cursor, CursorFollow, CursorContainer } from '@/components/animate-ui/primitives/animate/cursor'; @@ -993,6 +996,18 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) { critical: n.status === 'online' && isCritical(n), })), [processedNodes]); + const showPaidControls = isPaid && viewMode === 'grid'; + const activeFilterCount = + (prefs.filterStatus !== 'all' ? 1 : 0) + + (prefs.filterType !== 'all' ? 1 : 0) + + (prefs.filterCritical ? 1 : 0) + + (labelFilters.size > 0 ? 1 : 0); + const clearFilters = useCallback(() => { + updatePrefs({ filterStatus: 'all', filterType: 'all', filterCritical: false }); + setLabelFilters(new Set()); + }, [updatePrefs]); + const allNodes = localNode ? [localNode, ...remoteNodes] : remoteNodes; + return (
-
-
- - -
-
- {isPaid && ( + +
+ + + + Overview + + {isPaid && ( + + + Snapshots + + + )} + + +
+ {isPaid && ( + + )} - )} - +
-
- - - - - - Overview - - {isPaid && ( - - - Snapshots - - - )} - - {/* Loading State */} @@ -1110,147 +1102,183 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) { {/* Fleet Content */} {!loading && nodes.length > 0 && ( <> - {/* Paid: Search, Sort & Filter Toolbar */} - {isPaid && viewMode === 'grid' && ( -
- {/* Search */} -
- - setSearchQuery(e.target.value)} - className="pl-9 h-9" - /> -
- - {/* Sort */} - updatePrefs({ sortBy: v as SortField })} - placeholder="Sort by..." - /> - - - - {/* Filter pills */} -
- {(['all', 'online', 'offline'] as FilterStatus[]).map(status => ( - - ))} -
- -
- {(['all', 'local', 'remote'] as FilterType[]).map(type => ( - - ))} -
- - - - {fleetPalette.length > 0 && ( - <> -
- ({ value: p.key, label: p.name, color: p.color }))} - selected={labelFilters} - onSelectionChange={setLabelFilters} - placeholder="Tags" - renderOption={(option) => ( - - - {option.label} - - )} + {/* Overview Toolbar: Search, Sort, Filters, View Mode */} +
+ {showPaidControls && ( + <> +
+ + setSearchQuery(e.target.value)} + className="pl-9 h-9" /> - - )} -
- )} +
+
+ updatePrefs({ sortBy: v as SortField })} + placeholder="Sort by..." + /> +
+ + + + + + +
+ +
+ {(['all', 'online', 'offline'] as FilterStatus[]).map(status => ( + + ))} +
+
+
+ +
+ {(['all', 'local', 'remote'] as FilterType[]).map(type => ( + + ))} +
+
+
+ + +
+ {fleetPalette.length > 0 && ( +
+ + ({ value: p.key, label: p.name, color: p.color }))} + selected={labelFilters} + onSelectionChange={setLabelFilters} + placeholder="Tags" + renderOption={(option) => ( + + + {option.label} + + )} + /> +
+ )} + {activeFilterCount > 0 && ( + + )} +
+
+ + )} + +
+ + +
+
- {/* Node Grid or Topology */} {viewMode === 'topology' && processedNodes.length > 0 ? ( onNavigateToNode(id, '')} /> ) : processedNodes.length > 0 ? ( -
- {localNode && ( -
- -
- )} - {remoteNodes.length > 0 && ( -
- {remoteNodes.map(node => ( - - ))} -
- )} +
+ {allNodes.map(node => ( + + ))}
) : (
@@ -1263,8 +1291,7 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) { className="mt-3" onClick={() => { setSearchQuery(''); - updatePrefs({ filterStatus: 'all', filterType: 'all', filterCritical: false }); - setLabelFilters(new Set()); + clearFilters(); }} >