diff --git a/frontend/src/components/FleetView.tsx b/frontend/src/components/FleetView.tsx index a2a84e1f..5bcf129d 100644 --- a/frontend/src/components/FleetView.tsx +++ b/frontend/src/components/FleetView.tsx @@ -10,7 +10,7 @@ import { import { FleetMasthead } from './fleet/FleetMasthead'; import { FleetTopology } from './fleet/FleetTopology'; import { ReconnectingOverlay } from './FleetView/ReconnectingOverlay'; -import { NodeUpdatesModal } from './FleetView/NodeUpdatesModal'; +import { NodeUpdatesSheet } from './FleetView/NodeUpdatesSheet'; import { LocalUpdateConfirmDialog } from './FleetView/LocalUpdateConfirmDialog'; import { UpdateStatusBadge } from './FleetView/UpdateStatusBadge'; import type { NodeUpdateStatus } from './FleetView/types'; @@ -1255,8 +1255,8 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) { {/* Reconnecting overlay shown when local node is updating */} {reconnecting && } - {/* Node Updates modal */} - void; checkingUpdates: boolean; @@ -28,16 +26,16 @@ interface NodeUpdatesModalProps { triggerUpdateAll: () => Promise; } -export function NodeUpdatesModal({ +export function NodeUpdatesSheet({ open, onOpenChange, checkingUpdates, updateStatuses, updatingNodeId, fetchUpdateStatus, triggerNodeUpdate, retryNodeUpdate, dismissNodeUpdate, triggerUpdateAll, -}: NodeUpdatesModalProps) { - const [modalSearch, setModalSearch] = useState(''); +}: NodeUpdatesSheetProps) { + const [search, setSearch] = useState(''); const [recheckingUpdates, setRecheckingUpdates] = useState(false); const handleOpenChange = (next: boolean) => { onOpenChange(next); - if (!next) setModalSearch(''); + if (!next) setSearch(''); }; const handleRecheck = async () => { @@ -52,72 +50,75 @@ export function NodeUpdatesModal({ } }; - const upToDate = updateStatuses.filter(s => !s.updateAvailable && !s.updateStatus).length; + const upToDate = updateStatuses.filter(s => !s.updateAvailable && (!s.updateStatus || s.updateStatus === 'completed')).length; const available = updateStatuses.filter(s => s.updateAvailable && !s.updateStatus).length; const updating = updateStatuses.filter(s => s.updateStatus === 'updating').length; const failed = updateStatuses.filter(s => s.updateStatus === 'failed' || s.updateStatus === 'timeout').length; const updatableRemoteCount = updateStatuses.filter(s => s.updateAvailable && !s.updateStatus && s.type === 'remote').length; - const q = modalSearch.toLowerCase(); + const q = search.toLowerCase(); const filtered = q ? updateStatuses.filter(s => s.name.toLowerCase().includes(q) || s.type.includes(q)) : updateStatuses; - const gatewayLabel = formatVersion(updateStatuses[0]?.latestVersion); + const localEntry = updateStatuses.find(s => s.type === 'local') ?? updateStatuses[0]; + const gatewayLabel = formatVersion(localEntry?.latestVersion); return ( - - - - Node Updates - Check and apply updates across your fleet nodes. - + + + + Node Updates + Check and apply updates across your fleet nodes. + {checkingUpdates ? ( - + Checking for updates... ) : updateStatuses.length === 0 ? ( - + No nodes found. ) : ( - <> + {/* Summary stats */} - - - {upToDate} - - Up to date + + + + {upToDate} + + Up to date + - - - {available} - - Available + + {available} + + Available + - - - {updating} - - Updating + + {updating} + + Updating + - - - {failed} - - Failed + + {failed} + + Failed + {/* Search + gateway version */} - + setModalSearch(e.target.value)} + value={search} + onChange={e => setSearch(e.target.value)} className="h-8 pl-8 text-xs" /> @@ -128,18 +129,20 @@ export function NodeUpdatesModal({ )} - {/* Table header */} - - Node - Type - Current - Latest - Status + {/* Table column header */} + + + Node + Type + Current + Latest + Status + - {/* Node list */} - - + {/* Node list — fills remaining height, no cap */} + + {filtered.map(s => ( @@ -194,14 +197,14 @@ export function NodeUpdatesModal({ ))} {filtered.length === 0 && ( - No nodes match “{modalSearch}” + No nodes match “{search}” )} {/* Footer */} - + )} - > + )} - - + + ); }