feat(web): typed confirmation for deleting an SSH key
The last of the inline two-step deletes, and the one with the most reach: a key delete revokes it from every server at once, and for a generated key the stored private half goes with it. That copy is the only one Vantage holds, so unlike an uploaded key this cannot be undone by pasting the public half back. Body names how many servers lose access, and says the private key is destroyed only when there is one to destroy. The delete error moves out of the toast and into the dialog, which stays open on failure, matching the server and monitor deletes.
This commit is contained in:
@@ -5,7 +5,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { api, Server } from "@/lib/api";
|
||||
import { Badge, Button, Card, CardHeader, CardTitle, useToast } from "@/components/ui";
|
||||
import { Badge, Button, Card, CardHeader, CardTitle, ConfirmDialog, friendlyMessage, useToast } from "@/components/ui";
|
||||
import { Table, Thead, Tbody, Tr, Th, Td } from "@/components/ui";
|
||||
|
||||
function AssignModal({
|
||||
@@ -211,14 +211,19 @@ export default function KeyDetailPage() {
|
||||
onError: toast.error,
|
||||
});
|
||||
|
||||
const { mutate: deleteKey, isPending: isDeleting } = useMutation({
|
||||
const {
|
||||
mutate: deleteKey,
|
||||
isPending: isDeleting,
|
||||
error: deleteError,
|
||||
} = useMutation({
|
||||
mutationFn: () => api.deleteKey(keyId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["keys"] });
|
||||
toast.success(`Deleted ${key?.label ?? "the key"}.`);
|
||||
router.push("/keys");
|
||||
},
|
||||
onError: toast.error,
|
||||
// Shown in the dialog rather than toasted: it stays open on failure, and
|
||||
// the error belongs where the operator is still looking.
|
||||
});
|
||||
|
||||
const handleCopyKey = async () => {
|
||||
@@ -281,24 +286,53 @@ export default function KeyDetailPage() {
|
||||
</svg>
|
||||
Assign to Server
|
||||
</Button>
|
||||
{!confirmDelete ? (
|
||||
<Button variant="danger" onClick={() => setConfirmDelete(true)}>
|
||||
Delete Key
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-danger">Delete permanently?</span>
|
||||
<Button variant="danger" loading={isDeleting} onClick={() => deleteKey()}>
|
||||
Confirm
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={() => setConfirmDelete(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Button variant="danger" onClick={() => setConfirmDelete(true)}>
|
||||
Delete Key
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/*
|
||||
* Typed, like the server and monitor deletes. Deleting a key revokes it
|
||||
* from every server at once, and for a generated key the stored private
|
||||
* half goes with it — there is no copy anywhere else, so this is the one
|
||||
* delete on the fleet side that cannot be undone by re-uploading what
|
||||
* the operator already has.
|
||||
*/}
|
||||
<ConfirmDialog
|
||||
open={confirmDelete}
|
||||
title="Delete SSH key"
|
||||
confirmLabel="Delete key"
|
||||
requireTyped={key.label}
|
||||
loading={isDeleting}
|
||||
error={deleteError ? friendlyMessage(deleteError) : null}
|
||||
onClose={() => setConfirmDelete(false)}
|
||||
onConfirm={() => deleteKey()}
|
||||
body={
|
||||
<>
|
||||
<p>
|
||||
<span className="font-mono text-text-primary">{key.label}</span> is deleted
|
||||
{activeAssignments.length > 0 && (
|
||||
<>
|
||||
{" "}
|
||||
and revoked from{" "}
|
||||
<span className="text-text-primary">
|
||||
{activeAssignments.length} server{activeAssignments.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
. Agents rewrite authorized_keys within 30 seconds.
|
||||
</p>
|
||||
{key.has_private_key && (
|
||||
<p>
|
||||
The stored private key is destroyed with it. If this is the only copy, access it grants is gone for
|
||||
good.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<div className="space-y-6 lg:col-span-1">
|
||||
<Card>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user