redesign certificates page: tabs, drawers, relative expiry, status bar
- Split ACME / Imported / CA-mTLS into tabs with count badges - Add clickable status summary bar (expired / expiring soon / healthy) - Per-tab search filter by name and domain - Replace accordion cards with DataTable for imported certs - Slide-in Drawers (480 px) for add/edit imported and CA certs - File upload + show/hide toggle for private key in ImportCertDrawer - CaCertDrawer: Generate / Import PEM tabs for add, simple form for edit - CA tab: expandable rows showing issued client certs inline - RelativeTime component: "in 45 days" / "EXPIRED 3 days ago" with date tooltip - Remove CreateCaCertDialog and EditCaCertDialog (replaced by CaCertDrawer) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,8 +16,6 @@ import {
|
||||
InputAdornment,
|
||||
Stack,
|
||||
Switch,
|
||||
Tab,
|
||||
Tabs,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
@@ -27,12 +25,9 @@ import { useEffect, useRef, useState, useTransition } from "react";
|
||||
import type { CaCertificate } from "@/src/lib/models/ca-certificates";
|
||||
import type { IssuedClientCertificate } from "@/src/lib/models/issued-client-certificates";
|
||||
import {
|
||||
createCaCertificateAction,
|
||||
deleteCaCertificateAction,
|
||||
generateCaCertificateAction,
|
||||
issueClientCertificateAction,
|
||||
revokeIssuedClientCertificateAction,
|
||||
updateCaCertificateAction,
|
||||
} from "@/app/(dashboard)/certificates/ca-actions";
|
||||
|
||||
function downloadFile(filename: string, blob: Blob) {
|
||||
@@ -67,184 +62,6 @@ function formatFingerprint(value: string): string {
|
||||
return value.match(/.{1,2}/g)?.join(":") ?? value;
|
||||
}
|
||||
|
||||
export function CreateCaCertDialog({
|
||||
open,
|
||||
onClose,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [tab, setTab] = useState<"import" | "generate">("generate");
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const importFormRef = useRef<HTMLFormElement>(null);
|
||||
const generateFormRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
function handleClose() {
|
||||
setTab("generate");
|
||||
onClose();
|
||||
}
|
||||
|
||||
function handleImport(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(importFormRef.current!);
|
||||
startTransition(async () => {
|
||||
await createCaCertificateAction(formData);
|
||||
handleClose();
|
||||
});
|
||||
}
|
||||
|
||||
function handleGenerate(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(generateFormRef.current!);
|
||||
startTransition(async () => {
|
||||
await generateCaCertificateAction(formData);
|
||||
handleClose();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Add Client CA Certificate</DialogTitle>
|
||||
<DialogContent>
|
||||
<>
|
||||
<Tabs value={tab} onChange={(_, v) => setTab(v)} sx={{ mb: 2 }}>
|
||||
<Tab value="generate" label="Generate" />
|
||||
<Tab value="import" label="Import PEM" />
|
||||
</Tabs>
|
||||
|
||||
{tab === "generate" && (
|
||||
<form ref={generateFormRef} onSubmit={handleGenerate}>
|
||||
<Stack spacing={2}>
|
||||
<TextField
|
||||
name="name"
|
||||
label="Name"
|
||||
required
|
||||
fullWidth
|
||||
autoFocus
|
||||
placeholder="My Client CA"
|
||||
helperText="Display name in this UI"
|
||||
/>
|
||||
<TextField
|
||||
name="common_name"
|
||||
label="Common Name (CN)"
|
||||
fullWidth
|
||||
placeholder="My Client CA"
|
||||
helperText="CN field in the certificate. Defaults to the name above if left blank."
|
||||
/>
|
||||
<TextField
|
||||
name="validity_days"
|
||||
label="Validity"
|
||||
type="number"
|
||||
fullWidth
|
||||
defaultValue={3650}
|
||||
inputProps={{ min: 1, max: 3650 }}
|
||||
InputProps={{ endAdornment: <InputAdornment position="end">days</InputAdornment> }}
|
||||
/>
|
||||
<DialogActions sx={{ px: 0, pb: 0 }}>
|
||||
<Button onClick={handleClose} disabled={isPending}>Cancel</Button>
|
||||
<Button type="submit" variant="contained" disabled={isPending}>
|
||||
{isPending ? "Generating..." : "Generate CA Certificate"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Stack>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{tab === "import" && (
|
||||
<form ref={importFormRef} onSubmit={handleImport}>
|
||||
<Stack spacing={2}>
|
||||
<TextField
|
||||
name="name"
|
||||
label="Name"
|
||||
required
|
||||
fullWidth
|
||||
autoFocus
|
||||
placeholder="My Client CA"
|
||||
/>
|
||||
<TextField
|
||||
name="certificate_pem"
|
||||
label="Certificate PEM"
|
||||
required
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={6}
|
||||
placeholder="-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----"
|
||||
inputProps={{ style: { fontFamily: "monospace", fontSize: "0.8rem" } }}
|
||||
helperText="PEM-encoded X.509 CA certificate (no private key needed)"
|
||||
/>
|
||||
<DialogActions sx={{ px: 0, pb: 0 }}>
|
||||
<Button onClick={handleClose} disabled={isPending}>Cancel</Button>
|
||||
<Button type="submit" variant="contained" disabled={isPending}>
|
||||
{isPending ? "Adding..." : "Add CA Certificate"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Stack>
|
||||
</form>
|
||||
)}
|
||||
</>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export function EditCaCertDialog({
|
||||
open,
|
||||
cert,
|
||||
onClose,
|
||||
}: {
|
||||
open: boolean;
|
||||
cert: CaCertificate;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(formRef.current!);
|
||||
startTransition(async () => {
|
||||
await updateCaCertificateAction(cert.id, formData);
|
||||
onClose();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Edit CA Certificate</DialogTitle>
|
||||
<form ref={formRef} onSubmit={handleSubmit}>
|
||||
<DialogContent>
|
||||
<Stack spacing={2} mt={1}>
|
||||
<TextField
|
||||
name="name"
|
||||
label="Name"
|
||||
required
|
||||
fullWidth
|
||||
defaultValue={cert.name}
|
||||
/>
|
||||
<TextField
|
||||
name="certificate_pem"
|
||||
label="Certificate PEM"
|
||||
required
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={6}
|
||||
defaultValue={cert.certificate_pem}
|
||||
inputProps={{ style: { fontFamily: "monospace", fontSize: "0.8rem" } }}
|
||||
helperText="PEM-encoded X.509 CA certificate"
|
||||
/>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} disabled={isPending}>Cancel</Button>
|
||||
<Button type="submit" variant="contained" disabled={isPending}>
|
||||
{isPending ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export function IssueClientCertDialog({
|
||||
open,
|
||||
cert,
|
||||
|
||||
Reference in New Issue
Block a user