import { useState, useMemo } from 'react'; import { Link } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { getIssuers, testIssuerConnection, deleteIssuer, createIssuer } from '../api/client'; import PageHeader from '../components/PageHeader'; import DataTable from '../components/DataTable'; import type { Column } from '../components/DataTable'; import StatusBadge from '../components/StatusBadge'; import ErrorState from '../components/ErrorState'; import { formatDateTime } from '../api/utils'; import type { Issuer } from '../api/types'; import { issuerTypes, typeLabels, getIssuerCatalogStatus, type IssuerTypeConfig } from '../config/issuerTypes'; import TypeSelector from '../components/issuer/TypeSelector'; import ConfigForm from '../components/issuer/ConfigForm'; import ConfigDetailModal from '../components/issuer/ConfigDetailModal'; /** Derive display status from backend enabled boolean */ function issuerStatus(issuer: Issuer): string { if (issuer.enabled !== undefined) { return issuer.enabled ? 'Enabled' : 'Disabled'; } // Fallback for legacy data that may have status string return issuer.status || 'Unknown'; } export default function IssuersPage() { const queryClient = useQueryClient(); const [testResult, setTestResult] = useState<{ id: string; ok: boolean; msg: string } | null>(null); const [showCreateModal, setShowCreateModal] = useState(false); const [preselectedType, setPreselectedType] = useState(null); const [typeFilter, setTypeFilter] = useState(''); const [configModal, setConfigModal] = useState<{ title: string; config: Record } | null>(null); const { data, isLoading, error, refetch } = useQuery({ queryKey: ['issuers'], queryFn: () => getIssuers(), }); const testMutation = useMutation({ mutationFn: testIssuerConnection, onSuccess: (_data, id) => setTestResult({ id, ok: true, msg: 'Connection successful' }), onError: (err: Error, id) => setTestResult({ id, ok: false, msg: err.message }), }); const deleteMutation = useMutation({ mutationFn: deleteIssuer, onSuccess: () => queryClient.invalidateQueries({ queryKey: ['issuers'] }), }); const createMutation = useMutation({ mutationFn: (data: { name: string; type: string; config: Record }) => createIssuer(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['issuers'] }); setShowCreateModal(false); setPreselectedType(null); }, }); const catalogStatus = useMemo( () => getIssuerCatalogStatus(data?.data || []), [data?.data] ); // Filter issuers by type const filteredIssuers = useMemo(() => { if (!data?.data) return []; if (!typeFilter) return data.data; return data.data.filter(i => i.type === typeFilter); }, [data?.data, typeFilter]); const columns: Column[] = [ { key: 'name', label: 'Issuer', render: (i) => (
e.stopPropagation()}> {i.name}
{i.id}
), }, { key: 'type', label: 'Type', render: (i) => ( {typeLabels[i.type] || i.type} ), }, { key: 'status', label: 'Status', render: (i) => , }, { key: 'config', label: 'Config', render: (i) => { if (!i.config || Object.keys(i.config).length === 0) return ; return ( ); }, }, { key: 'created', label: 'Created', render: (i) => {formatDateTime(i.created_at)}, }, { key: 'actions', label: '', render: (i) => (
), }, ]; return ( <> { setPreselectedType(null); setShowCreateModal(true); }} className="px-4 py-2 bg-brand-600 text-white rounded font-medium hover:bg-brand-700 transition-colors text-sm" > + New Issuer } /> {testResult && (
{testResult.id}: {testResult.msg}
)}
{error ? ( refetch()} /> ) : ( <> {/* Issuer Type Catalog Cards */}

Issuer Types

{catalogStatus.map(({ type, status, count }) => ( { setPreselectedType(type.id); setShowCreateModal(true); }} onFilter={() => { // Match both the canonical id and aliases const filterValue = type.id === 'local' ? 'local' : type.id; setTypeFilter(prev => prev === filterValue ? '' : filterValue); }} /> ))}
{/* Configured Issuers Table */}

Configured Issuers

)}
{/* Config Detail Modal */} {configModal && ( setConfigModal(null)} /> )} {/* Create Issuer Modal */} {showCreateModal && ( { createMutation.mutate({ name, type, config }); }} onCancel={() => { setShowCreateModal(false); setPreselectedType(null); }} isSubmitting={createMutation.isPending} /> )} ); } // ─── Catalog Card ─────────────────────────────────────────────── interface CatalogCardProps { type: IssuerTypeConfig; status: 'connected' | 'available' | 'coming_soon'; count: number; onConfigure: () => void; onFilter: () => void; } function CatalogCard({ type, status, count, onConfigure, onFilter }: CatalogCardProps) { const statusConfig = { connected: { label: `${count} configured`, cls: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/30' }, available: { label: 'Available', cls: 'bg-brand-500/10 text-brand-400 border-brand-500/30' }, coming_soon: { label: 'Coming Soon', cls: 'bg-gray-500/10 text-gray-400 border-gray-500/30' }, }; const { label, cls } = statusConfig[status]; return (
{type.icon} {type.name}
{label}

{type.description}

{status === 'connected' && ( )} {status === 'available' && ( )}
); } // ─── Create Issuer Modal ──────────────────────────────────────── interface CreateIssuerModalProps { preselectedType: string | null; onSubmit: (name: string, type: string, config: Record) => void; onCancel: () => void; isSubmitting: boolean; } function CreateIssuerModal({ preselectedType, onSubmit, onCancel, isSubmitting }: CreateIssuerModalProps) { const [step, setStep] = useState<'type' | 'config'>(preselectedType ? 'config' : 'type'); const [selectedType, setSelectedType] = useState(preselectedType); const [form, setForm] = useState>(() => { if (preselectedType) { const tc = issuerTypes.find(t => t.id === preselectedType); const defaults: Record = {}; tc?.configFields.forEach(f => { if (f.defaultValue) defaults[f.key] = f.defaultValue; }); return defaults; } return {}; }); const selectedTypeConfig = issuerTypes.find(t => t.id === selectedType); function handleTypeSelect(typeId: string) { setSelectedType(typeId); const tc = issuerTypes.find(t => t.id === typeId); const defaults: Record = {}; tc?.configFields.forEach(f => { if (f.defaultValue) defaults[f.key] = f.defaultValue; }); setForm(defaults); setStep('config'); } function handleSubmit() { if (!selectedType || !form.name) return; const config = { ...form }; const name = config.name as string; delete config.name; onSubmit(name, selectedType, config); } return (
{/* Header */}

{step === 'type' ? 'Create Issuer' : `Configure ${selectedTypeConfig?.name || 'Issuer'}`}

{/* Content */}
{step === 'type' ? ( ) : (
{/* Name field */}
setForm({ ...form, name: e.target.value })} placeholder="e.g., Production CA" className="w-full px-3 py-2 bg-surface border border-surface-border rounded text-ink placeholder-ink-faint focus:outline-none focus:border-brand-500 transition-colors" />
{/* Type-specific fields via ConfigForm */} {selectedTypeConfig && ( setForm({ ...form, [key]: value })} /> )}
)}
{/* Footer */}
{step === 'config' && ( )} {step === 'config' && ( )}
); }