feat: add mobile card views to ImportedTab and CaTab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
fuomag9
2026-03-12 01:28:09 +01:00
parent b97e41ed07
commit 222da9c731
2 changed files with 68 additions and 0 deletions

View File

@@ -18,6 +18,8 @@ import {
TableHead,
TableRow,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import MoreVertIcon from "@mui/icons-material/MoreVert";
@@ -171,6 +173,8 @@ export function CaTab({ caCertificates, search, statusFilter }: Props) {
const [drawerCert, setDrawerCert] = useState<CaCertificateView | null | false>(false);
const [deleteCert, setDeleteCert] = useState<CaCertificateView | null>(null);
const [expandedId, setExpandedId] = useState<number | null>(null);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
const filtered = caCertificates.filter((ca) => {
// CA certs have no expiry status so if filtering by expiry, hide them
@@ -179,6 +183,50 @@ export function CaTab({ caCertificates, search, statusFilter }: Props) {
return true;
});
if (isMobile) {
return (
<Stack spacing={2}>
<Box sx={{ display: "flex", justifyContent: "flex-end" }}>
<Button startIcon={<AddIcon />} variant="outlined" size="small" onClick={() => setDrawerCert(null)}>
Add CA Certificate
</Button>
</Box>
{filtered.length === 0 ? (
<Card variant="outlined" sx={{ py: 6, textAlign: "center" }}>
<Typography color="text.secondary">
{search || statusFilter ? "No CA certificates match" : "No CA certificates configured."}
</Typography>
</Card>
) : (
<Stack spacing={1.5}>
{filtered.map((ca) => {
const activeCount = ca.issuedCerts.filter((c) => !c.revoked_at).length;
return (
<Card key={ca.id} variant="outlined" sx={{ p: 2 }}>
<Stack spacing={0.5}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography variant="subtitle2" fontWeight={700}>{ca.name}</Typography>
<CaActionsMenu ca={ca} onEdit={() => setDrawerCert(ca)} onDelete={() => setDeleteCert(ca)} />
</Stack>
<Stack direction="row" spacing={1} flexWrap="wrap">
{ca.has_private_key && <Chip label="Key stored" size="small" color="success" variant="outlined" />}
{ca.issuedCerts.length > 0 && (
<Chip label={`${activeCount}/${ca.issuedCerts.length} active`} size="small" color={activeCount > 0 ? "success" : "default"} variant="outlined" />
)}
<Typography variant="caption" color="text.secondary">{formatRelativeDate(ca.created_at)}</Typography>
</Stack>
</Stack>
</Card>
);
})}
</Stack>
)}
<CaCertDrawer open={drawerCert !== false} cert={drawerCert || null} onClose={() => setDrawerCert(false)} />
{deleteCert && <DeleteCaCertDialog open={!!deleteCert} cert={deleteCert} onClose={() => setDeleteCert(null)} />}
</Stack>
);
}
return (
<Stack spacing={2}>
<Box sx={{ display: "flex", justifyContent: "flex-end" }}>

View File

@@ -4,6 +4,7 @@ import {
Alert,
Box,
Button,
Card,
Chip,
IconButton,
Menu,
@@ -88,8 +89,26 @@ function ActionsMenu({ cert, onEdit }: { cert: ImportedCertView; onEdit: () => v
);
}
function importedMobileCard(c: ImportedCertView, onEdit: () => void) {
return (
<Card variant="outlined" sx={{ p: 2 }}>
<Stack spacing={0.5}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography variant="subtitle2" fontWeight={700}>{c.name}</Typography>
<ActionsMenu cert={c} onEdit={onEdit} />
</Stack>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: "0.75rem" }}>
{c.domains.slice(0, 2).join(", ")}{c.domains.length > 2 ? ` +${c.domains.length - 2} more` : ""}
</Typography>
<RelativeTime validTo={c.validTo} status={c.expiryStatus} />
</Stack>
</Card>
);
}
export function ImportedTab({ importedCerts, managedCerts, search, statusFilter }: Props) {
const [drawerCert, setDrawerCert] = useState<ImportedCertView | null | false>(false);
const mobileCardRenderer = (c: ImportedCertView) => importedMobileCard(c, () => setDrawerCert(c));
const filtered = importedCerts.filter((c) => {
if (statusFilter && c.expiryStatus !== statusFilter) return false;
@@ -164,6 +183,7 @@ export function ImportedTab({ importedCerts, managedCerts, search, statusFilter
data={filtered}
keyField="id"
emptyMessage="No imported certificates match"
mobileCard={mobileCardRenderer}
/>
{/* Legacy managed certs */}