import { useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { getCertificate, getCertificateVersions, triggerRenewal, triggerDeployment, archiveCertificate, revokeCertificate, updateCertificate, getTargets, getJobs, getPolicies, getProfiles } from '../api/client'; import { REVOCATION_REASONS } from '../api/types'; import PageHeader from '../components/PageHeader'; import StatusBadge from '../components/StatusBadge'; import ErrorState from '../components/ErrorState'; import { formatDate, formatDateTime, daysUntil, expiryColor, timeAgo } from '../api/utils'; import type { Job } from '../api/types'; function InfoRow({ label, value, editable, onEdit }: { label: string; value: React.ReactNode; editable?: boolean; onEdit?: () => void }) { return (
{label}
{value} {editable && onEdit && ( )}
); } // Timeline step component for deployment status function TimelineStep({ label, status, time, isLast }: { label: string; status: 'completed' | 'active' | 'pending' | 'failed'; time?: string; isLast?: boolean }) { const dotStyles = { completed: 'bg-emerald-500 ring-emerald-500/30', active: 'bg-blue-500 ring-blue-500/30 animate-pulse', pending: 'bg-slate-600 ring-slate-600/30', failed: 'bg-red-500 ring-red-500/30', }; const lineStyles = { completed: 'bg-emerald-500/50', active: 'bg-blue-500/30', pending: 'bg-slate-700', failed: 'bg-red-500/30', }; const textStyles = { completed: 'text-emerald-400', active: 'text-blue-400', pending: 'text-slate-500', failed: 'text-red-400', }; return (
{!isLast &&
}
{label}
{time &&
{time}
}
); } function DeploymentTimeline({ certId, certStatus, createdAt, issuedAt }: { certId: string; certStatus: string; createdAt: string; issuedAt: string }) { const { data: jobsData } = useQuery({ queryKey: ['jobs', { certificate_id: certId }], queryFn: () => getJobs({ certificate_id: certId }), }); const jobs = jobsData?.data || []; const issuanceJobs = jobs.filter((j: Job) => j.type === 'Issuance' || j.type === 'Renewal'); const deployJobs = jobs.filter((j: Job) => j.type === 'Deployment'); const latestIssuance = issuanceJobs[0]; const latestDeploy = deployJobs[0]; // Determine step statuses const getRequestedStatus = () => 'completed' as const; const getRequestedTime = () => formatDateTime(createdAt); const getIssuedStatus = () => { if (issuedAt) return 'completed' as const; if (latestIssuance?.status === 'Running' || latestIssuance?.status === 'AwaitingCSR' || latestIssuance?.status === 'AwaitingApproval') return 'active' as const; if (latestIssuance?.status === 'Failed') return 'failed' as const; return 'pending' as const; }; const getIssuedTime = () => { if (issuedAt) return formatDateTime(issuedAt); if (latestIssuance) return `${latestIssuance.status} — ${timeAgo(latestIssuance.created_at)}`; return undefined; }; const getDeployStatus = () => { if (!issuedAt) return 'pending' as const; if (latestDeploy?.status === 'Completed') return 'completed' as const; if (latestDeploy?.status === 'Running') return 'active' as const; if (latestDeploy?.status === 'Failed') return 'failed' as const; if (latestDeploy?.status === 'Pending') return 'active' as const; return 'pending' as const; }; const getDeployTime = () => { if (latestDeploy?.status === 'Completed') return formatDateTime(latestDeploy.completed_at); if (latestDeploy) return `${latestDeploy.status} — ${timeAgo(latestDeploy.created_at)}`; return undefined; }; const getActiveStatus = () => { if (certStatus === 'Active') return 'completed' as const; if (certStatus === 'Revoked') return 'failed' as const; if (certStatus === 'Expired') return 'failed' as const; if (latestDeploy?.status === 'Completed') return 'completed' as const; return 'pending' as const; }; const getActiveTime = () => { if (certStatus === 'Revoked') return 'Revoked'; if (certStatus === 'Expired') return 'Expired'; if (certStatus === 'Active') return 'Currently active'; return undefined; }; return (

Lifecycle Timeline

); } function InlinePolicyEditor({ certId, currentPolicyId, currentProfileId }: { certId: string; currentPolicyId: string; currentProfileId: string }) { const queryClient = useQueryClient(); const [editing, setEditing] = useState(false); const [policyId, setPolicyId] = useState(currentPolicyId); const [profileId, setProfileId] = useState(currentProfileId); const { data: policies } = useQuery({ queryKey: ['policies'], queryFn: () => getPolicies(), enabled: editing, }); const { data: profiles } = useQuery({ queryKey: ['profiles'], queryFn: () => getProfiles(), enabled: editing, }); const saveMutation = useMutation({ mutationFn: () => updateCertificate(certId, { renewal_policy_id: policyId, certificate_profile_id: profileId, }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['certificate', certId] }); setEditing(false); }, }); if (!editing) { return (

Policy & Profile

); } return (

Edit Policy & Profile

{saveMutation.isError && (
{saveMutation.error instanceof Error ? saveMutation.error.message : 'Failed to save'}
)}
); } export default function CertificateDetailPage() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const queryClient = useQueryClient(); const [showDeploy, setShowDeploy] = useState(false); const [deployTargetId, setDeployTargetId] = useState(''); const [showRevoke, setShowRevoke] = useState(false); const [revokeReason, setRevokeReason] = useState('unspecified'); const { data: cert, isLoading, error, refetch } = useQuery({ queryKey: ['certificate', id], queryFn: () => getCertificate(id!), enabled: !!id, }); const { data: versions } = useQuery({ queryKey: ['certificate-versions', id], queryFn: () => getCertificateVersions(id!), enabled: !!id, }); const { data: targets } = useQuery({ queryKey: ['targets'], queryFn: () => getTargets(), enabled: showDeploy, }); const renewMutation = useMutation({ mutationFn: () => triggerRenewal(id!), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['certificate', id] }); queryClient.invalidateQueries({ queryKey: ['certificates'] }); }, }); const deployMutation = useMutation({ mutationFn: () => triggerDeployment(id!, deployTargetId), onSuccess: () => { setShowDeploy(false); setDeployTargetId(''); queryClient.invalidateQueries({ queryKey: ['certificate', id] }); }, }); const archiveMutation = useMutation({ mutationFn: () => archiveCertificate(id!), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['certificates'] }); navigate('/certificates'); }, }); const revokeMutation = useMutation({ mutationFn: () => revokeCertificate(id!, revokeReason), onSuccess: () => { setShowRevoke(false); setRevokeReason('unspecified'); queryClient.invalidateQueries({ queryKey: ['certificate', id] }); queryClient.invalidateQueries({ queryKey: ['certificates'] }); }, }); if (isLoading) { return ( <>
Loading...
); } if (error || !cert) { return ( <> refetch()} /> ); } const days = daysUntil(cert.expires_at); const isRevoked = cert.status === 'Revoked'; const isArchived = cert.status === 'Archived'; const canRevoke = !isRevoked && !isArchived; return ( <> {canRevoke && ( )} {!isArchived && ( )}
} />
{renewMutation.isSuccess && (
Renewal triggered successfully. A renewal job has been created.
)} {renewMutation.isError && (
Failed to trigger renewal: {renewMutation.error instanceof Error ? renewMutation.error.message : 'Unknown error'}
)} {deployMutation.isSuccess && (
Deployment triggered. A deployment job has been created.
)} {deployMutation.isError && (
Failed to deploy: {deployMutation.error instanceof Error ? deployMutation.error.message : 'Unknown error'}
)} {archiveMutation.isError && (
Failed to archive: {archiveMutation.error instanceof Error ? archiveMutation.error.message : 'Unknown error'}
)} {revokeMutation.isSuccess && (
Certificate revoked successfully. It has been added to the CRL.
)} {revokeMutation.isError && (
Failed to revoke: {revokeMutation.error instanceof Error ? revokeMutation.error.message : 'Unknown error'}
)} {/* Revocation Banner */} {isRevoked && (
Certificate Revoked
Reason: {REVOCATION_REASONS.find(r => r.value === cert.revocation_reason)?.label || cert.revocation_reason || 'Unspecified'} {cert.revoked_at && <> · Revoked {formatDateTime(cert.revoked_at)}}
)} {/* Deployment Status Timeline */}
{/* Certificate Info */}

Certificate Details

} /> {cert.fingerprint.slice(0, 24)}... : '—' } />
{/* Lifecycle */}

Lifecycle

{formatDate(cert.expires_at)} ({days <= 0 ? 'expired' : `${days} days`}) } /> {isRevoked && ( <> {cert.revoked_at ? formatDateTime(cert.revoked_at) : '—'} } /> {REVOCATION_REASONS.find(r => r.value === cert.revocation_reason)?.label || cert.revocation_reason || '—'} } /> )}
{/* Inline Policy Editor */} {/* Tags */} {cert.tags && Object.keys(cert.tags).length > 0 && (

Tags

{Object.entries(cert.tags).map(([k, v]) => ( {k}: {v} ))}
)} {/* Version History */}

Version History {versions?.data?.length ? `(${versions.data.length})` : ''}

{!versions?.data?.length ? (

No versions yet

) : (
{versions.data.map((v, idx) => (
Version {v.version} {idx === 0 && Current}
{v.serial_number}
{formatDate(v.not_before)} — {formatDate(v.not_after)}
{formatDateTime(v.created_at)}
{idx > 0 && cert?.status !== 'Archived' && cert?.status !== 'Revoked' && ( )}
))}
)}
{/* Deploy Modal */} {showDeploy && (
setShowDeploy(false)}>
e.stopPropagation()}>

Deploy Certificate

{deployMutation.isError && (
{deployMutation.error instanceof Error ? deployMutation.error.message : 'Unknown error'}
)}
)} {/* Revoke Modal */} {showRevoke && (
setShowRevoke(false)}>
e.stopPropagation()}>

Revoke Certificate

This action cannot be undone. The certificate will be added to the CRL and marked as revoked.

{revokeMutation.isError && (
{revokeMutation.error instanceof Error ? revokeMutation.error.message : 'Unknown error'}
)}
)} ); }