mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-28 12:49:03 +00:00
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.
This commit is contained in:
@@ -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 && <ReconnectingOverlay preUpdateStartedAt={preUpdateStartedAt} />}
|
||||
|
||||
{/* Node Updates modal */}
|
||||
<NodeUpdatesModal
|
||||
{/* Node Updates sheet */}
|
||||
<NodeUpdatesSheet
|
||||
open={showUpdateModal}
|
||||
onOpenChange={setShowUpdateModal}
|
||||
checkingUpdates={checkingUpdates}
|
||||
|
||||
+61
-58
@@ -3,9 +3,7 @@ import {
|
||||
Search, Loader2, Check, CircleCheck, CircleAlert, AlertTriangle,
|
||||
Download, RefreshCw, Monitor, Globe,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -15,7 +13,7 @@ import { formatVersion } from '@/lib/version';
|
||||
import { UpdateStatusBadge } from './UpdateStatusBadge';
|
||||
import type { NodeUpdateStatus } from './types';
|
||||
|
||||
interface NodeUpdatesModalProps {
|
||||
interface NodeUpdatesSheetProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
checkingUpdates: boolean;
|
||||
@@ -28,16 +26,16 @@ interface NodeUpdatesModalProps {
|
||||
triggerUpdateAll: () => Promise<void>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[85vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Node Updates</DialogTitle>
|
||||
<DialogDescription className="sr-only">Check and apply updates across your fleet nodes.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Sheet open={open} onOpenChange={handleOpenChange}>
|
||||
<SheetContent side="right" className="w-[700px] sm:max-w-[700px] flex flex-col p-0">
|
||||
<SheetHeader className="px-6 pt-6 pb-4 shrink-0 border-b">
|
||||
<SheetTitle>Node Updates</SheetTitle>
|
||||
<SheetDescription className="sr-only">Check and apply updates across your fleet nodes.</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
{checkingUpdates ? (
|
||||
<div className="flex items-center justify-center py-16 text-muted-foreground text-sm gap-2">
|
||||
<div className="flex flex-1 items-center justify-center text-muted-foreground text-sm gap-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Checking for updates...
|
||||
</div>
|
||||
) : updateStatuses.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-16 text-muted-foreground text-sm">
|
||||
<div className="flex flex-1 items-center justify-center text-muted-foreground text-sm">
|
||||
No nodes found.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col flex-1 min-h-0">
|
||||
{/* Summary stats */}
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
<div className="rounded-lg border border-card-border border-t-card-border-top bg-card shadow-card-bevel px-3 py-2 text-center">
|
||||
<div className="text-lg font-medium tabular-nums tracking-tight text-stat-value">{upToDate}</div>
|
||||
<div className="text-[10px] text-stat-subtitle flex items-center justify-center gap-1">
|
||||
<CircleCheck className="w-3 h-3 text-success" strokeWidth={1.5} /> Up to date
|
||||
<div className="px-6 pt-4 pb-3 shrink-0">
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
<div className="rounded-lg border border-card-border border-t-card-border-top bg-card shadow-card-bevel px-3 py-2 text-center">
|
||||
<div className="text-lg font-medium tabular-nums tracking-tight text-stat-value">{upToDate}</div>
|
||||
<div className="text-[10px] text-stat-subtitle flex items-center justify-center gap-1">
|
||||
<CircleCheck className="w-3 h-3 text-success" strokeWidth={1.5} /> Up to date
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-card-border border-t-card-border-top bg-card shadow-card-bevel px-3 py-2 text-center">
|
||||
<div className="text-lg font-medium tabular-nums tracking-tight text-stat-value">{available}</div>
|
||||
<div className="text-[10px] text-stat-subtitle flex items-center justify-center gap-1">
|
||||
<CircleAlert className="w-3 h-3 text-warning" strokeWidth={1.5} /> Available
|
||||
<div className="rounded-lg border border-card-border border-t-card-border-top bg-card shadow-card-bevel px-3 py-2 text-center">
|
||||
<div className="text-lg font-medium tabular-nums tracking-tight text-stat-value">{available}</div>
|
||||
<div className="text-[10px] text-stat-subtitle flex items-center justify-center gap-1">
|
||||
<CircleAlert className="w-3 h-3 text-warning" strokeWidth={1.5} /> Available
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-card-border border-t-card-border-top bg-card shadow-card-bevel px-3 py-2 text-center">
|
||||
<div className="text-lg font-medium tabular-nums tracking-tight text-stat-value">{updating}</div>
|
||||
<div className="text-[10px] text-stat-subtitle flex items-center justify-center gap-1">
|
||||
<Loader2 className="w-3 h-3 text-brand" strokeWidth={1.5} /> Updating
|
||||
<div className="rounded-lg border border-card-border border-t-card-border-top bg-card shadow-card-bevel px-3 py-2 text-center">
|
||||
<div className="text-lg font-medium tabular-nums tracking-tight text-stat-value">{updating}</div>
|
||||
<div className="text-[10px] text-stat-subtitle flex items-center justify-center gap-1">
|
||||
<Loader2 className="w-3 h-3 text-brand" strokeWidth={1.5} /> Updating
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-card-border border-t-card-border-top bg-card shadow-card-bevel px-3 py-2 text-center">
|
||||
<div className="text-lg font-medium tabular-nums tracking-tight text-stat-value">{failed}</div>
|
||||
<div className="text-[10px] text-stat-subtitle flex items-center justify-center gap-1">
|
||||
<AlertTriangle className="w-3 h-3 text-destructive/70" strokeWidth={1.5} /> Failed
|
||||
<div className="rounded-lg border border-card-border border-t-card-border-top bg-card shadow-card-bevel px-3 py-2 text-center">
|
||||
<div className="text-lg font-medium tabular-nums tracking-tight text-stat-value">{failed}</div>
|
||||
<div className="text-[10px] text-stat-subtitle flex items-center justify-center gap-1">
|
||||
<AlertTriangle className="w-3 h-3 text-destructive/70" strokeWidth={1.5} /> Failed
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search + gateway version */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 px-6 pb-3 shrink-0">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Filter nodes..."
|
||||
value={modalSearch}
|
||||
onChange={e => setModalSearch(e.target.value)}
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
className="h-8 pl-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
@@ -128,18 +129,20 @@ export function NodeUpdatesModal({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Table header */}
|
||||
<div className="grid grid-cols-[1fr_80px_100px_100px_120px] gap-2 px-3 text-[10px] leading-3 font-mono text-stat-subtitle uppercase tracking-[0.18em]">
|
||||
<span>Node</span>
|
||||
<span>Type</span>
|
||||
<span>Current</span>
|
||||
<span>Latest</span>
|
||||
<span className="text-right">Status</span>
|
||||
{/* Table column header */}
|
||||
<div className="px-6 pb-2 shrink-0">
|
||||
<div className="grid grid-cols-[1fr_80px_100px_100px_120px] gap-2 px-3 text-[10px] leading-3 font-mono text-stat-subtitle uppercase tracking-[0.18em]">
|
||||
<span>Node</span>
|
||||
<span>Type</span>
|
||||
<span>Current</span>
|
||||
<span>Latest</span>
|
||||
<span className="text-right">Status</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Node list */}
|
||||
<ScrollArea className="flex-1 min-h-0 max-h-[40vh] -mx-1 px-1">
|
||||
<div className="space-y-1">
|
||||
{/* Node list — fills remaining height, no cap */}
|
||||
<ScrollArea className="flex-1 min-h-0 px-6">
|
||||
<div className="space-y-1 pb-2">
|
||||
{filtered.map(s => (
|
||||
<div key={s.nodeId} className="grid grid-cols-[1fr_80px_100px_100px_120px] gap-2 items-center rounded-lg border border-card-border border-t-card-border-top bg-card shadow-card-bevel px-3 py-2">
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
@@ -194,14 +197,14 @@ export function NodeUpdatesModal({
|
||||
))}
|
||||
{filtered.length === 0 && (
|
||||
<div className="flex items-center justify-center py-8 text-muted-foreground text-sm">
|
||||
No nodes match “{modalSearch}”
|
||||
No nodes match “{search}”
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between pt-2 border-t border-border/50">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-t border-border/50 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -224,9 +227,9 @@ export function NodeUpdatesModal({
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user