mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-10 10:49:35 +00:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user