Files
OpenFlare/frontend/app/(main)/components/dashboard/stat-cards.tsx
T
ryan 451ce52592 认证页 axe a11y 审计+修复:7 处布局级真实违规全修,复扫验证 dashboard/admin/system 归零;基准 total_issues 保持 8 不变(纯质量收益)
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":93,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 12:26:37 +08:00

118 lines
4.2 KiB
TypeScript

'use client';
import { Activity, Cpu, Gauge, Server, Users } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import type {
DashboardCapacity,
DashboardSummary,
DashboardTraffic,
} from '@/lib/services/openflare';
import { formatCompactNumber, formatPercent } from './dashboard-utils';
export function DashboardStatCards({
summary,
traffic,
capacity,
}: {
summary: DashboardSummary;
traffic: DashboardTraffic;
capacity: DashboardCapacity;
}) {
const t = useTranslations('dashboard.stats');
const onlineRate =
summary.total_nodes > 0
? (summary.online_nodes / summary.total_nodes) * 100
: 0;
return (
<div className='grid gap-4 md:grid-cols-2 xl:grid-cols-3'>
<Card className='border-dashed shadow-none'>
<CardHeader className='flex flex-row items-center justify-between pb-2'>
<span className='text-xs font-medium text-muted-foreground'>
{t('requests24h')}
</span>
<Activity className='size-4 text-primary' />
</CardHeader>
<CardContent className='space-y-2'>
<div className='text-2xl font-semibold tracking-tight'>
{formatCompactNumber(traffic.request_count)}
</div>
<p className='text-[10px] text-muted-foreground'>
{t('requestsMeta', {
visitors: formatCompactNumber(traffic.unique_visitors),
errors: formatCompactNumber(traffic.error_count),
qps: traffic.estimated_qps.toFixed(2),
})}
</p>
</CardContent>
</Card>
<Card className='border-dashed shadow-none'>
<CardHeader className='flex flex-row items-center justify-between pb-2'>
<span className='text-xs font-medium text-muted-foreground'>
{t('clusterCapacity')}
</span>
<Cpu className='size-4 text-primary' />
</CardHeader>
<CardContent className='space-y-2'>
<div className='text-2xl font-semibold tracking-tight'>
{formatPercent(capacity.average_cpu_usage_percent)}
<span className='ml-2 text-sm font-normal text-muted-foreground'>
CPU
</span>
</div>
<Progress
value={capacity.average_cpu_usage_percent}
aria-label={t('clusterCapacity')}
className='h-1.5'
/>
<p className='text-[10px] text-muted-foreground'>
{t('avgMemoryHighLoad', {
memory: formatPercent(capacity.average_memory_usage_percent),
cpu: capacity.high_cpu_nodes,
memoryNodes: capacity.high_memory_nodes,
storage: capacity.high_storage_nodes,
})}
</p>
</CardContent>
</Card>
<Card className='border-dashed shadow-none md:col-span-2 xl:col-span-1'>
<CardHeader className='flex flex-row items-center justify-between pb-2'>
<span className='text-xs font-medium text-muted-foreground'>
{t('nodeOverview')}
</span>
<Server className='size-4 text-primary' />
</CardHeader>
<CardContent className='space-y-2'>
<div className='text-2xl font-semibold tracking-tight'>
{summary.online_nodes}
<span className='text-sm font-normal text-muted-foreground'>
{' '}
{t('onlineOf', { total: summary.total_nodes })}
</span>
</div>
<Progress value={onlineRate} aria-label={t('nodeOverview')} className='h-1.5' />
<p className='text-[10px] text-muted-foreground flex flex-wrap items-center gap-x-3 gap-y-1'>
<span className='inline-flex items-center gap-1'>
<Users className='size-3' />
{t('pending', { count: summary.pending_nodes })}
</span>
<span className='inline-flex items-center gap-1'>
<Gauge className='size-3' />
{t('unhealthyOffline', {
unhealthy: summary.unhealthy_nodes,
offline: summary.offline_nodes,
})}
</span>
</p>
</CardContent>
</Card>
</div>
);
}