6ecd195073
- 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>
145 lines
4.4 KiB
TypeScript
145 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
import { Box, Stack, Tab, Tabs, TextField, Typography } from "@mui/material";
|
|
import { useState } from "react";
|
|
import type { AcmeHost, CaCertificateView, CertExpiryStatus, ImportedCertView, ManagedCertView } from "./page";
|
|
import { StatusSummaryBar } from "./components/StatusSummaryBar";
|
|
import { AcmeTab } from "./components/AcmeTab";
|
|
import { ImportedTab } from "./components/ImportedTab";
|
|
import { CaTab } from "./components/CaTab";
|
|
|
|
type TabId = "acme" | "imported" | "ca";
|
|
|
|
type Props = {
|
|
acmeHosts: AcmeHost[];
|
|
importedCerts: ImportedCertView[];
|
|
managedCerts: ManagedCertView[];
|
|
caCertificates: CaCertificateView[];
|
|
acmePagination: { total: number; page: number; perPage: number };
|
|
};
|
|
|
|
function countExpiry(statuses: (CertExpiryStatus | null)[]) {
|
|
let expired = 0;
|
|
let expiringSoon = 0;
|
|
let healthy = 0;
|
|
for (const s of statuses) {
|
|
if (s === "expired") expired++;
|
|
else if (s === "expiring_soon") expiringSoon++;
|
|
else if (s === "ok") healthy++;
|
|
}
|
|
return { expired, expiringSoon, healthy };
|
|
}
|
|
|
|
export default function CertificatesClient({
|
|
acmeHosts,
|
|
importedCerts,
|
|
managedCerts,
|
|
caCertificates,
|
|
acmePagination,
|
|
}: Props) {
|
|
const [activeTab, setActiveTab] = useState<TabId>("acme");
|
|
const [searchAcme, setSearchAcme] = useState("");
|
|
const [searchImported, setSearchImported] = useState("");
|
|
const [searchCa, setSearchCa] = useState("");
|
|
const [statusFilter, setStatusFilter] = useState<string | null>(null);
|
|
|
|
// Aggregate expiry counts across all cert types
|
|
const allStatuses: (CertExpiryStatus | null)[] = [
|
|
...acmeHosts.map((h) => h.certExpiryStatus),
|
|
...importedCerts.map((c) => c.expiryStatus),
|
|
];
|
|
const { expired, expiringSoon, healthy } = countExpiry(allStatuses);
|
|
|
|
const search = activeTab === "acme" ? searchAcme : activeTab === "imported" ? searchImported : searchCa;
|
|
const setSearch =
|
|
activeTab === "acme" ? setSearchAcme : activeTab === "imported" ? setSearchImported : setSearchCa;
|
|
|
|
function handleTabChange(_: React.SyntheticEvent, value: TabId) {
|
|
setActiveTab(value);
|
|
setStatusFilter(null);
|
|
}
|
|
|
|
return (
|
|
<Stack spacing={3} sx={{ width: "100%" }}>
|
|
{/* Page header */}
|
|
<Stack spacing={1}>
|
|
<Typography variant="h4" fontWeight={600}>
|
|
SSL/TLS Certificates
|
|
</Typography>
|
|
<Typography color="text.secondary">
|
|
Caddy automatically handles HTTPS certificates for all proxy hosts using Let's Encrypt.
|
|
Import custom certificates only when needed (internal CA, special requirements, etc.).
|
|
</Typography>
|
|
</Stack>
|
|
|
|
{/* Status summary bar */}
|
|
<StatusSummaryBar
|
|
expired={expired}
|
|
expiringSoon={expiringSoon}
|
|
healthy={healthy}
|
|
filter={statusFilter}
|
|
onFilter={setStatusFilter}
|
|
/>
|
|
|
|
{/* Tabs */}
|
|
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
|
<Tabs value={activeTab} onChange={handleTabChange}>
|
|
<Tab
|
|
label={`ACME (${acmePagination.total})`}
|
|
value="acme"
|
|
/>
|
|
<Tab
|
|
label={`Imported (${importedCerts.length})`}
|
|
value="imported"
|
|
/>
|
|
<Tab
|
|
label={`CA / mTLS (${caCertificates.length})`}
|
|
value="ca"
|
|
/>
|
|
</Tabs>
|
|
</Box>
|
|
|
|
{/* Per-tab search */}
|
|
<TextField
|
|
placeholder={
|
|
activeTab === "acme"
|
|
? "Search by host name or domain…"
|
|
: activeTab === "imported"
|
|
? "Search by name or domain…"
|
|
: "Search by name…"
|
|
}
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
size="small"
|
|
sx={{ maxWidth: 400 }}
|
|
inputProps={{ "aria-label": "search" }}
|
|
/>
|
|
|
|
{/* Tab panels */}
|
|
{activeTab === "acme" && (
|
|
<AcmeTab
|
|
acmeHosts={acmeHosts}
|
|
acmePagination={acmePagination}
|
|
search={searchAcme}
|
|
statusFilter={statusFilter}
|
|
/>
|
|
)}
|
|
{activeTab === "imported" && (
|
|
<ImportedTab
|
|
importedCerts={importedCerts}
|
|
managedCerts={managedCerts}
|
|
search={searchImported}
|
|
statusFilter={statusFilter}
|
|
/>
|
|
)}
|
|
{activeTab === "ca" && (
|
|
<CaTab
|
|
caCertificates={caCertificates}
|
|
search={searchCa}
|
|
statusFilter={statusFilter}
|
|
/>
|
|
)}
|
|
</Stack>
|
|
);
|
|
}
|