'use client'; import {useMemo, useState} from 'react'; import {useQuery} from '@tanstack/react-query'; import {Activity, HardDrive, Link2, ShieldCheck, Users} from 'lucide-react'; import {Area, AreaChart, CartesianGrid, XAxis, YAxis} from 'recharts'; import {Button} from '@/components/ui/button'; import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@/components/ui/card'; import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig, } from '@/components/ui/chart'; import {Skeleton} from '@/components/ui/skeleton'; import { ZoneService, zoneQueryKey, type ZoneOverview, type ZoneStatsRange, } from '@/lib/services/openflare'; import {formatDateTime} from '@/lib/utils'; import {formatBytes, formatCompactNumber} from '@/lib/utils/metrics'; import {cn} from '@/lib/utils'; const rangeOptions: Array<{value: ZoneStatsRange; label: string}> = [ {value: '24h', label: '24 小时'}, {value: '7d', label: '7 天'}, {value: '30d', label: '30 天'}, ]; const visitorsChartConfig = { value: {label: '唯一访问者', color: 'hsl(217 91% 60%)'}, } satisfies ChartConfig; const requestsChartConfig = { value: {label: '请求总数', color: 'hsl(217 91% 60%)'}, } satisfies ChartConfig; const bytesChartConfig = { value: {label: '已提供数据', color: 'hsl(217 91% 60%)'}, } satisfies ChartConfig; function formatAxisLabel(iso: string, range: ZoneStatsRange) { const date = new Date(iso); if (Number.isNaN(date.getTime())) { return ''; } if (range === '24h') { return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: undefined, hour12: true, }); } return date.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', }); } function formatWindowLabel(startedAt?: string, endedAt?: string) { if (!startedAt || !endedAt) { return '—'; } const start = new Date(startedAt); const end = new Date(endedAt); if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) { return '—'; } const fmt = (value: Date) => value .toLocaleDateString('en-GB', {day: 'numeric', month: 'long'}) .toUpperCase(); return `${fmt(start)} — ${fmt(end)}`; } export function ZoneOverviewPanel({ overview, zoneId, }: { overview: ZoneOverview; zoneId: number; }) { const [range, setRange] = useState('24h'); const boundRoutes = new Set( overview.domains.flatMap((domain) => domain.proxy_route_id ? [domain.proxy_route_id] : [], ), ).size; const certificates = overview.domains.filter( (domain) => domain.cert_id !== null, ).length; const statsQuery = useQuery({ queryKey: [...zoneQueryKey, zoneId, 'stats', range], queryFn: () => ZoneService.getStats(zoneId, range), enabled: zoneId > 0, }); const stats = statsQuery.data; const chartData = useMemo( () => (stats?.series ?? []).map((point) => ({ label: formatAxisLabel(point.bucket_started_at, range), at: point.bucket_started_at, visitors: point.unique_visitors, requests: point.request_count, bytes: point.bytes_sent, })), [range, stats?.series], ); return (
{rangeOptions.map((option) => ( ))}

{formatWindowLabel(stats?.window_started_at, stats?.window_ended_at)}

{statsQuery.isLoading ? (
{Array.from({length: 3}).map((_, index) => ( ))}
) : statsQuery.isError ? (
加载流量统计失败
) : (
{!stats?.available ? (

分析存储暂不可用,图表可能为空。请确认 ClickHouse 已启用。

) : null} formatBytes(value, {zeroText: '0 B'})} />
)}
Zone 信息

根域

{overview.zone.domain}

创建时间

{formatDateTime(overview.zone.created_at)}

); } function MetricTrendCard({ icon: Icon, label, value, data, dataKey, config, gradientId, valueFormatter, }: { icon: typeof Users; label: string; value: string; data: Array>; dataKey: string; config: ChartConfig; gradientId: string; valueFormatter?: (value: number) => string; }) { return (
{label}

{value}

valueFormatter ? valueFormatter(Number(tick)) : formatCompactNumber(Number(tick)) } /> { const numeric = Number(raw); return valueFormatter ? valueFormatter(numeric) : formatCompactNumber(numeric); }} /> } />
); }