Files
Charon/frontend/src/components/CertificateChainViewer.tsx
GitHub Actions 30c9d735aa feat: add certificate export and upload dialogs
- Implemented CertificateExportDialog for exporting certificates in various formats (PEM, PFX, DER) with options to include private keys and set passwords.
- Created CertificateUploadDialog for uploading certificates, including validation and support for multiple file types (certificates, private keys, chain files).
- Updated DeleteCertificateDialog to use 'domains' instead of 'domain' for consistency.
- Refactored BulkDeleteCertificateDialog and DeleteCertificateDialog tests to accommodate changes in certificate structure.
- Added FileDropZone component for improved file upload experience.
- Enhanced translation files with new keys for certificate management features.
- Updated Certificates page to utilize the new CertificateUploadDialog and clean up the upload logic.
- Adjusted Dashboard and ProxyHosts pages to reflect changes in certificate data structure.
2026-04-13 04:01:31 +00:00

73 lines
2.6 KiB
TypeScript

import { Link2, ShieldCheck } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import type { ChainEntry } from '../api/certificates'
interface CertificateChainViewerProps {
chain: ChainEntry[]
}
function getChainLabel(index: number, total: number, t: (key: string) => string): string {
if (index === 0) return t('certificates.chainLeaf')
if (index === total - 1 && total > 1) return t('certificates.chainRoot')
return t('certificates.chainIntermediate')
}
export default function CertificateChainViewer({ chain }: CertificateChainViewerProps) {
const { t } = useTranslation()
if (!chain || chain.length === 0) {
return (
<p className="text-sm text-content-muted italic">{t('certificates.noChainData')}</p>
)
}
return (
<div
className="space-y-0"
role="list"
aria-label={t('certificates.certificateChain')}
>
{chain.map((entry, index) => {
const label = getChainLabel(index, chain.length, t)
const isLast = index === chain.length - 1
return (
<div key={index} role="listitem">
<div className="flex items-start gap-3">
<div className="flex flex-col items-center">
<div className="flex h-8 w-8 items-center justify-center rounded-full border border-gray-700 bg-surface-muted">
{index === 0 ? (
<ShieldCheck className="h-4 w-4 text-brand-400" aria-hidden="true" />
) : (
<Link2 className="h-4 w-4 text-content-muted" aria-hidden="true" />
)}
</div>
{!isLast && (
<div className="w-px h-6 bg-gray-700" aria-hidden="true" />
)}
</div>
<div className="min-w-0 flex-1 pb-2">
<div className="flex items-center gap-2">
<span className="text-xs font-medium uppercase tracking-wide text-content-muted">
{label}
</span>
</div>
<p className="text-sm font-medium text-content-primary truncate" title={entry.subject}>
{entry.subject}
</p>
<p className="text-xs text-content-muted truncate" title={entry.issuer}>
{t('certificates.issuerOrg')}: {entry.issuer}
</p>
<p className="text-xs text-content-muted">
{t('certificates.expiresAt')}: {new Date(entry.expires_at).toLocaleDateString()}
</p>
</div>
</div>
</div>
)
})}
</div>
)
}