Files
OpenFlare/frontend/app/(main)/rate-limits/components/analysis-tab.tsx
T
ryan 5a8722ff07 feat(frontend): 控制台 next-intl 中英双语
接入无 URL 前缀的 zh-CN/en,顶栏与外观设置可切换语言;选择写入 cookie 后刷新生效。
2026-08-16 16:49:16 +08:00

123 lines
3.4 KiB
TypeScript

'use client';
import { Gauge } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { RankCard } from '@/components/data/rank-card';
import { EmptyStateWithBorder } from '@/components/layout/empty';
import { ErrorInline } from '@/components/layout/error';
import { LoadingStateWithBorder } from '@/components/layout/loading';
import type {
AccessLogOverview,
DistributionItem,
} from '@/lib/services/openflare';
import {
formatOverviewRangeHint,
RATE_LIMIT_RANGE_OPTIONS,
type RateLimitRangeHours,
} from '../../access-logs/components/access-log-utils';
import { OverviewToolbar } from '../../access-logs/components/overview-toolbar';
import { RatePressureChart } from './rate-pressure-chart';
function toAvgRpsItems(items: DistributionItem[] | undefined, hours: number) {
const windowSeconds = Math.max(hours, 1) * 3600;
return (items ?? []).map((item) => ({
label: item.key,
value: item.value / windowSeconds,
}));
}
function formatRps(value: number) {
if (!Number.isFinite(value)) return '—';
if (value >= 100) {
return value.toLocaleString('zh-CN', { maximumFractionDigits: 1 });
}
return value.toLocaleString('zh-CN', {
maximumFractionDigits: 3,
minimumFractionDigits: 0,
});
}
export function AnalysisTab({
data,
loading,
error,
hours,
hosts,
onHoursChange,
onHostsChange,
onRetry,
}: {
data?: AccessLogOverview;
loading: boolean;
error: Error | null;
hours: RateLimitRangeHours;
hosts: string[];
onHoursChange: (hours: RateLimitRangeHours) => void;
onHostsChange: (hosts: string[]) => void;
onRetry: () => void;
}) {
const t = useTranslations('rateLimits');
const tLogs = useTranslations('accessLogs');
const rangeHint = formatOverviewRangeHint(hours, (key, values) =>
tLogs(key, values),
);
const rangeOptions = RATE_LIMIT_RANGE_OPTIONS.map((option) => ({
value: option.value,
label: t(option.labelKey),
}));
const hostItems = toAvgRpsItems(data?.top_hosts, hours);
const ipItems = toAvgRpsItems(data?.top_ips, hours);
return (
<div className='space-y-6'>
<OverviewToolbar
hours={hours}
hosts={hosts}
onHoursChange={(next) => onHoursChange(next as RateLimitRangeHours)}
onHostsChange={onHostsChange}
rangeOptions={rangeOptions}
/>
{loading ? (
<LoadingStateWithBorder
icon={Gauge}
description={t('loadingPressure')}
/>
) : error ? (
<ErrorInline
message={error.message || t('loadFailed')}
onRetry={onRetry}
/>
) : !data ? (
<EmptyStateWithBorder
icon={Gauge}
title={t('emptyTitle')}
description={t('emptyDesc')}
/>
) : (
<>
<RatePressureChart data={data} hours={hours} />
<div className='grid gap-4 lg:grid-cols-2'>
<RankCard
title={t('topHost')}
description={t('avgRate', { range: rangeHint })}
items={hostItems}
color='#38bdf8'
valueFormatter={(value) => `${formatRps(value)} req/s`}
/>
<RankCard
title={t('topIp')}
description={t('avgRate', { range: rangeHint })}
items={ipItems}
color='#a78bfa'
valueFormatter={(value) => `${formatRps(value)} req/s`}
/>
</div>
</>
)}
</div>
);
}