diff --git a/frontend/src/components/FleetView.tsx b/frontend/src/components/FleetView.tsx index 5bcf129d..c76d63e7 100644 --- a/frontend/src/components/FleetView.tsx +++ b/frontend/src/components/FleetView.tsx @@ -2,9 +2,9 @@ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { useExperimental } from '@/hooks/useExperimental'; import { Server, Cpu, MemoryStick, HardDrive, RefreshCw, ChevronDown, ChevronRight, - Layers, Wifi, WifiOff, Search, ArrowUpDown, AlertTriangle, - Play, Square, RotateCcw, ExternalLink, Camera, Download, Loader2, - LayoutGrid, Network, SlidersHorizontal, + Layers, Wifi, WifiOff, Search, AlertTriangle, + RotateCcw, ExternalLink, Camera, Download, Loader2, + Network, SlidersHorizontal, Send, KeyRound, ArrowLeftRight, } from 'lucide-react'; import { FleetMasthead } from './fleet/FleetMasthead'; @@ -13,13 +13,11 @@ import { ReconnectingOverlay } from './FleetView/ReconnectingOverlay'; import { NodeUpdatesSheet } from './FleetView/NodeUpdatesSheet'; import { LocalUpdateConfirmDialog } from './FleetView/LocalUpdateConfirmDialog'; import { UpdateStatusBadge } from './FleetView/UpdateStatusBadge'; -import type { NodeUpdateStatus } from './FleetView/types'; +import type { NodeUpdateStatus, ViewMode, FleetPreferences, FleetPaletteEntry } from './FleetView/types'; +import { OverviewToolbar } from './FleetView/OverviewToolbar'; import { Button } from '@/components/ui/button'; 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 { Tabs, TabsContent, TabsList, TabsTrigger, TabsHighlight, TabsHighlightItem } from '@/components/ui/tabs'; import { springs } from '@/lib/motion'; import { apiFetch, fetchForNode } from '@/lib/api'; @@ -33,21 +31,12 @@ import { DeploymentsTab } from './blueprints/DeploymentsTab'; import { toast } from '@/components/ui/toast-store'; import { LabelDot } from './LabelPill'; import { type Label as StackLabel, type LabelColor } from './label-types'; -import { MultiSelectCombobox } from '@/components/ui/multi-select-combobox'; import { formatVersion } from '@/lib/version'; -interface FleetPaletteEntry { - key: string; - name: string; - color: LabelColor; -} - function labelPaletteKey(name: string, color: LabelColor): string { return `${name.trim().toLowerCase()}|${color}`; } -const FILTER_SECTION_LABEL_CLASS = 'text-[10px] leading-3 font-mono uppercase tracking-[0.18em] text-stat-subtitle'; - // --- Types --- interface FleetNodeStats { @@ -82,19 +71,6 @@ interface StackContainer { Status?: string; } -type SortField = 'name' | 'cpu' | 'memory' | 'containers' | 'status'; -type SortDir = 'asc' | 'desc'; -type FilterStatus = 'all' | 'online' | 'offline'; -type FilterType = 'all' | 'local' | 'remote'; - -interface FleetPreferences { - sortBy: SortField; - sortDir: SortDir; - filterStatus: FilterStatus; - filterType: FilterType; - filterCritical: boolean; -} - const PREFS_KEY = 'sencho-fleet-preferences'; function loadPreferences(): FleetPreferences { @@ -522,7 +498,7 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) { const [refreshing, setRefreshing] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [lastSyncAt, setLastSyncAt] = useState(null); - const [viewMode, setViewMode] = useState<'grid' | 'topology'>('grid'); + const [viewMode, setViewMode] = useState('grid'); const [prefs, setPrefs] = useState(loadPreferences); const [fleetPalette, setFleetPalette] = useState([]); const [fleetStackLabelMap, setFleetStackLabelMap] = useState>>({}); @@ -850,12 +826,6 @@ 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()); @@ -991,162 +961,19 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) { {/* Fleet Content */} {!loading && nodes.length > 0 && ( <> - {/* 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 && ( - - )} -
-
- - )} - -
- - -
-
+ {viewMode === 'topology' && processedNodes.length > 0 ? ( [] = [ + { value: 'grid', label: 'Grid', icon: LayoutGrid }, + { value: 'topology', label: 'Topology', icon: Network }, +]; + +function renderPaletteOption(option: { label: string; color?: string }) { + return ( + + + {option.label} + + ); +} + +interface OverviewToolbarProps { + isPaid: boolean; + viewMode: ViewMode; + onViewModeChange: (mode: ViewMode) => void; + searchQuery: string; + onSearchQueryChange: (q: string) => void; + prefs: FleetPreferences; + onPrefsChange: (update: Partial) => void; + fleetPalette: FleetPaletteEntry[]; + labelFilters: Set; + onLabelFiltersChange: (filters: Set) => void; + onClearFilters: () => void; +} + +export function OverviewToolbar({ + isPaid, + viewMode, + onViewModeChange, + searchQuery, + onSearchQueryChange, + prefs, + onPrefsChange, + fleetPalette, + labelFilters, + onLabelFiltersChange, + onClearFilters, +}: OverviewToolbarProps) { + 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 paletteOptions = useMemo( + () => fleetPalette.map(p => ({ value: p.key, label: p.name, color: p.color })), + [fleetPalette], + ); + + return ( +
+ {showPaidControls && ( + <> +
+ + onSearchQueryChange(e.target.value)} + className="pl-9 h-9" + /> +
+
+ onPrefsChange({ sortBy: v as SortField })} + placeholder="Sort by..." + /> +
+ + + + + + +
+ +
+ {(['all', 'online', 'offline'] as FilterStatus[]).map(status => ( + + ))} +
+
+
+ +
+ {(['all', 'local', 'remote'] as FilterType[]).map(type => ( + + ))} +
+
+
+ + +
+ {fleetPalette.length > 0 && ( +
+ + +
+ )} + {activeFilterCount > 0 && ( + + )} +
+
+ + )} + + +
+ ); +} diff --git a/frontend/src/components/FleetView/types.ts b/frontend/src/components/FleetView/types.ts index c0c83f47..9feb03ab 100644 --- a/frontend/src/components/FleetView/types.ts +++ b/frontend/src/components/FleetView/types.ts @@ -1,3 +1,5 @@ +import type { LabelColor } from '../label-types'; + export interface NodeUpdateStatus { nodeId: number; name: string; @@ -8,3 +10,23 @@ export interface NodeUpdateStatus { updateStatus: 'updating' | 'completed' | 'timeout' | 'failed' | null; error?: string | null; } + +export type ViewMode = 'grid' | 'topology'; +export type SortField = 'name' | 'cpu' | 'memory' | 'containers' | 'status'; +export type SortDir = 'asc' | 'desc'; +export type FilterStatus = 'all' | 'online' | 'offline'; +export type FilterType = 'all' | 'local' | 'remote'; + +export interface FleetPreferences { + sortBy: SortField; + sortDir: SortDir; + filterStatus: FilterStatus; + filterType: FilterType; + filterCritical: boolean; +} + +export interface FleetPaletteEntry { + key: string; + name: string; + color: LabelColor; +}