fix: polish CertificatesPage layout
- 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:
65
ui/src/pages/vendor/CertificatesPage.tsx
vendored
65
ui/src/pages/vendor/CertificatesPage.tsx
vendored
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user