'use client'; import { useTranslations } from 'next-intl'; import { useMemo } from 'react'; import type { EChartsOption } from 'echarts'; import ReactECharts from 'echarts-for-react'; import { Cell, Pie, PieChart } from 'recharts'; import { RankCard } from '@/components/data/rank-card'; import { TrendChart } from '@/components/data/trend-chart'; import { EmptyStateWithBorder } from '@/components/layout/empty'; import { ErrorInline } from '@/components/layout/error'; import { LoadingStateWithBorder } from '@/components/layout/loading'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, type ChartConfig, } from '@/components/ui/chart'; import type { AccessLogOverview, DistributionItem, } from '@/lib/services/openflare'; import { formatBytes, formatCompactNumber } from '@/lib/utils/metrics'; import { formatOverviewRangeHint, formatOverviewTrendLabel, type OverviewRangeHours, } from './access-log-utils'; import { OverviewToolbar } from './overview-toolbar'; const DEVICE_COLORS = [ '#38bdf8', '#34d399', '#f59e0b', '#a78bfa', '#f472b6', '#94a3b8', ]; function SparklineMetricCard({ title, value, hint, color, fillColor, labels, values, valueFormatter, }: { title: string; value: string; hint: string; color: string; fillColor: string; labels: string[]; values: number[]; valueFormatter?: (value: number) => string; }) { const option = useMemo( () => ({ animationDuration: 400, grid: { left: 0, right: 0, top: 8, bottom: 0, }, xAxis: { type: 'category', show: false, boundaryGap: false, data: labels, }, yAxis: { type: 'value', show: false, min: 0, }, tooltip: { trigger: 'axis', backgroundColor: 'rgba(15, 23, 42, 0.92)', borderWidth: 0, textStyle: { color: '#e2e8f0', fontSize: 12, }, formatter: (params: unknown) => { const items = Array.isArray(params) ? params : []; const item = items[0] as | { axisValueLabel?: string; value?: number } | undefined; if (!item) return ''; const raw = typeof item.value === 'number' ? item.value : 0; const formatted = valueFormatter ? valueFormatter(raw) : formatCompactNumber(raw); return `${item.axisValueLabel ?? ''}
${formatted}`; }, }, series: [ { type: 'line', data: values, smooth: true, showSymbol: false, lineStyle: { color, width: 2, }, areaStyle: { color: fillColor, }, }, ], }), [color, fillColor, labels, valueFormatter, values], ); return (

{title}

{value}

{hint}

{labels.length > 0 ? ( ) : null}
); } function toRankItems(items: DistributionItem[] | undefined) { return (items ?? []).map((item) => ({ label: item.key, value: item.value, })); } function PieDistributionCard({ title, description, items, emptyMessage, }: { title: string; description: string; items: DistributionItem[]; emptyMessage: string; }) { const chartData = useMemo( () => items.map((item, index) => ({ name: item.key, value: item.value, fill: DEVICE_COLORS[index % DEVICE_COLORS.length], })), [items], ); const chartConfig = useMemo(() => { const config: ChartConfig = {}; chartData.forEach((item) => { config[item.name] = { label: item.name, color: item.fill, }; }); return config; }, [chartData]); return ( {title} {description} {chartData.length === 0 ? (
{emptyMessage}
) : ( {chartData.map((entry) => ( ))} ( <> {name} {formatCompactNumber(Number(value ?? 0))} )} /> } /> } /> )}
); } export function OverviewTab({ data, loading, error, hours, hosts, onHoursChange, onHostsChange, onRetry, }: { data?: AccessLogOverview; loading: boolean; error: Error | null; hours: OverviewRangeHours; hosts: string[]; onHoursChange: (hours: OverviewRangeHours) => void; onHostsChange: (hosts: string[]) => void; onRetry: () => void; }) { const t = useTranslations('accessLogs.overview'); return (
{loading ? ( ) : error ? ( ) : !data ? ( ) : ( )}
); } function OverviewContent({ data, hours, }: { data: AccessLogOverview; hours: number; }) { const t = useTranslations('accessLogs'); const requestLabels = data.trends.requests.map((point) => formatOverviewTrendLabel(point.bucket_started_at, hours), ); const requestValues = data.trends.requests.map((point) => point.value); const visitValues = data.trends.visits.map((point) => point.value); const bandwidthValues = data.trends.bandwidth.map((point) => point.value); const hint = formatOverviewRangeHint(hours, (key, values) => t(key, values)); return ( <>
Requests over time {t('overview.requestsHint')}
); }