mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-05 08:27:42 +00:00
refactor(frontend): migrate Labels and Suppressions dialogs to Modal chrome (#947)
Bring the two settings panels onto §10 Modal primitives. LabelsSection: - Create/edit form -> Modal at sm with kicker LABELS · NEW or LABELS · EDIT - Delete confirm -> destructive ConfirmModal with kicker LABELS · DELETE · IRREVERSIBLE - handleDelete now closes from finally so the dialog clears on errors too (the new ConfirmModal Promise-aware behaviour keeps it open until state closes it) SuppressionsPanel: - New suppression form -> Modal at md with kicker SUPPRESSIONS · NEW - Remove confirm -> destructive ConfirmModal with kicker SUPPRESSIONS · REMOVE · IRREVERSIBLE - handleDelete already closed from finally; no change needed there
This commit is contained in:
@@ -2,23 +2,7 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import { Plus, Pencil, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from '@/components/ui/dialog';
|
||||
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, ConfirmModal } from '@/components/ui/modal';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { toast } from '@/components/ui/toast-store';
|
||||
import { SENCHO_LABELS_CHANGED } from '@/lib/events';
|
||||
@@ -132,12 +116,13 @@ export function LabelsSection({ onLabelsChanged }: LabelsSectionProps = {}) {
|
||||
throw new Error(data?.error || 'Failed to delete label.');
|
||||
}
|
||||
toast.success('Label deleted.');
|
||||
setDeleteTarget(null);
|
||||
fetchLabels();
|
||||
onLabelsChanged?.();
|
||||
window.dispatchEvent(new Event(SENCHO_LABELS_CHANGED));
|
||||
} catch (err: unknown) {
|
||||
toast.error((err as Error)?.message || 'Something went wrong.');
|
||||
} finally {
|
||||
setDeleteTarget(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -193,65 +178,66 @@ export function LabelsSection({ onLabelsChanged }: LabelsSectionProps = {}) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Create / Edit Dialog */}
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[380px]">
|
||||
<DialogTitle>{editingLabel ? 'Edit Label' : 'Create Label'}</DialogTitle>
|
||||
<VisuallyHidden><DialogDescription>Manage label properties</DialogDescription></VisuallyHidden>
|
||||
<div className="space-y-4 mt-2">
|
||||
<Input
|
||||
placeholder="Label name"
|
||||
value={formName}
|
||||
onChange={e => setFormName(e.target.value)}
|
||||
className="font-mono"
|
||||
maxLength={30}
|
||||
autoFocus
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleSave(); }}
|
||||
/>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-2">Color</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{LABEL_COLORS.map(c => (
|
||||
<button
|
||||
key={c}
|
||||
type="button"
|
||||
className={`w-7 h-7 rounded-full border-2 transition-colors ${c === formColor ? 'border-foreground scale-110' : 'border-transparent hover:border-muted-foreground/30'}`}
|
||||
style={{ backgroundColor: `var(--label-${c})` }}
|
||||
onClick={() => setFormColor(c)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="ghost" onClick={() => setDialogOpen(false)}>Cancel</Button>
|
||||
<Button onClick={handleSave} disabled={saving || !formName.trim()}>
|
||||
{saving ? 'Saving...' : editingLabel ? 'Save' : 'Create'}
|
||||
</Button>
|
||||
{/* Create / Edit Modal */}
|
||||
<Modal open={dialogOpen} onOpenChange={setDialogOpen} size="sm">
|
||||
<ModalHeader
|
||||
kicker={editingLabel ? 'LABELS · EDIT' : 'LABELS · NEW'}
|
||||
title={editingLabel ? 'Edit label' : 'Create label'}
|
||||
description="Manage label properties"
|
||||
/>
|
||||
<ModalBody>
|
||||
<Input
|
||||
placeholder="Label name"
|
||||
value={formName}
|
||||
onChange={e => setFormName(e.target.value)}
|
||||
className="font-mono"
|
||||
maxLength={30}
|
||||
autoFocus
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleSave(); }}
|
||||
/>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-2">Color</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{LABEL_COLORS.map(c => (
|
||||
<button
|
||||
key={c}
|
||||
type="button"
|
||||
className={`w-7 h-7 rounded-full border-2 transition-colors ${c === formColor ? 'border-foreground scale-110' : 'border-transparent hover:border-muted-foreground/30'}`}
|
||||
style={{ backgroundColor: `var(--label-${c})` }}
|
||||
onClick={() => setFormColor(c)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</ModalBody>
|
||||
<ModalFooter
|
||||
secondary={
|
||||
<Button variant="outline" size="sm" onClick={() => setDialogOpen(false)} disabled={saving}>
|
||||
Cancel
|
||||
</Button>
|
||||
}
|
||||
primary={
|
||||
<Button size="sm" onClick={handleSave} disabled={saving || !formName.trim()}>
|
||||
{saving ? 'Saving...' : editingLabel ? 'Save' : 'Create'}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
{/* Delete Confirmation */}
|
||||
<AlertDialog open={!!deleteTarget} onOpenChange={open => !open && setDeleteTarget(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete label “{deleteTarget?.name}”?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will remove the label from all stacks. This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ConfirmModal
|
||||
open={!!deleteTarget}
|
||||
onOpenChange={open => !open && setDeleteTarget(null)}
|
||||
variant="destructive"
|
||||
kicker="LABELS · DELETE · IRREVERSIBLE"
|
||||
title={`Delete label "${deleteTarget?.name ?? ''}"`}
|
||||
confirmLabel="Delete"
|
||||
onConfirm={handleDelete}
|
||||
>
|
||||
<p className="text-sm text-stat-subtitle">
|
||||
Removes the label from every stack across the fleet.
|
||||
</p>
|
||||
</ConfirmModal>
|
||||
</CapabilityGate>
|
||||
</PaidGate>
|
||||
);
|
||||
|
||||
@@ -5,24 +5,7 @@ import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, ConfirmModal } from '@/components/ui/modal';
|
||||
import { ChevronLeft, ChevronRight, Plus, ShieldOff, Trash2 } from 'lucide-react';
|
||||
import { toast } from '@/components/ui/toast-store';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
@@ -273,94 +256,89 @@ export function SuppressionsPanel({ isReplica }: SuppressionsPanelProps) {
|
||||
</ScrollArea>
|
||||
)}
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>New Suppression</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Accept a CVE as known-benign so it stops triggering alerts across the fleet.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s-cve">CVE or advisory ID</Label>
|
||||
<Input
|
||||
id="s-cve"
|
||||
placeholder="CVE-2024-12345 or GHSA-xxxx-xxxx-xxxx"
|
||||
value={form.cveId}
|
||||
onChange={(e) => setForm({ ...form, cveId: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s-pkg">Package (optional)</Label>
|
||||
<Input
|
||||
id="s-pkg"
|
||||
placeholder="e.g. openssl (leave blank to match every package)"
|
||||
value={form.pkgName}
|
||||
onChange={(e) => setForm({ ...form, pkgName: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s-image">Image pattern (optional)</Label>
|
||||
<Input
|
||||
id="s-image"
|
||||
placeholder="e.g. registry.internal/* (leave blank for all images)"
|
||||
value={form.imagePattern}
|
||||
onChange={(e) => setForm({ ...form, imagePattern: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s-reason">Reason</Label>
|
||||
<textarea
|
||||
id="s-reason"
|
||||
className="flex min-h-[72px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
placeholder="Why is this CVE safe to accept?"
|
||||
value={form.reason}
|
||||
onChange={(e) => setForm({ ...form, reason: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s-expiry">Expires in (days, optional)</Label>
|
||||
<Input
|
||||
id="s-expiry"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="Leave blank for no expiry"
|
||||
value={form.expiresInDays}
|
||||
onChange={(e) => setForm({ ...form, expiresInDays: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<Modal open={dialogOpen} onOpenChange={setDialogOpen} size="md">
|
||||
<ModalHeader
|
||||
kicker="SUPPRESSIONS · NEW"
|
||||
title="New suppression"
|
||||
description="Accept a CVE as known-benign so it stops triggering alerts across the fleet."
|
||||
/>
|
||||
<ModalBody>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s-cve">CVE or advisory ID</Label>
|
||||
<Input
|
||||
id="s-cve"
|
||||
placeholder="CVE-2024-12345 or GHSA-xxxx-xxxx-xxxx"
|
||||
value={form.cveId}
|
||||
onChange={(e) => setForm({ ...form, cveId: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDialogOpen(false)} disabled={saving}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s-pkg">Package (optional)</Label>
|
||||
<Input
|
||||
id="s-pkg"
|
||||
placeholder="e.g. openssl (leave blank to match every package)"
|
||||
value={form.pkgName}
|
||||
onChange={(e) => setForm({ ...form, pkgName: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s-image">Image pattern (optional)</Label>
|
||||
<Input
|
||||
id="s-image"
|
||||
placeholder="e.g. registry.internal/* (leave blank for all images)"
|
||||
value={form.imagePattern}
|
||||
onChange={(e) => setForm({ ...form, imagePattern: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s-reason">Reason</Label>
|
||||
<textarea
|
||||
id="s-reason"
|
||||
className="flex min-h-[72px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
placeholder="Why is this CVE safe to accept?"
|
||||
value={form.reason}
|
||||
onChange={(e) => setForm({ ...form, reason: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s-expiry">Expires in (days, optional)</Label>
|
||||
<Input
|
||||
id="s-expiry"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="Leave blank for no expiry"
|
||||
value={form.expiresInDays}
|
||||
onChange={(e) => setForm({ ...form, expiresInDays: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter
|
||||
secondary={
|
||||
<Button variant="outline" size="sm" onClick={() => setDialogOpen(false)} disabled={saving}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={saving}>
|
||||
}
|
||||
primary={
|
||||
<Button size="sm" onClick={handleSave} disabled={saving}>
|
||||
{saving ? 'Saving...' : 'Create'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<AlertDialog open={deleteRow !== null} onOpenChange={(open) => !open && setDeleteRow(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Remove suppression?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Future scan results will surface {deleteRow?.cve_id} again wherever it applies.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Remove
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ConfirmModal
|
||||
open={deleteRow !== null}
|
||||
onOpenChange={(open) => !open && setDeleteRow(null)}
|
||||
variant="destructive"
|
||||
kicker="SUPPRESSIONS · REMOVE · IRREVERSIBLE"
|
||||
title="Remove suppression"
|
||||
confirmLabel="Remove"
|
||||
onConfirm={handleDelete}
|
||||
>
|
||||
<p className="text-sm text-stat-subtitle">
|
||||
Future scan results will surface <span className="font-mono font-medium text-stat-value">{deleteRow?.cve_id}</span> again wherever it applies.
|
||||
</p>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user