feat: add 24-hour traffic and capacity trend charts to dashboard and node detail pages

- Implemented TrendChart component for visualizing traffic and capacity trends.
- Enhanced DashboardOverview and NodeDetailPage components to display 24-hour request and capacity trends.
- Updated types to include traffic and capacity trend data structures.
- Created observability trends service to aggregate traffic and capacity data.
- Added tests for traffic report generation and trend data handling.
- Introduced new dependencies for charting (echarts and echarts-for-react).
This commit is contained in:
ryan
2026-03-14 11:48:05 +08:00
parent e94132a7d9
commit 16bb9e5879
25 changed files with 1063 additions and 109 deletions
@@ -3,6 +3,7 @@
import Link from 'next/link';
import { useQuery } from '@tanstack/react-query';
import { TrendChart } from '@/components/data/trend-chart';
import { EmptyState } from '@/components/feedback/empty-state';
import { ErrorState } from '@/components/feedback/error-state';
import { LoadingState } from '@/components/feedback/loading-state';
@@ -32,6 +33,14 @@ function getErrorMessage(error: unknown) {
return error instanceof Error ? error.message : '请求失败,请稍后重试。';
}
function formatTrendHour(value: string) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return '—';
}
return `${date.getHours().toString().padStart(2, '0')}:00`;
}
function getAlertVariant(
alert: DashboardAlert,
): 'success' | 'warning' | 'danger' | 'info' {
@@ -215,6 +224,68 @@ export function DashboardOverview() {
</div>
</AppCard>
<div className="grid gap-6 xl:grid-cols-2">
<AppCard
title="24 小时请求趋势"
description="观察整体请求量和错误量是否出现异常抬升。"
>
<TrendChart
labels={overview.trends.traffic_24h.map((point) =>
formatTrendHour(point.bucket_started_at),
)}
series={[
{
label: '请求量',
color: '#f59e0b',
fillColor: 'rgba(245, 158, 11, 0.18)',
variant: 'area',
values: overview.trends.traffic_24h.map(
(point) => point.request_count,
),
},
{
label: '错误量',
color: '#ef4444',
values: overview.trends.traffic_24h.map(
(point) => point.error_count,
),
},
]}
/>
</AppCard>
<AppCard
title="24 小时容量趋势"
description="按小时聚合 CPU 与内存使用率,判断整体容量是否持续紧张。"
>
<TrendChart
labels={overview.trends.capacity_24h.map((point) =>
formatTrendHour(point.bucket_started_at),
)}
series={[
{
label: '平均 CPU',
color: '#0f766e',
fillColor: 'rgba(15, 118, 110, 0.15)',
variant: 'area',
values: overview.trends.capacity_24h.map(
(point) => point.average_cpu_usage_percent,
),
valueFormatter: formatPercent,
},
{
label: '平均内存',
color: '#2563eb',
values: overview.trends.capacity_24h.map(
(point) => point.average_memory_usage_percent,
),
valueFormatter: formatPercent,
},
]}
/>
</AppCard>
</div>
<div className="grid gap-6 xl:grid-cols-[1.2fr_0.8fr]">
<AppCard
title="节点健康列表"
@@ -30,6 +30,25 @@ export interface DashboardConfig {
pending_nodes: number;
}
export interface TrafficTrendPoint {
bucket_started_at: string;
request_count: number;
error_count: number;
unique_visitor_count: number;
}
export interface CapacityTrendPoint {
bucket_started_at: string;
average_cpu_usage_percent: number;
average_memory_usage_percent: number;
reported_nodes: number;
}
export interface DashboardTrends {
traffic_24h: TrafficTrendPoint[];
capacity_24h: CapacityTrendPoint[];
}
export interface DashboardNodeHealth {
id: number;
node_id: string;
@@ -63,6 +82,7 @@ export interface DashboardOverview {
traffic: DashboardTraffic;
capacity: DashboardCapacity;
config: DashboardConfig;
trends: DashboardTrends;
nodes: DashboardNodeHealth[];
active_alerts: DashboardAlert[];
}
@@ -8,6 +8,7 @@ import { useEffect, useMemo, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { z } from 'zod';
import { TrendChart } from '@/components/data/trend-chart';
import { EmptyState } from '@/components/feedback/empty-state';
import { ErrorState } from '@/components/feedback/error-state';
import { InlineMessage } from '@/components/feedback/inline-message';
@@ -143,6 +144,14 @@ function formatUptime(seconds?: number | null) {
return `${minutes} 分钟`;
}
function formatTrendHour(value: string) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return '—';
}
return `${date.getHours().toString().padStart(2, '0')}:00`;
}
function getHealthEventVariant(
event: NodeObservability['health_events'][number],
): 'success' | 'warning' | 'danger' | 'info' {
@@ -829,6 +838,76 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
</AppCard>
</div>
<div className="grid gap-6 xl:grid-cols-2">
<AppCard
title="24 小时请求趋势"
description="按小时聚合该节点的请求量和错误量,判断是否存在突发流量或持续异常。"
>
<TrendChart
labels={
observability?.trends.traffic_24h.map((point) =>
formatTrendHour(point.bucket_started_at),
) ?? []
}
series={[
{
label: '请求量',
color: '#f59e0b',
fillColor: 'rgba(245, 158, 11, 0.18)',
variant: 'area',
values:
observability?.trends.traffic_24h.map(
(point) => point.request_count,
) ?? [],
},
{
label: '错误量',
color: '#ef4444',
values:
observability?.trends.traffic_24h.map(
(point) => point.error_count,
) ?? [],
},
]}
/>
</AppCard>
<AppCard
title="24 小时容量趋势"
description="观察该节点 CPU 与内存使用率在 24 小时内的变化,辅助扩容和排障。"
>
<TrendChart
labels={
observability?.trends.capacity_24h.map((point) =>
formatTrendHour(point.bucket_started_at),
) ?? []
}
series={[
{
label: '平均 CPU',
color: '#0f766e',
fillColor: 'rgba(15, 118, 110, 0.15)',
variant: 'area',
values:
observability?.trends.capacity_24h.map(
(point) => point.average_cpu_usage_percent,
) ?? [],
valueFormatter: formatPercent,
},
{
label: '平均内存',
color: '#2563eb',
values:
observability?.trends.capacity_24h.map(
(point) => point.average_memory_usage_percent,
) ?? [],
valueFormatter: formatPercent,
},
]}
/>
</AppCard>
</div>
<div className="grid gap-4 xl:grid-cols-3">
<AppCard title="更新模式">
<div className="space-y-3">
+20
View File
@@ -99,6 +99,25 @@ export interface NodeTrafficReport {
source_countries_json: string;
}
export interface NodeTrafficTrendPoint {
bucket_started_at: string;
request_count: number;
error_count: number;
unique_visitor_count: number;
}
export interface NodeCapacityTrendPoint {
bucket_started_at: string;
average_cpu_usage_percent: number;
average_memory_usage_percent: number;
reported_nodes: number;
}
export interface NodeObservabilityTrends {
traffic_24h: NodeTrafficTrendPoint[];
capacity_24h: NodeCapacityTrendPoint[];
}
export interface NodeHealthEvent {
event_type: string;
severity: string;
@@ -116,4 +135,5 @@ export interface NodeObservability {
metric_snapshots: NodeMetricSnapshot[];
traffic_reports: NodeTrafficReport[];
health_events: NodeHealthEvent[];
trends: NodeObservabilityTrends;
}