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
@@ -4,6 +4,10 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
import { DashboardOverview } from '@/features/dashboard/components/dashboard-overview';
vi.mock('echarts-for-react', () => ({
default: () => <div data-testid="echarts-mock" />,
}));
describe('DashboardOverview', () => {
afterEach(() => {
vi.unstubAllGlobals();
@@ -15,80 +19,86 @@ describe('DashboardOverview', () => {
vi.fn((input: RequestInfo | URL) => {
const url = String(input);
if (url.includes('/nodes/')) {
if (url.includes('/dashboard/overview')) {
return Promise.resolve(
new Response(
JSON.stringify({
success: true,
message: '',
data: [{ id: 1, status: 'online', auto_update_enabled: true }],
}),
),
);
}
if (url.includes('/config-versions/')) {
return Promise.resolve(
new Response(
JSON.stringify({
success: true,
message: '',
data: [{ id: 1, version: '20260311-001', is_active: true, created_at: '2026-03-11T10:00:00Z' }],
}),
),
);
}
if (url.includes('/managed-domains/')) {
return Promise.resolve(
new Response(
JSON.stringify({
success: true,
message: '',
data: [{ id: 1, domain: '*.example.com', enabled: true, cert_id: 1 }],
}),
),
);
}
if (url.includes('/tls-certificates/')) {
return Promise.resolve(
new Response(
JSON.stringify({
success: true,
message: '',
data: [
{
id: 1,
name: 'example',
not_after: '2026-04-01T00:00:00Z',
updated_at: '2026-03-10T10:00:00Z',
data: {
generated_at: '2026-03-14T08:00:00Z',
summary: {
total_nodes: 2,
online_nodes: 2,
offline_nodes: 0,
pending_nodes: 0,
unhealthy_nodes: 1,
active_alerts: 1,
lagging_nodes: 1,
},
],
}),
),
);
}
if (url.includes('/proxy-routes/')) {
return Promise.resolve(
new Response(
JSON.stringify({
success: true,
message: '',
data: [{ id: 1, enabled: true, enable_https: true, custom_headers: '[{"key":"X-Test","value":"1"}]' }],
}),
),
);
}
if (url.includes('/user/search')) {
return Promise.resolve(
new Response(
JSON.stringify({
success: true,
message: '',
data: [{ id: 1, role: 100, status: 1, username: 'root' }],
traffic: {
request_count: 900,
unique_visitors: 200,
error_count: 36,
estimated_qps: 15,
reported_nodes: 2,
},
capacity: {
average_cpu_usage_percent: 68.5,
average_memory_usage_percent: 71.8,
high_cpu_nodes: 1,
high_memory_nodes: 1,
high_storage_nodes: 1,
},
config: {
active_version: '20260314-001',
lagging_nodes: 1,
pending_nodes: 0,
},
trends: {
traffic_24h: Array.from({ length: 24 }, (_, index) => ({
bucket_started_at: `2026-03-13T${String(index).padStart(2, '0')}:00:00Z`,
request_count: index * 10,
error_count: index,
unique_visitor_count: index * 3,
})),
capacity_24h: Array.from({ length: 24 }, (_, index) => ({
bucket_started_at: `2026-03-13T${String(index).padStart(2, '0')}:00:00Z`,
average_cpu_usage_percent: index,
average_memory_usage_percent: index + 10,
reported_nodes: 2,
})),
},
nodes: [
{
id: 1,
node_id: 'node-a',
name: 'edge-a',
status: 'online',
openresty_status: 'healthy',
current_version: '20260314-001',
last_seen_at: '2026-03-14T08:00:00Z',
active_event_count: 0,
cpu_usage_percent: 45,
memory_usage_percent: 50,
storage_usage_percent: 60,
request_count: 600,
error_count: 6,
unique_visitor_count: 120,
},
],
active_alerts: [
{
node_id: 'node-b',
node_name: 'edge-b',
event_type: 'openresty_unhealthy',
severity: 'critical',
message: 'reload failed',
last_triggered_at: '2026-03-14T07:59:00Z',
status: 'active',
},
],
},
}),
),
);
@@ -112,8 +122,8 @@ describe('DashboardOverview', () => {
</QueryClientProvider>,
);
expect(screen.getByText('控制台仪表盘')).toBeInTheDocument();
expect(await screen.findByText('节点摘要')).toBeInTheDocument();
expect(await screen.findByText('用户概览')).toBeInTheDocument();
expect(await screen.findByText('系统运行总览')).toBeInTheDocument();
expect(await screen.findByText('24 小时请求趋势')).toBeInTheDocument();
expect(await screen.findByText('节点健康列表')).toBeInTheDocument();
});
});