refactor(frontend): migrate PolicyBlockDialog, StateReviewDialog, EvictionDialog to Modal chrome (D-6) (#910)

* refactor(frontend): migrate PolicyBlockDialog to Modal chrome (D-6)

* refactor(frontend): migrate StateReviewDialog to Modal chrome (D-6)

* refactor(frontend): migrate EvictionDialog to Modal chrome (D-6)
This commit is contained in:
Anso
2026-05-04 13:16:02 -04:00
committed by GitHub
parent 46b3d53274
commit 993ab98f31
3 changed files with 94 additions and 91 deletions
@@ -1,8 +1,6 @@
import { useState } from 'react';
import { AlertTriangle, Camera } from 'lucide-react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
} from '@/components/ui/dialog';
import { Modal, ModalDestructiveHeader, ModalBody, ModalFooter } from '@/components/ui/modal';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -27,32 +25,35 @@ export function EvictionDialog({
}
return (
<Dialog open={open} onOpenChange={(o) => { if (!o) reset(); onOpenChange(o); }}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<div className="flex items-center gap-2 text-brand font-mono text-[10px] uppercase tracking-[0.2em]">
<span className="inline-block w-1 h-3 bg-brand" />
Withdraw deployment
</div>
<DialogTitle className="font-serif italic text-xl tracking-[-0.01em]">
Stop {blueprintName} on {nodeName}
</DialogTitle>
<DialogDescription>
{isStateful
? 'This blueprint is stateful. Choose how to handle its data on this node.'
: 'Sencho will run docker compose down and remove the blueprint directory on this node.'}
</DialogDescription>
</DialogHeader>
<Modal
open={open}
onOpenChange={(o) => { if (!o) reset(); onOpenChange(o); }}
size="lg"
>
<ModalDestructiveHeader
kicker={`${blueprintName.toUpperCase()} · EVICT`}
title={`Stop ${blueprintName} on ${nodeName}`}
description={
isStateful
? 'This blueprint is stateful. Choose how to handle its data on this node.'
: 'Sencho will run docker compose down and remove the blueprint directory on this node.'
}
/>
<ModalBody>
<p className="text-sm text-muted-foreground">
{isStateful
? 'This blueprint is stateful. Choose how to handle its data on this node.'
: 'Sencho will run docker compose down and remove the blueprint directory on this node.'}
</p>
{isStateful && (
<div className="space-y-3">
<div className="rounded-lg border border-warning/30 bg-warning/5 p-3 flex gap-2">
<AlertTriangle className="w-4 h-4 text-warning shrink-0 mt-0.5" strokeWidth={1.5} />
<p className="text-xs text-stat-subtitle leading-relaxed">
Named volumes or bind mounts were detected. Evicting destroys the named volumes managed by this stack on <span className="font-mono">{nodeName}</span>. Bind mounts on the host filesystem are left in place.
Named volumes or bind mounts were detected. Evicting destroys the named volumes managed by this stack on{' '}
<span className="font-mono">{nodeName}</span>. Bind mounts on the host filesystem are left in place.
</p>
</div>
<button
type="button"
onClick={() => onConfirm('snapshot_then_evict')}
@@ -67,7 +68,6 @@ export function EvictionDialog({
Captures the compose definition into the existing fleet-snapshot store, then runs the eviction. Note: volume bytes are not shipped; that ships in a future Volume Migration feature.
</p>
</button>
<div className="space-y-2 rounded-lg border border-destructive/30 bg-destructive/5 p-3">
<div className="font-mono text-[10px] uppercase tracking-[0.2em] text-destructive">
Evict and destroy data
@@ -85,14 +85,18 @@ export function EvictionDialog({
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={busy}>
</ModalBody>
<ModalFooter
secondary={
<Button variant="outline" size="sm" onClick={() => onOpenChange(false)} disabled={busy}>
Cancel
</Button>
{isStateful ? (
}
primary={
isStateful ? (
<Button
variant="outline"
size="sm"
className="text-destructive border-destructive/40 hover:bg-destructive/10"
disabled={destructiveDisabled || busy}
onClick={() => onConfirm('evict_and_destroy')}
@@ -100,12 +104,16 @@ export function EvictionDialog({
Evict and destroy data
</Button>
) : (
<Button onClick={() => onConfirm('standard')} disabled={busy}>
<Button
size="sm"
onClick={() => onConfirm('standard')}
disabled={busy}
>
Withdraw deployment
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
)
}
/>
</Modal>
);
}
@@ -1,7 +1,5 @@
import { Database, Play } from 'lucide-react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
} from '@/components/ui/dialog';
import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/ui/modal';
import { Button } from '@/components/ui/button';
interface StateReviewDialogProps {
@@ -17,21 +15,17 @@ export function StateReviewDialog({
open, onOpenChange, blueprintName, nodeName, busy, onAccept,
}: StateReviewDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<div className="flex items-center gap-2 text-brand font-mono text-[10px] uppercase tracking-[0.2em]">
<span className="inline-block w-1 h-3 bg-brand" />
Confirm first deploy
</div>
<DialogTitle className="font-serif italic text-xl tracking-[-0.01em]">
Deploy {blueprintName} to {nodeName}?
</DialogTitle>
<DialogDescription>
This blueprint is stateful and has never run on this node. Sencho will create empty volumes unless you choose to restore from a prior snapshot.
</DialogDescription>
</DialogHeader>
<Modal open={open} onOpenChange={onOpenChange} size="lg">
<ModalHeader
kicker={`${blueprintName.toUpperCase()} · DEPLOY`}
title={`Deploy ${blueprintName} to ${nodeName}?`}
description="This blueprint is stateful and has never run on this node. Sencho will create empty volumes unless you choose to restore from a prior snapshot."
/>
<ModalBody>
<p className="text-sm text-muted-foreground">
This blueprint is stateful and has never run on this node. Sencho will create
empty volumes unless you choose to restore from a prior snapshot.
</p>
<div className="space-y-2">
<button
type="button"
@@ -47,7 +41,6 @@ export function StateReviewDialog({
Create empty named volumes on this node. The container starts with whatever default state its image carries.
</p>
</button>
<button
type="button"
disabled
@@ -62,13 +55,14 @@ export function StateReviewDialog({
</p>
</button>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={busy}>
</ModalBody>
<ModalFooter
primary={
<Button variant="outline" size="sm" onClick={() => onOpenChange(false)} disabled={busy}>
Cancel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
}
/>
</Modal>
);
}
@@ -1,14 +1,9 @@
import { ShieldAlert } from 'lucide-react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
Modal,
ModalDestructiveHeader,
ModalBody,
ModalFooter,
} from '@/components/ui/modal';
import { Button } from '@/components/ui/button';
import { SeverityChip } from '@/components/VulnerabilityScanSheet';
import type { VulnSeverity } from '@/types/security';
@@ -58,21 +53,19 @@ export function PolicyBlockDialog({
const violations = payload?.violations ?? [];
return (
<AlertDialog open={open} onOpenChange={(next) => { if (!next) onClose(); }}>
<AlertDialogContent className="sm:max-w-xl">
<AlertDialogHeader>
<div className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle flex items-center gap-2">
<ShieldAlert className="h-3.5 w-3.5 text-destructive" aria-hidden />
Policy block · {stackName}
</div>
<AlertDialogTitle>Deploy blocked by security policy</AlertDialogTitle>
<AlertDialogDescription>
Policy <span className="font-medium text-foreground">{policyName}</span> blocks deploys
when any image meets or exceeds <span className="font-medium text-foreground">{maxSeverity}</span>.
The following {violations.length === 1 ? 'image' : `${violations.length} images`} triggered the block.
</AlertDialogDescription>
</AlertDialogHeader>
<Modal open={open} onOpenChange={(next) => { if (!next) onClose(); }} size="xl">
<ModalDestructiveHeader
kicker={`${stackName.toUpperCase()} · SCAN POLICY · BLOCKED`}
title="Deploy blocked by security policy"
description={`Policy ${policyName} blocks deploys when any image meets or exceeds ${maxSeverity}.`}
/>
<ModalBody>
<p className="text-sm text-muted-foreground">
Policy <span className="font-medium text-foreground">{policyName}</span> blocks deploys
when any image meets or exceeds{' '}
<span className="font-medium text-foreground">{maxSeverity}</span>.{' '}
The following {violations.length === 1 ? 'image' : `${violations.length} images`} triggered the block.
</p>
<div className="border border-glass-border bg-card/60 shadow-card-bevel divide-y divide-glass-border">
{violations.length === 0 ? (
<div className="px-4 py-3 text-sm text-muted-foreground">
@@ -92,12 +85,18 @@ export function PolicyBlockDialog({
))
)}
</div>
<AlertDialogFooter>
<AlertDialogCancel onClick={onClose}>Close</AlertDialogCancel>
{canBypass ? (
</ModalBody>
<ModalFooter
secondary={
<Button variant="outline" size="sm" onClick={onClose}>
Close
</Button>
}
primary={
canBypass ? (
<Button
variant="destructive"
size="sm"
disabled={bypassing}
onClick={(e) => {
e.preventDefault();
@@ -107,10 +106,12 @@ export function PolicyBlockDialog({
{bypassing ? 'Deploying…' : 'Deploy anyway'}
</Button>
) : (
<AlertDialogAction disabled>Admin required to bypass</AlertDialogAction>
)}
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Button variant="outline" size="sm" disabled>
Admin required to bypass
</Button>
)
}
/>
</Modal>
);
}