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();
}}
>