From f0d03bc58f65d1b7f95897060c34c840f6025cb7 Mon Sep 17 00:00:00 2001 From: Anso Date: Wed, 6 May 2026 19:15:33 -0400 Subject: [PATCH] refactor(frontend): migrate ResourcesView dialogs to Modal chrome (#945) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace raw Dialog/AlertDialog usage in ResourcesView with the §10 Modal primitives: - Prune confirms (managed and all scopes) -> destructive ConfirmModal with single shared kicker and scope-conditional title/hint/label - Delete confirms (image, network, volume) -> destructive ConfirmModal - Bulk purge of unmanaged containers -> destructive ConfirmModal - Create network form -> Modal + ModalHeader + ModalBody + ModalFooter Also enforce single-line kickers in the modal primitive by adding whitespace-nowrap to the header kicker, so the rune at the top never wraps even at sm width. Tighten handlePurgeOrphans to close the dialog in finally rather than only on success, since the new ConfirmModal Promise-aware behaviour keeps the dialog open until state closes it. --- frontend/src/components/ResourcesView.tsx | 277 +++++++++++----------- frontend/src/components/ui/modal.tsx | 2 +- 2 files changed, 138 insertions(+), 141 deletions(-) diff --git a/frontend/src/components/ResourcesView.tsx b/frontend/src/components/ResourcesView.tsx index 74e8be40..464cc15c 100644 --- a/frontend/src/components/ResourcesView.tsx +++ b/frontend/src/components/ResourcesView.tsx @@ -6,8 +6,7 @@ import { springs } from '@/lib/motion'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; -import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; +import { Modal, ModalHeader, ModalBody, ModalFooter, ConfirmModal } from "@/components/ui/modal"; import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Combobox } from "@/components/ui/combobox"; @@ -515,7 +514,6 @@ export default function ResourcesView() { throw new Error(data?.error || 'Failed to purge selected containers'); } toast.success(`Purged ${selectedOrphans.length} unmanaged container(s)`); - setBulkPurgeConfirm(false); await fetchAllData(); } catch (error) { const err = error as Record; @@ -523,6 +521,7 @@ export default function ResourcesView() { } finally { toast.dismiss(loadingId); setIsActioning(false); + setBulkPurgeConfirm(false); } }; @@ -1208,158 +1207,156 @@ export default function ResourcesView() { {/* ── Dialogs ── */} {/* Prune Confirm */} - !open && setConfirmPrune(null)}> - - - {confirmPrune?.scope === 'all' ? ( - <> - - - Prune All Docker {confirmPrune?.target} - - - This will prune all unused {confirmPrune?.target} from the Docker daemon - - including those from external projects not managed by Sencho. This cannot be undone. - - - ) : ( - <> - Prune Sencho-Managed {confirmPrune?.target} - - Only unused {confirmPrune?.target} belonging to your Sencho stacks will be removed. - External Docker resources are not affected. - - - )} - - - Cancel - - {isActioning ? 'Pruning...' : confirmPrune?.scope === 'all' ? 'Prune All' : 'Prune'} - - - - + !open && setConfirmPrune(null)} + variant="destructive" + kicker="RESOURCES · PRUNE · IRREVERSIBLE" + title={ + confirmPrune?.scope === 'all' + ? `Prune all Docker ${confirmPrune?.target}` + : `Prune Sencho-managed ${confirmPrune?.target}` + } + hint={confirmPrune?.scope === 'all' ? 'AFFECTS external Docker resources' : 'KEEPS external resources'} + confirmLabel={isActioning ? 'Pruning...' : (confirmPrune?.scope === 'all' ? 'Prune all' : 'Prune')} + confirming={isActioning} + onConfirm={handlePrune} + > +

+ {confirmPrune?.scope === 'all' ? ( + <> + Prunes all unused {confirmPrune?.target} from the Docker daemon, including those from{' '} + external projects not managed by Sencho. + + ) : ( + <> + Removes only unused {confirmPrune?.target} belonging to your Sencho stacks. External Docker resources are{' '} + not affected. + + )} +

+
{/* Delete Confirm */} - !open && setConfirmDelete(null)}> - - - Delete {confirmDelete?.type.slice(0, -1)} - - Permanently delete {confirmDelete?.name || confirmDelete?.id.substring(0, 12)}? This cannot be undone. - - - - Cancel - - {isActioning ? 'Deleting...' : 'Delete'} - - - - + !open && setConfirmDelete(null)} + variant="destructive" + kicker="RESOURCES · DELETE · IRREVERSIBLE" + title={`Delete ${confirmDelete?.type.slice(0, -1) ?? ''}`} + confirmLabel={isActioning ? 'Deleting...' : 'Delete'} + confirming={isActioning} + onConfirm={handleDelete} + > +

+ Permanently deletes{' '} + + {confirmDelete?.name || confirmDelete?.id.substring(0, 12)} + . +

+
{/* Unmanaged Container Purge Confirm */} - - - - Purge Selected Unmanaged Containers - - Permanently remove {selectedOrphans.length} container{selectedOrphans.length !== 1 ? 's' : ''} from external projects? - This will force-stop and remove them. This cannot be undone. - - - - Cancel - - {isActioning ? 'Purging...' : 'Purge'} - - - - + +

+ Force-stops and removes {selectedOrphans.length} container{selectedOrphans.length !== 1 ? 's' : ''} from external projects not managed by Sencho. +

+
- {/* Create Network Dialog */} - - - - Create Network - Create a new Docker network for inter-container communication. - -
+ {/* Create Network Modal */} + + + +
+ + setCreateNetworkForm(f => ({ ...f, name: e.target.value }))} + /> +
+
+ + ({ value: d, label: d }))} + value={createNetworkForm.driver} + onValueChange={v => setCreateNetworkForm(f => ({ ...f, driver: (v || 'bridge') as NetworkDriver }))} + placeholder="Select driver..." + searchPlaceholder="Search drivers..." + emptyText="No matching driver." + /> +
+
- + setCreateNetworkForm(f => ({ ...f, name: e.target.value }))} + value={createNetworkForm.subnet} + onChange={e => setCreateNetworkForm(f => ({ ...f, subnet: e.target.value }))} />
- - ({ value: d, label: d }))} - value={createNetworkForm.driver} - onValueChange={v => setCreateNetworkForm(f => ({ ...f, driver: (v || 'bridge') as NetworkDriver }))} - placeholder="Select driver..." - searchPlaceholder="Search drivers..." - emptyText="No matching driver." + + setCreateNetworkForm(f => ({ ...f, gateway: e.target.value }))} />
-
-
- - setCreateNetworkForm(f => ({ ...f, subnet: e.target.value }))} - /> -
-
- - setCreateNetworkForm(f => ({ ...f, gateway: e.target.value }))} - /> -
-
-
-
- setCreateNetworkForm(f => ({ ...f, internal: v }))} - /> - -
-
- setCreateNetworkForm(f => ({ ...f, attachable: v }))} - /> - -
-
- - - - - -
+ } + primary={ + + } + /> + {/* Image Details Sheet */} setInspectImageId(null)} /> diff --git a/frontend/src/components/ui/modal.tsx b/frontend/src/components/ui/modal.tsx index 566b791c..8928184d 100644 --- a/frontend/src/components/ui/modal.tsx +++ b/frontend/src/components/ui/modal.tsx @@ -83,7 +83,7 @@ function HeaderShell({ return (
-
+
{kicker}