refactor(frontend): migrate FleetSnapshots dialogs to Modal chrome (#950)

Two AlertDialog instances move onto §10 Modal primitives:

- Snapshot delete confirm -> destructive ConfirmModal hoisted to a
  single parent-level instance driven by confirmDeleteId state. Each
  row's trash button now calls setConfirmDeleteId(snapshot.id) instead
  of wrapping its own AlertDialog
- RestoreButton's per-restore confirm -> ConfirmModal with the
  redeploy checkbox in the body slot. Promise-aware onConfirm closes
  the dialog from finally
This commit is contained in:
Anso
2026-05-06 20:18:33 -04:00
committed by GitHub
parent 0442bd29b3
commit fe06838bf7
+71 -82
View File
@@ -13,11 +13,7 @@ import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { import { ConfirmModal } from '@/components/ui/modal';
AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { apiFetch } from '@/lib/api'; import { apiFetch } from '@/lib/api';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
@@ -84,6 +80,7 @@ export default function FleetSnapshots() {
const [previewFiles, setPreviewFiles] = useState<Set<string>>(new Set()); const [previewFiles, setPreviewFiles] = useState<Set<string>>(new Set());
const [restoringStack, setRestoringStack] = useState<string | null>(null); const [restoringStack, setRestoringStack] = useState<string | null>(null);
const [deletingId, setDeletingId] = useState<number | null>(null); const [deletingId, setDeletingId] = useState<number | null>(null);
const [confirmDeleteId, setConfirmDeleteId] = useState<number | null>(null);
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [cloudSnapshotIds, setCloudSnapshotIds] = useState<Set<number>>(new Set()); const [cloudSnapshotIds, setCloudSnapshotIds] = useState<Set<number>>(new Set());
const [uploadingId, setUploadingId] = useState<number | null>(null); const [uploadingId, setUploadingId] = useState<number | null>(null);
@@ -631,39 +628,19 @@ export default function FleetSnapshots() {
</Button> </Button>
)} )}
{isAdmin && ( {isAdmin && (
<AlertDialog> <Button
<AlertDialogTrigger asChild> variant="ghost"
<Button size="sm"
variant="ghost" className="h-7 px-2 text-xs text-destructive/60 hover:bg-destructive hover:text-destructive-foreground"
size="sm" disabled={deletingId === snapshot.id}
className="h-7 px-2 text-xs text-destructive/60 hover:bg-destructive hover:text-destructive-foreground" onClick={() => setConfirmDeleteId(snapshot.id)}
disabled={deletingId === snapshot.id} >
> {deletingId === snapshot.id ? (
{deletingId === snapshot.id ? ( <Loader2 className="w-3.5 h-3.5 animate-spin" />
<Loader2 className="w-3.5 h-3.5 animate-spin" /> ) : (
) : ( <Trash2 className="w-3.5 h-3.5" strokeWidth={1.5} />
<Trash2 className="w-3.5 h-3.5" strokeWidth={1.5} /> )}
)} </Button>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete snapshot?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete this fleet snapshot. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
onClick={() => handleDelete(snapshot.id)}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)} )}
</div> </div>
</TableCell> </TableCell>
@@ -674,6 +651,26 @@ export default function FleetSnapshots() {
</Table> </Table>
</div> </div>
)} )}
<ConfirmModal
open={confirmDeleteId !== null}
onOpenChange={(open) => { if (!open) setConfirmDeleteId(null); }}
variant="destructive"
kicker="SNAPSHOTS · DELETE · IRREVERSIBLE"
title="Delete snapshot"
confirmLabel="Delete"
onConfirm={async () => {
if (confirmDeleteId !== null) {
const id = confirmDeleteId;
setConfirmDeleteId(null);
await handleDelete(id);
}
}}
>
<p className="text-sm text-stat-subtitle">
Permanently removes this fleet snapshot.
</p>
</ConfirmModal>
</div> </div>
); );
} }
@@ -691,32 +688,40 @@ function RestoreButton({ nodeId, nodeName, stackName, restoring, onRestore }: {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
<AlertDialog open={open} onOpenChange={setOpen}> <>
<AlertDialogTrigger asChild> <Button
<Button variant="outline"
variant="outline" size="sm"
size="sm" className="h-7 px-2.5 text-xs gap-1.5 ml-3 mt-1"
className="h-7 px-2.5 text-xs gap-1.5 ml-3 mt-1" disabled={restoring}
disabled={restoring} onClick={() => setOpen(true)}
> >
{restoring ? ( {restoring ? (
<Loader2 className="w-3 h-3 animate-spin" /> <Loader2 className="w-3 h-3 animate-spin" />
) : ( ) : (
<RotateCcw className="w-3 h-3" strokeWidth={1.5} /> <RotateCcw className="w-3 h-3" strokeWidth={1.5} />
)} )}
Restore Restore
</Button> </Button>
</AlertDialogTrigger> <ConfirmModal
<AlertDialogContent> open={open}
<AlertDialogHeader> onOpenChange={setOpen}
<AlertDialogTitle> kicker="SNAPSHOTS · RESTORE"
Restore {stackName} on {nodeName}? title={`Restore ${stackName} on ${nodeName}`}
</AlertDialogTitle> confirmLabel={restoring ? 'Restoring...' : 'Restore'}
<AlertDialogDescription> confirming={restoring}
This will overwrite the current compose files with the snapshot version. onConfirm={async () => {
</AlertDialogDescription> try {
</AlertDialogHeader> await onRestore(nodeId, stackName, redeploy);
<div className="flex items-center space-x-2 py-2"> } finally {
setOpen(false);
}
}}
>
<p className="text-sm text-stat-subtitle">
Overwrites the current compose files with the snapshot version.
</p>
<div className="flex items-center space-x-2 pt-1">
<Checkbox <Checkbox
id={`redeploy-${nodeId}-${stackName}`} id={`redeploy-${nodeId}-${stackName}`}
checked={redeploy} checked={redeploy}
@@ -729,23 +734,7 @@ function RestoreButton({ nodeId, nodeName, stackName, restoring, onRestore }: {
Redeploy stack after restore Redeploy stack after restore
</Label> </Label>
</div> </div>
<AlertDialogFooter> </ConfirmModal>
<AlertDialogCancel>Cancel</AlertDialogCancel> </>
<Button
disabled={restoring}
onClick={async () => {
try {
await onRestore(nodeId, stackName, redeploy);
} finally {
setOpen(false);
}
}}
>
{restoring && <Loader2 className="w-3.5 h-3.5 animate-spin mr-1.5" />}
Restore
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
); );
} }