'use client'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; import { FileKey, Plus, RefreshCw, Trash2 } from 'lucide-react'; import { toast } from 'sonner'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { EmptyStateWithBorder } from '@/components/layout/empty'; import { ErrorInline } from '@/components/layout/error'; import { LoadingStateWithBorder } from '@/components/layout/loading'; import type { TlsCertificateItem } from '@/lib/services/openflare'; import { TlsCertificateService } from '@/lib/services/openflare'; import { formatDateTime } from '@/lib/utils'; import { CertificateApplyDialog } from '../websites/components/certificate-apply-dialog'; import { CertificateDetailDialog } from '../websites/components/certificate-detail-dialog'; import { CertificateEditorDialog } from '../websites/components/certificate-editor-dialog'; import { CertificateImportDialog } from '../websites/components/certificate-import-dialog'; import { WebsiteStatusBadge } from '../websites/components/status-badge'; import { useTranslations } from 'next-intl'; import { getCertificateStatus, getErrorMessage, } from '../websites/components/website-utils'; const certificatesQueryKey = ['openflare', 'tls-certificates']; type CertificateApplyMode = 'edit-acme' | 'convert-upload'; export default function CertificatesPage() { const t = useTranslations('certificates'); const tc = useTranslations('common'); const queryClient = useQueryClient(); const [importOpen, setImportOpen] = useState(false); const [applyOpen, setApplyOpen] = useState(false); const [detailOpen, setDetailOpen] = useState(false); const [editorOpen, setEditorOpen] = useState(false); const [deleteTarget, setDeleteTarget] = useState( null, ); const [selectedCertificateId, setSelectedCertificateId] = useState< number | null >(null); const [applyCertificate, setApplyCertificate] = useState(null); const [applyMode, setApplyMode] = useState('edit-acme'); const certificatesQuery = useQuery({ queryKey: certificatesQueryKey, queryFn: () => TlsCertificateService.list(), }); const deleteMutation = useMutation({ mutationFn: (id: number) => TlsCertificateService.deleteById(id), onSuccess: async () => { toast.success(t('deleted')); setDeleteTarget(null); await queryClient.invalidateQueries({ queryKey: certificatesQueryKey }); }, onError: (error) => toast.error(getErrorMessage(error, t('requestFailed'))), }); const renewMutation = useMutation({ mutationFn: (id: number) => TlsCertificateService.renew(id), onSuccess: async (cert) => { toast.success(t('renewSubmitted', { name: cert.name })); await queryClient.invalidateQueries({ queryKey: certificatesQueryKey }); }, onError: (error) => toast.error(getErrorMessage(error, t('requestFailed'))), }); const certificates = useMemo( () => certificatesQuery.data ?? [], [certificatesQuery.data], ); const handleOpenEditor = (certificate: TlsCertificateItem) => { if (certificate.provider === 'acme') { setApplyMode('edit-acme'); setApplyCertificate(certificate); setApplyOpen(true); } else { setSelectedCertificateId(certificate.id); setEditorOpen(true); } }; return (

{t('title')}

{t('listTitle')} {t('listDesc')} {certificatesQuery.isLoading ? ( ) : certificatesQuery.isError ? (
void certificatesQuery.refetch()} className='justify-center' />
) : certificates.length === 0 ? ( ) : (
{certificates.map((certificate) => { const status = getCertificateStatus(certificate, t); return (

{certificate.name}

{t('notBeforeLabel', { value: formatDateTime(certificate.not_before), })}

{t('notAfterLabel', { value: formatDateTime(certificate.not_after), })}

{t('sourceLabel', { value: certificate.provider === 'acme' ? t('sourceAcme') : t('sourceUpload'), })}

{certificate.apply_status === 'applying' ? (

{t('statusLabel', { value: certificate.provider === 'upload' ? t('converting') : t('applying'), })}

) : null} {certificate.apply_status === 'error' ? (

{t('statusError', { value: certificate.provider === 'upload' ? t('convertFailed') : t('applyFailed'), message: certificate.apply_message, })}

) : null}

{t('remarkLabel', { value: certificate.remark || t('noRemark'), })}

{certificate.provider === 'acme' ? ( ) : null}
); })}
)}
toast.success(t('imported', { name: certificate.name })) } /> toast.success(t('applySubmitted', { name: certificate.name })) } /> {applyCertificate ? ( { setApplyOpen(open); if (!open) setApplyCertificate(null); }} mode={applyMode} certificate={applyCertificate} onApplied={(certificate) => { setApplyCertificate(null); toast.success( applyMode === 'convert-upload' ? t('convertSubmitted', { name: certificate.name }) : t('reapplySubmitted', { name: certificate.name }), ); }} /> ) : null} { setDetailOpen(false); const item = certificates.find((c) => c.id === selectedCertificateId); if (item) handleOpenEditor(item); }} onDelete={() => { const item = certificates.find((c) => c.id === selectedCertificateId); if (item) { setDetailOpen(false); setDeleteTarget(item); } }} deleting={deleteMutation.isPending} /> toast.success(t('updated', { name: certificate.name })) } onConvert={(certificate) => { setEditorOpen(false); setApplyMode('convert-upload'); setApplyCertificate(certificate); setApplyOpen(true); }} /> !open && setDeleteTarget(null)} > {t('deleteTitle')} {t('deleteDesc', { name: deleteTarget?.name ?? '' })} {tc('cancel')} deleteTarget && deleteMutation.mutate(deleteTarget.id) } > {tc('delete')}
); }