Files
caddy-proxy-manager/app/(dashboard)/certificates/components/ImportCertDrawer.tsx
T
fuomag9 6ecd195073 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>
2026-03-06 22:36:46 +01:00

207 lines
6.6 KiB
TypeScript

"use client";
import {
Box,
Button,
Drawer,
IconButton,
InputAdornment,
Stack,
TextField,
Tooltip,
Typography,
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import VisibilityIcon from "@mui/icons-material/Visibility";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
import UploadFileIcon from "@mui/icons-material/UploadFile";
import { useRef, useState, useTransition } from "react";
import { createCertificateAction, updateCertificateAction } from "../actions";
import type { ImportedCertView } from "../page";
type Props = {
open: boolean;
cert: ImportedCertView | null;
onClose: () => void;
};
export function ImportCertDrawer({ open, cert, onClose }: Props) {
const isEdit = cert !== null;
const [isPending, startTransition] = useTransition();
const [showKey, setShowKey] = useState(false);
const [certPem, setCertPem] = useState("");
const [keyPem, setKeyPem] = useState("");
const formRef = useRef<HTMLFormElement>(null);
const certFileRef = useRef<HTMLInputElement>(null);
const keyFileRef = useRef<HTMLInputElement>(null);
function handleClose() {
setCertPem("");
setKeyPem("");
setShowKey(false);
onClose();
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const formData = new FormData(formRef.current!);
startTransition(async () => {
if (isEdit) {
await updateCertificateAction(cert.id, formData);
} else {
await createCertificateAction(formData);
}
handleClose();
});
}
function readFile(file: File, setter: (v: string) => void) {
const reader = new FileReader();
reader.onload = (e) => setter(e.target?.result as string);
reader.readAsText(file);
}
return (
<Drawer
anchor="right"
open={open}
onClose={handleClose}
PaperProps={{ sx: { width: { xs: "100%", sm: 480 }, p: 3 } }}
>
<Stack spacing={3} height="100%">
{/* Header */}
<Stack direction="row" alignItems="center" justifyContent="space-between">
<Typography variant="h6" fontWeight={600}>
{isEdit ? "Edit Certificate" : "Import Certificate"}
</Typography>
<IconButton onClick={handleClose} size="small">
<CloseIcon />
</IconButton>
</Stack>
{/* Form */}
<Box
component="form"
ref={formRef}
onSubmit={handleSubmit}
sx={{ flex: 1, overflowY: "auto", display: "flex", flexDirection: "column", gap: 2 }}
>
<input type="hidden" name="type" value="imported" />
<TextField
name="name"
label="Name"
defaultValue={isEdit ? cert.name : ""}
required
fullWidth
autoFocus
helperText="Descriptive name to identify this certificate"
/>
<TextField
name="domain_names"
label="Domains (one per line)"
defaultValue={isEdit ? cert.domains.join("\n") : ""}
multiline
minRows={3}
fullWidth
helperText="Domains covered by this certificate"
/>
{/* Certificate PEM */}
<Stack spacing={1}>
<TextField
name="certificate_pem"
label="Certificate PEM"
placeholder={"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"}
multiline
minRows={6}
fullWidth
value={certPem}
onChange={(e) => setCertPem(e.target.value)}
helperText="Full chain recommended (cert + intermediates)"
inputProps={{ style: { fontFamily: "monospace", fontSize: "0.8rem" } }}
/>
<input
type="file"
ref={certFileRef}
accept=".pem,.crt,.cer,.txt"
style={{ display: "none" }}
onChange={(e) => {
const file = e.target.files?.[0];
if (file) readFile(file, setCertPem);
}}
/>
<Button
size="small"
variant="outlined"
startIcon={<UploadFileIcon />}
onClick={() => certFileRef.current?.click()}
sx={{ alignSelf: "flex-start" }}
>
Load from file
</Button>
</Stack>
{/* Private Key PEM */}
<Stack spacing={1}>
<TextField
name="private_key_pem"
label="Private Key PEM"
placeholder={"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"}
multiline={showKey}
minRows={showKey ? 6 : undefined}
type={showKey ? "text" : "password"}
fullWidth
value={keyPem}
onChange={(e) => setKeyPem(e.target.value)}
helperText="Keep this secure! Never share your private key"
inputProps={{ style: { fontFamily: "monospace", fontSize: "0.8rem" } }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Tooltip title={showKey ? "Hide" : "Show"}>
<IconButton size="small" onClick={() => setShowKey((v) => !v)} edge="end">
{showKey ? <VisibilityOffIcon /> : <VisibilityIcon />}
</IconButton>
</Tooltip>
</InputAdornment>
),
}}
/>
<input
type="file"
ref={keyFileRef}
accept=".pem,.key,.txt"
style={{ display: "none" }}
onChange={(e) => {
const file = e.target.files?.[0];
if (file) readFile(file, setKeyPem);
}}
/>
<Button
size="small"
variant="outlined"
startIcon={<UploadFileIcon />}
onClick={() => keyFileRef.current?.click()}
sx={{ alignSelf: "flex-start" }}
>
Load from file
</Button>
</Stack>
{/* Actions */}
<Stack direction="row" spacing={1} justifyContent="flex-end" sx={{ mt: "auto", pt: 2 }}>
<Button onClick={handleClose} disabled={isPending}>
Cancel
</Button>
<Button type="submit" variant="contained" disabled={isPending}>
{isPending ? "Saving..." : isEdit ? "Save Changes" : "Import Certificate"}
</Button>
</Stack>
</Box>
</Stack>
</Drawer>
);
}