From 8277bda0fa657634a6747a7384f0f9404d3f1f7f Mon Sep 17 00:00:00 2001 From: Anso Date: Mon, 4 May 2026 17:03:36 -0400 Subject: [PATCH] refactor(frontend): convert Node Updates dialog from modal to sheet (#917) Swaps Dialog for Sheet in NodeUpdatesSheet (renamed from NodeUpdatesModal). Sheet provides full viewport height, removing the max-h-[85vh] cap on the container and the max-h-[40vh] cap on the node list scroll area. Width fixed at 700px. Also fixes two stat-counter bugs carried over from the original inline code: completed nodes now count toward the Up to date tile, and the gateway latest-version label now resolves via the local node entry rather than relying on array position. No prop or behavior changes. --- frontend/src/components/FleetView.tsx | 6 +- ...eUpdatesModal.tsx => NodeUpdatesSheet.tsx} | 119 +++++++++--------- 2 files changed, 64 insertions(+), 61 deletions(-) rename frontend/src/components/FleetView/{NodeUpdatesModal.tsx => NodeUpdatesSheet.tsx} (66%) 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 */} -
+
- +
)} - -
+ + ); }