fix: polish CertificatesPage layout
All checks were successful
CI / build (push) Successful in 1m7s
CI / docker (push) Successful in 44s

- Truncate fingerprint with hover tooltip
- Remove duplicate warning icon in stale banner
- Style file inputs to match design system
- Bump grid min-width for better card spacing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-10 19:04:16 +02:00
parent a48c4bfd08
commit 22752ffcb1

View File

@@ -7,7 +7,7 @@ import {
Spinner, Spinner,
useToast, useToast,
} from '@cameleer/design-system'; } from '@cameleer/design-system';
import { Upload, ShieldCheck, RotateCcw, Trash2, AlertTriangle } from 'lucide-react'; import { Upload, ShieldCheck, RotateCcw, Trash2 } from 'lucide-react';
import { import {
useVendorCertificates, useVendorCertificates,
useStageCertificate, useStageCertificate,
@@ -35,6 +35,12 @@ function expiryColor(iso: string | null | undefined): string | undefined {
return undefined; return undefined;
} }
function shortenFingerprint(fp: string | null | undefined): string {
if (!fp) return '—';
// Show first 23 chars (8 hex pairs) + ellipsis
return fp.length > 23 ? fp.slice(0, 23) + '...' : fp;
}
function CertCard({ function CertCard({
cert, cert,
title, title,
@@ -68,8 +74,8 @@ function CertCard({
</div> </div>
<div className={styles.kvRow}> <div className={styles.kvRow}>
<span className={styles.kvLabel}>Fingerprint</span> <span className={styles.kvLabel}>Fingerprint</span>
<span className={styles.kvValueMono} style={{ fontSize: '0.7rem', maxWidth: 220, textAlign: 'right' }}> <span className={styles.kvValueMono} title={cert.fingerprint} style={{ fontSize: '0.7rem' }}>
{cert.fingerprint} {shortenFingerprint(cert.fingerprint)}
</span> </span>
</div> </div>
<div className={styles.kvRow}> <div className={styles.kvRow}>
@@ -94,6 +100,34 @@ function CertCard({
); );
} }
function FileField({ label, inputRef, accept }: {
label: string;
inputRef: React.RefObject<HTMLInputElement | null>;
accept: string;
}) {
return (
<div>
<label style={{ fontSize: 13, color: 'var(--text-secondary)', display: 'block', marginBottom: 4 }}>
{label}
</label>
<input
ref={inputRef}
type="file"
accept={accept}
style={{
fontSize: 13,
color: 'var(--text-primary)',
background: 'var(--bg-inset)',
border: '1px solid var(--border)',
borderRadius: 6,
padding: '6px 8px',
width: '100%',
}}
/>
</div>
);
}
export function CertificatesPage() { export function CertificatesPage() {
const { toast } = useToast(); const { toast } = useToast();
const { data, isLoading, isError } = useVendorCertificates(); const { data, isLoading, isError } = useVendorCertificates();
@@ -143,7 +177,6 @@ export function CertificatesPage() {
const result = await stageMutation.mutateAsync(formData); const result = await stageMutation.mutateAsync(formData);
if (result.valid) { if (result.valid) {
toast({ title: 'Certificate staged successfully', variant: 'success' }); toast({ title: 'Certificate staged successfully', variant: 'success' });
// Clear file inputs
if (certInputRef.current) certInputRef.current.value = ''; if (certInputRef.current) certInputRef.current.value = '';
if (keyInputRef.current) keyInputRef.current.value = ''; if (keyInputRef.current) keyInputRef.current.value = '';
if (caInputRef.current) caInputRef.current.value = ''; if (caInputRef.current) caInputRef.current.value = '';
@@ -192,13 +225,12 @@ export function CertificatesPage() {
{data.staleTenantCount > 0 && ( {data.staleTenantCount > 0 && (
<Alert variant="warning" title="CA bundle updated"> <Alert variant="warning" title="CA bundle updated">
<AlertTriangle size={14} style={{ verticalAlign: 'middle', marginRight: 6 }} />
{data.staleTenantCount} tenant{data.staleTenantCount > 1 ? 's' : ''} need a restart {data.staleTenantCount} tenant{data.staleTenantCount > 1 ? 's' : ''} need a restart
to pick up the updated CA bundle. to pick up the updated CA bundle.
</Alert> </Alert>
)} )}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: 16 }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))', gap: 16 }}>
{data.active && ( {data.active && (
<CertCard cert={data.active} title="Active Certificate" /> <CertCard cert={data.active} title="Active Certificate" />
)} )}
@@ -251,24 +283,9 @@ export function CertificatesPage() {
{/* Upload card */} {/* Upload card */}
<Card title="Upload Certificate"> <Card title="Upload Certificate">
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<div> <FileField label="Certificate (PEM) *" inputRef={certInputRef} accept=".pem,.crt,.cer" />
<label style={{ fontSize: 13, color: 'var(--text-secondary)', display: 'block', marginBottom: 4 }}> <FileField label="Private Key (PEM) *" inputRef={keyInputRef} accept=".pem,.key" />
Certificate (PEM) * <FileField label="CA Bundle (PEM, optional)" inputRef={caInputRef} accept=".pem,.crt,.cer" />
</label>
<input ref={certInputRef} type="file" accept=".pem,.crt,.cer" />
</div>
<div>
<label style={{ fontSize: 13, color: 'var(--text-secondary)', display: 'block', marginBottom: 4 }}>
Private Key (PEM) *
</label>
<input ref={keyInputRef} type="file" accept=".pem,.key" />
</div>
<div>
<label style={{ fontSize: 13, color: 'var(--text-secondary)', display: 'block', marginBottom: 4 }}>
CA Bundle (PEM, optional)
</label>
<input ref={caInputRef} type="file" accept=".pem,.crt,.cer" />
</div>
<Button <Button
variant="primary" variant="primary"
onClick={handleUpload} onClick={handleUpload}