import { Area, Bar, Column, Pie } from '@ant-design/plots'; import { Alert, Col, List, Row, Space, Table, Typography } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { EmptyState } from '../components/common/EmptyState'; import { MetricCard } from '../components/common/MetricCard'; import { PageHeader } from '../components/common/PageHeader'; import { SectionCard } from '../components/common/SectionCard'; import { StatusPill } from '../components/common/StatusPill'; import { buildDashboardSummary, buildDomainRanking, buildHourlyHeatmap, buildStatusDistribution, buildTrendSeries } from '../frontend/analytics-model.js'; import { buildDomainHealth } from '../frontend/domain-model.js'; import { useI18n } from '../frontend/i18n/react'; import { brandColors } from '../frontend/theme'; import type { Analytics, Domain, RuntimeConfig, SendEvent, SmtpCredential } from '../frontend/types'; interface DashboardProps { analytics: Analytics | null; domains: Domain[]; events: SendEvent[]; config: RuntimeConfig | null; smtpCredential: SmtpCredential | null; } export default function Dashboard({ analytics, domains, events, config, smtpCredential }: DashboardProps) { const { t } = useI18n(); const summary = buildDashboardSummary({ analytics, domains, events, config, smtpCredential }); const trendSeries = buildTrendSeries(analytics); const trendData = trendSeries.flatMap((item) => [ { date: item.date, type: t('metrics.total'), value: item.total }, { date: item.date, type: t('metrics.accepted'), value: item.accepted }, { date: item.date, type: t('metrics.failed'), value: item.failed } ]); const statusData = buildStatusDistribution(analytics).map((item) => ({ ...item, label: statusLabel(item.status, t) })); const rankingData = buildDomainRanking(analytics); const hourlyData = buildHourlyHeatmap(analytics); const lastSentLabel = summary.lastSentAt ? new Date(summary.lastSentAt).toLocaleString() : t('common.notFound'); const columns: ColumnsType = [ { title: 'Time', dataIndex: 'createdAt', render: (value) => new Date(value).toLocaleString() }, { title: 'Recipient', dataIndex: 'recipients', render: (value: string[]) => value.join(', ') }, { title: 'Domain', dataIndex: 'domain' }, { title: 'Subject', dataIndex: 'subject', ellipsis: true }, { title: t('common.status'), dataIndex: 'status', render: (value) => ( {statusLabel(value, t)} ) } ]; return ( {config?.usingDefaultAdminPassword ? ( ) : null} {t('dashboard.smtpStatus')}:{' '} {summary.smtpReady ? t('dashboard.smtpReady') : t('dashboard.smtpNotConfigured')} } /> 0 ? 'warning' : 'default'} hint={summary.dnsIssues > 0 ? t('dashboard.dnsActionHint') : undefined} /> {trendData.length ? ( ) : ( )} {statusData.length ? ( `${datum.label || ''}${datum.value != null ? ` ${datum.value}` : ''}`, position: 'outside' }} legend={{ color: { position: 'bottom' } }} tooltip={{ title: (datum: { label?: string }) => datum.label || '', items: [ (datum: { value?: number }) => ({ name: t('metrics.total'), value: datum.value ?? 0 }) ] }} /> ) : ( )} {rankingData.length ? ( ) : ( )} {hourlyData.length ? ( ) : ( )} {analytics?.recentFailures?.length ? ( ( {item.subject || item.domain || '-'}} description={ {new Date(item.createdAt).toLocaleString()} {item.detail} } /> )} /> ) : ( )} {domains.slice(0, 6).map((domain) => { const health = buildDomainHealth(domain); return (
{domain.domain} DNS {health.passed}/{health.total}
{domainHealthLabel(health.status, t)}
); })} {!domains.length ? : null}
{t('dashboard.lastSentAt')}: {lastSentLabel} } > ); } function statusLabel(status: string, t: (key: string) => string) { if (status === 'queued') return t('logs.statusQueued'); if (status === 'sent') return t('logs.statusSent'); if (status === 'deferred') return t('logs.statusDeferred'); if (status === 'bounced') return t('logs.statusBounced'); if (status === 'failed') return t('logs.statusFailed'); return status || t('dashboard.statusUnknown'); } function statusTone(status: string): 'success' | 'warning' | 'error' | 'info' | 'neutral' { if (status === 'queued') return 'info'; if (status === 'sent') return 'success'; if (status === 'deferred') return 'warning'; if (status === 'bounced' || status === 'failed') return 'error'; return 'neutral'; } function domainHealthLabel(status: string, t: (key: string) => string) { if (status === 'success') return t('domains.healthy'); if (status === 'warning') return t('domains.waitingDns'); return t('domains.needsAction'); } function domainHealthTone(status: string): 'success' | 'warning' | 'error' { if (status === 'success') return 'success'; if (status === 'warning') return 'warning'; return 'error'; }