feat(rate-limits): use 5m trend buckets and 24h/7d ranges

Overview API accepts bucket_minutes (5|60); rate-limit analysis uses
5-minute buckets and drops 15d/30d presets. Widen rank value column.
This commit is contained in:
ryan
2026-07-19 19:18:15 +08:00
parent 86d2d6b0ad
commit b56290d79d
10 changed files with 90 additions and 39 deletions
@@ -9,6 +9,9 @@ export type SearchDraft = {
export type OverviewRangeHours = 24 | 168 | 360 | 720;
/** 限流分析等短窗口场景:仅 24 小时 / 7 天 */
export type RateLimitRangeHours = 24 | 168;
export const PAGE_SIZE_OPTIONS = [20, 50, 100, 200];
export const OVERVIEW_RANGE_OPTIONS: {
@@ -21,6 +24,14 @@ export const OVERVIEW_RANGE_OPTIONS: {
{ value: 720, label: '30 天' },
];
export const RATE_LIMIT_RANGE_OPTIONS: {
value: RateLimitRangeHours;
label: string;
}[] = [
{ value: 24, label: '24 小时' },
{ value: 168, label: '7 天' },
];
export const DETAIL_SORT_OPTIONS = [
{ value: 'logged_at:desc', label: '时间从新到旧' },
{ value: 'logged_at:asc', label: '时间从旧到新' },
@@ -24,7 +24,9 @@ import { cn } from '@/lib/utils';
import {
OVERVIEW_RANGE_OPTIONS,
RATE_LIMIT_RANGE_OPTIONS,
type OverviewRangeHours,
type RateLimitRangeHours,
} from './access-log-utils';
type ManagedZoneDomains = {
@@ -309,11 +311,13 @@ export function OverviewToolbar({
hosts,
onHoursChange,
onHostsChange,
rangeOptions = OVERVIEW_RANGE_OPTIONS,
}: {
hours: OverviewRangeHours;
hours: OverviewRangeHours | RateLimitRangeHours;
hosts: string[];
onHoursChange: (hours: OverviewRangeHours) => void;
onHostsChange: (hosts: string[]) => void;
rangeOptions?: ReadonlyArray<{ value: number; label: string }>;
}) {
return (
<div className='flex flex-wrap items-center justify-end gap-2'>
@@ -334,7 +338,7 @@ export function OverviewToolbar({
size='sm'
className='justify-end'
>
{OVERVIEW_RANGE_OPTIONS.map((option) => (
{rangeOptions.map((option) => (
<ToggleGroupItem
key={option.value}
value={String(option.value)}
@@ -347,3 +351,5 @@ export function OverviewToolbar({
</div>
);
}
export { RATE_LIMIT_RANGE_OPTIONS };
@@ -13,7 +13,8 @@ import type {
import {
formatOverviewRangeHint,
type OverviewRangeHours,
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';
@@ -50,9 +51,9 @@ export function AnalysisTab({
data?: AccessLogOverview;
loading: boolean;
error: Error | null;
hours: OverviewRangeHours;
hours: RateLimitRangeHours;
hosts: string[];
onHoursChange: (hours: OverviewRangeHours) => void;
onHoursChange: (hours: RateLimitRangeHours) => void;
onHostsChange: (hosts: string[]) => void;
onRetry: () => void;
}) {
@@ -65,8 +66,9 @@ export function AnalysisTab({
<OverviewToolbar
hours={hours}
hosts={hosts}
onHoursChange={onHoursChange}
onHoursChange={(next) => onHoursChange(next as RateLimitRangeHours)}
onHostsChange={onHostsChange}
rangeOptions={RATE_LIMIT_RANGE_OPTIONS}
/>
{loading ? (
@@ -14,7 +14,7 @@ import {
type OverviewRangeHours,
} from '../../access-logs/components/access-log-utils';
const BUCKET_SECONDS = 3600;
const DEFAULT_BUCKET_SECONDS = 300;
const RPS_COLOR = '#38bdf8';
const VISITS_COLOR = '#a78bfa';
@@ -58,6 +58,11 @@ type RatePressureChartProps = {
};
export function RatePressureChart({ data, hours }: RatePressureChartProps) {
const bucketSeconds =
(data?.bucket_minutes && data.bucket_minutes > 0
? data.bucket_minutes
: 5) * 60 || DEFAULT_BUCKET_SECONDS;
const rangeLabel = useMemo(() => {
const end = data?.generated_at ? new Date(data.generated_at) : new Date();
const start = new Date(end.getTime() - hours * 3600 * 1000);
@@ -82,12 +87,12 @@ export function RatePressureChart({ data, hours }: RatePressureChartProps) {
const time = req?.bucket_started_at ?? visit?.bucket_started_at ?? '';
rawTimes.push(time);
labels.push(formatAxisTime(time, hours));
rpsValues.push((req?.value ?? 0) / BUCKET_SECONDS);
rpsValues.push((req?.value ?? 0) / bucketSeconds);
visitValues.push(visit?.value ?? 0);
}
return { labels, rpsValues, visitValues, rawTimes };
}, [data?.trends.requests, data?.trends.visits, hours]);
}, [bucketSeconds, data?.trends.requests, data?.trends.visits, hours]);
const option = useMemo<EChartsOption>(() => {
const rpsMax = calculateNiceAxisMax(chartModel.rpsValues);
+4 -3
View File
@@ -11,7 +11,7 @@ import { LoadingStateWithBorder } from '@/components/layout/loading';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { AccessLogService } from '@/lib/services/openflare';
import type { OverviewRangeHours } from '../access-logs/components/access-log-utils';
import type { RateLimitRangeHours } from '../access-logs/components/access-log-utils';
import { AnalysisTab } from './components/analysis-tab';
import { ConfigTab } from './components/config-tab';
@@ -27,15 +27,16 @@ function RateLimitsPageContent() {
const router = useRouter();
const searchParams = useSearchParams();
const tab = resolveTab(searchParams.get('tab'));
const [hours, setHours] = useState<OverviewRangeHours>(24);
const [hours, setHours] = useState<RateLimitRangeHours>(24);
const [hosts, setHosts] = useState<string[]>([]);
const overviewQuery = useQuery({
queryKey: ['openflare', 'rate-limits', 'overview', hours, hosts],
queryKey: ['openflare', 'rate-limits', 'overview', hours, hosts, 5],
queryFn: () =>
AccessLogService.getOverview({
hours,
hosts: hosts.length > 0 ? hosts : undefined,
bucket_minutes: 5,
}),
enabled: !!user?.is_admin && tab === 'analysis',
});
+1 -1
View File
@@ -82,7 +82,7 @@ export function RankChart({
}}
/>
</div>
<span className='w-14 text-right tabular-nums text-foreground/80 text-[13px] font-medium'>
<span className='min-w-14 max-w-28 text-right tabular-nums text-foreground/80 text-[13px] font-medium'>
{valueFormatter(item.value)}
</span>
</div>
+2
View File
@@ -505,6 +505,7 @@ export interface AccessLogOverviewFilters {
host?: string;
hosts?: string[];
hours?: number;
bucket_minutes?: number;
}
export interface AccessLogOverviewMetricPoint {
@@ -515,6 +516,7 @@ export interface AccessLogOverviewMetricPoint {
export interface AccessLogOverview {
generated_at: string;
hours: number;
bucket_minutes?: number;
summary: {
total_requests: number;
total_visits: number;