import { useQuery } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend, } from 'recharts'; import { getCertificates, getAgents, getJobs, getNotifications, getHealth, getDashboardSummary, getCertificatesByStatus, getExpirationTimeline, getJobTrends, getIssuanceRate, } from '../api/client'; import PageHeader from '../components/PageHeader'; import StatusBadge from '../components/StatusBadge'; import { daysUntil, expiryColor, formatDate } from '../api/utils'; const STATUS_COLORS: Record = { Active: '#10b981', Expiring: '#f59e0b', Expired: '#ef4444', Revoked: '#8b5cf6', Pending: '#6366f1', RenewalInProgress: '#3b82f6', Failed: '#f43f5e', Archived: '#64748b', }; function StatCard({ label, value, icon, color }: { label: string; value: string | number; icon: string; color: string }) { const colorMap: Record = { success: 'bg-emerald-500/10 text-emerald-400', warning: 'bg-amber-500/10 text-amber-400', danger: 'bg-red-500/10 text-red-400', info: 'bg-blue-500/10 text-blue-400', }; return (

{label}

{value}

); } function ChartCard({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
); } const CustomTooltip = ({ active, payload, label }: any) => { if (!active || !payload?.length) return null; return (

{label}

{payload.map((entry: any, i: number) => (

{entry.name}: {typeof entry.value === 'number' && entry.name?.includes('rate') ? `${entry.value.toFixed(1)}%` : entry.value}

))}
); }; export default function DashboardPage() { const navigate = useNavigate(); const { data: health } = useQuery({ queryKey: ['health'], queryFn: getHealth, refetchInterval: 30000 }); const { data: summary } = useQuery({ queryKey: ['dashboard-summary'], queryFn: getDashboardSummary, refetchInterval: 30000 }); const { data: statusCounts } = useQuery({ queryKey: ['certs-by-status'], queryFn: getCertificatesByStatus, refetchInterval: 30000 }); const { data: expirationTimeline } = useQuery({ queryKey: ['expiration-timeline'], queryFn: () => getExpirationTimeline(90), refetchInterval: 60000 }); const { data: jobTrends } = useQuery({ queryKey: ['job-trends'], queryFn: () => getJobTrends(30), refetchInterval: 30000 }); const { data: issuanceRate } = useQuery({ queryKey: ['issuance-rate'], queryFn: () => getIssuanceRate(30), refetchInterval: 60000 }); const { data: certs } = useQuery({ queryKey: ['certificates', {}], queryFn: () => getCertificates(), refetchInterval: 30000 }); const { data: jobs } = useQuery({ queryKey: ['jobs', {}], queryFn: () => getJobs(), refetchInterval: 10000 }); const totalCerts = summary?.total_certificates || 0; const expiringSoon = summary?.expiring_certificates || 0; const expired = summary?.expired_certificates || 0; const activeAgents = summary?.active_agents || 0; const pendingJobs = summary?.pending_jobs || 0; // Prepare pie chart data const pieData = (statusCounts || []).filter(s => s.count > 0).map(s => ({ name: s.status, value: s.count, fill: STATUS_COLORS[s.status] || '#64748b', })); // Format expiration heatmap for display — aggregate weekly for 90 days const weeklyExpiration = (expirationTimeline || []).reduce<{ week: string; count: number }[]>((acc, bucket, i) => { const weekIdx = Math.floor(i / 7); if (!acc[weekIdx]) { acc[weekIdx] = { week: bucket.date, count: 0 }; } acc[weekIdx].count += bucket.count; return acc; }, []); // Format dates for x-axis labels const formatShortDate = (dateStr: string) => { const d = new Date(dateStr + 'T00:00:00'); return `${d.getMonth() + 1}/${d.getDate()}`; }; return ( <>
{/* Stats */}
0 ? 'warning' : 'success'} icon="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> 0 ? 'danger' : 'success'} icon="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> 0 ? 'warning' : 'info'} icon="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
{/* Charts Row 1 */}
{/* Certificates by Status (Pie) */} {pieData.length > 0 ? ( `${name}: ${value}`} labelLine={false} > {pieData.map((entry, index) => ( ))} } /> {value}} /> ) : (
No certificate data
)}
{/* Expiration Heatmap (Bar chart by week) */} {weeklyExpiration.length > 0 ? ( } /> ) : (
No expiration data
)}
{/* Charts Row 2 */}
{/* Job Trends (Line chart) */} {(jobTrends || []).length > 0 ? ( } /> {value}} /> ) : (
No job trend data
)}
{/* Issuance Rate (Bar chart) */} {(issuanceRate || []).length > 0 ? ( } /> ) : (
No issuance data
)}
{/* Expiring Certificates */}

Certificates Expiring Soon

{!certs?.data?.length ? (

No certificates

) : (
{certs.data .filter(c => c.status !== 'Archived') .sort((a, b) => new Date(a.expires_at).getTime() - new Date(b.expires_at).getTime()) .slice(0, 5) .map(c => { const days = daysUntil(c.expires_at); return (
navigate(`/certificates/${c.id}`)} className="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-slate-700/50 cursor-pointer transition-colors" >
{c.common_name}
{c.environment || 'no env'}
{days <= 0 ? 'Expired' : `${days} days`}
{formatDate(c.expires_at)}
); })}
)}
{/* Recent Jobs */}

Recent Jobs

{!jobs?.data?.length ? (

No jobs

) : (
{jobs.data.slice(0, 5).map(j => (
{j.type}
{j.certificate_id}
))}
)}
{/* Pending Jobs Banner */} {pendingJobs > 0 && (

{pendingJobs} pending job{pendingJobs > 1 ? 's' : ''}

Jobs are waiting to be processed

)}
); }