'use client'; import { useMemo } from 'react'; import type { EChartsOption } from 'echarts'; import ReactECharts from 'echarts-for-react'; import { RankChart } from '@/components/data/rank-chart'; 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 { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; import type { AccessLogOverview } from '@/lib/services/openflare'; import { formatBytes, formatCompactNumber } from '@/lib/utils/metrics'; import { formatOverviewRangeHint, formatOverviewTrendLabel, OVERVIEW_RANGE_OPTIONS, type OverviewRangeHours, } from './access-log-utils'; 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 RankCard({ title, description, items, color, }: { title: string; description: string; items: { label: string; value: number }[]; color: string; }) { return ( {title} {description} ); } function OverviewRangeSwitch({ hours, onHoursChange, }: { hours: OverviewRangeHours; onHoursChange: (hours: OverviewRangeHours) => void; }) { return (
{ if (!value) return; onHoursChange(Number.parseInt(value, 10) as OverviewRangeHours); }} variant='outline' size='sm' className='justify-end' > {OVERVIEW_RANGE_OPTIONS.map((option) => ( {option.label} ))}
); } export function OverviewTab({ data, loading, error, hours, onHoursChange, onRetry, }: { data?: AccessLogOverview; loading: boolean; error: Error | null; hours: OverviewRangeHours; onHoursChange: (hours: OverviewRangeHours) => void; onRetry: () => void; }) { return (
{loading ? ( ) : error ? ( ) : !data ? ( ) : ( )}
); } function OverviewContent({ data, hours, }: { data: AccessLogOverview; hours: number; }) { 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); return ( <>
Requests over time 观察请求量是否出现异常抬升或回落。
({ label: item.key, value: item.value, }))} /> ({ label: item.key, value: item.value, }))} /> ({ label: item.key, value: item.value, }))} />
); }