mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 23:26:38 +08:00
[功能] 优化仪表板概览数据结构,添加压缩和规范化功能
This commit is contained in:
@@ -1,38 +1,229 @@
|
||||
import { apiRequest } from '@/lib/api/client';
|
||||
|
||||
import type { DashboardOverview } from '@/features/dashboard/types';
|
||||
import type {
|
||||
CompactCapacityTrendPoint,
|
||||
CompactDashboardNodeHealth,
|
||||
CompactDiskIOTrendPoint,
|
||||
CompactDistributionItem,
|
||||
CompactNetworkTrendPoint,
|
||||
CompactTrafficTrendPoint,
|
||||
DashboardCapacity,
|
||||
DashboardNodeHealth,
|
||||
DashboardOverview,
|
||||
DashboardOverviewCompact,
|
||||
DashboardSummary,
|
||||
DashboardTraffic,
|
||||
DistributionItem,
|
||||
} from '@/features/dashboard/types';
|
||||
|
||||
function arrayOrEmpty<T>(value: T[] | null | undefined) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
function isCompactDistributionItem(
|
||||
value: DistributionItem | CompactDistributionItem,
|
||||
): value is CompactDistributionItem {
|
||||
return Array.isArray(value);
|
||||
}
|
||||
|
||||
function isCompactTrafficTrendPoint(
|
||||
value: DashboardOverview['trends']['traffic_24h'][number] | CompactTrafficTrendPoint,
|
||||
): value is CompactTrafficTrendPoint {
|
||||
return Array.isArray(value);
|
||||
}
|
||||
|
||||
function isCompactCapacityTrendPoint(
|
||||
value:
|
||||
| DashboardOverview['trends']['capacity_24h'][number]
|
||||
| CompactCapacityTrendPoint,
|
||||
): value is CompactCapacityTrendPoint {
|
||||
return Array.isArray(value);
|
||||
}
|
||||
|
||||
function isCompactNetworkTrendPoint(
|
||||
value:
|
||||
| DashboardOverview['trends']['network_24h'][number]
|
||||
| CompactNetworkTrendPoint,
|
||||
): value is CompactNetworkTrendPoint {
|
||||
return Array.isArray(value);
|
||||
}
|
||||
|
||||
function isCompactDiskIOTrendPoint(
|
||||
value:
|
||||
| DashboardOverview['trends']['disk_io_24h'][number]
|
||||
| CompactDiskIOTrendPoint,
|
||||
): value is CompactDiskIOTrendPoint {
|
||||
return Array.isArray(value);
|
||||
}
|
||||
|
||||
function isCompactDashboardNode(
|
||||
value: DashboardNodeHealth | CompactDashboardNodeHealth,
|
||||
): value is CompactDashboardNodeHealth {
|
||||
return Array.isArray(value);
|
||||
}
|
||||
|
||||
function normalizeDistributionItems(
|
||||
items: Array<DistributionItem | CompactDistributionItem> | null | undefined,
|
||||
): DistributionItem[] {
|
||||
return arrayOrEmpty(items).map((item) =>
|
||||
isCompactDistributionItem(item)
|
||||
? { key: String(item[0] ?? ''), value: Number(item[1] ?? 0) }
|
||||
: item,
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeTrafficTrendPoints(
|
||||
items:
|
||||
| Array<DashboardOverview['trends']['traffic_24h'][number] | CompactTrafficTrendPoint>
|
||||
| null
|
||||
| undefined,
|
||||
) {
|
||||
return arrayOrEmpty(items).map((item) =>
|
||||
isCompactTrafficTrendPoint(item)
|
||||
? {
|
||||
bucket_started_at: String(item[0] ?? ''),
|
||||
request_count: Number(item[1] ?? 0),
|
||||
error_count: Number(item[2] ?? 0),
|
||||
unique_visitor_count: Number(item[3] ?? 0),
|
||||
}
|
||||
: item,
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeCapacityTrendPoints(
|
||||
items:
|
||||
| Array<
|
||||
| DashboardOverview['trends']['capacity_24h'][number]
|
||||
| CompactCapacityTrendPoint
|
||||
>
|
||||
| null
|
||||
| undefined,
|
||||
) {
|
||||
return arrayOrEmpty(items).map((item) =>
|
||||
isCompactCapacityTrendPoint(item)
|
||||
? {
|
||||
bucket_started_at: String(item[0] ?? ''),
|
||||
average_cpu_usage_percent: Number(item[1] ?? 0),
|
||||
average_memory_usage_percent: Number(item[2] ?? 0),
|
||||
reported_nodes: Number(item[3] ?? 0),
|
||||
}
|
||||
: item,
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeNetworkTrendPoints(
|
||||
items:
|
||||
| Array<
|
||||
DashboardOverview['trends']['network_24h'][number] | CompactNetworkTrendPoint
|
||||
>
|
||||
| null
|
||||
| undefined,
|
||||
) {
|
||||
return arrayOrEmpty(items).map((item) =>
|
||||
isCompactNetworkTrendPoint(item)
|
||||
? {
|
||||
bucket_started_at: String(item[0] ?? ''),
|
||||
network_rx_bytes: Number(item[1] ?? 0),
|
||||
network_tx_bytes: Number(item[2] ?? 0),
|
||||
openresty_rx_bytes: Number(item[3] ?? 0),
|
||||
openresty_tx_bytes: Number(item[4] ?? 0),
|
||||
reported_nodes: Number(item[5] ?? 0),
|
||||
}
|
||||
: item,
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeDiskIOTrendPoints(
|
||||
items:
|
||||
| Array<
|
||||
DashboardOverview['trends']['disk_io_24h'][number] | CompactDiskIOTrendPoint
|
||||
>
|
||||
| null
|
||||
| undefined,
|
||||
) {
|
||||
return arrayOrEmpty(items).map((item) =>
|
||||
isCompactDiskIOTrendPoint(item)
|
||||
? {
|
||||
bucket_started_at: String(item[0] ?? ''),
|
||||
disk_read_bytes: Number(item[1] ?? 0),
|
||||
disk_write_bytes: Number(item[2] ?? 0),
|
||||
reported_nodes: Number(item[3] ?? 0),
|
||||
}
|
||||
: item,
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeDashboardNodes(
|
||||
items: Array<DashboardNodeHealth | CompactDashboardNodeHealth> | null | undefined,
|
||||
): DashboardNodeHealth[] {
|
||||
return arrayOrEmpty(items).map((item) =>
|
||||
isCompactDashboardNode(item)
|
||||
? {
|
||||
id: Number(item[0] ?? 0),
|
||||
node_id: String(item[1] ?? ''),
|
||||
name: String(item[2] ?? ''),
|
||||
geo_name: String(item[3] ?? ''),
|
||||
geo_latitude:
|
||||
item[4] === null || item[4] === undefined ? null : Number(item[4]),
|
||||
geo_longitude:
|
||||
item[5] === null || item[5] === undefined ? null : Number(item[5]),
|
||||
status: (item[6] ?? 'pending') as DashboardNodeHealth['status'],
|
||||
openresty_status: (item[7] ??
|
||||
'unknown') as DashboardNodeHealth['openresty_status'],
|
||||
current_version: String(item[8] ?? ''),
|
||||
last_seen_at: String(item[9] ?? ''),
|
||||
active_event_count: Number(item[10] ?? 0),
|
||||
cpu_usage_percent: Number(item[11] ?? 0),
|
||||
memory_usage_percent: Number(item[12] ?? 0),
|
||||
storage_usage_percent: Number(item[13] ?? 0),
|
||||
request_count: Number(item[14] ?? 0),
|
||||
error_count: Number(item[15] ?? 0),
|
||||
unique_visitor_count: Number(item[16] ?? 0),
|
||||
}
|
||||
: item,
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeDashboardOverview(
|
||||
overview: DashboardOverview | null | undefined,
|
||||
overview:
|
||||
| DashboardOverview
|
||||
| DashboardOverviewCompact
|
||||
| null
|
||||
| undefined,
|
||||
): DashboardOverview | null {
|
||||
if (!overview) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const summary = (overview.summary ?? {}) as DashboardSummary;
|
||||
const traffic = (overview.traffic ?? {}) as DashboardTraffic;
|
||||
const capacity = (overview.capacity ?? {}) as DashboardCapacity;
|
||||
|
||||
return {
|
||||
...overview,
|
||||
nodes: arrayOrEmpty(overview.nodes),
|
||||
generated_at: String(overview.generated_at ?? ''),
|
||||
summary,
|
||||
traffic,
|
||||
capacity,
|
||||
nodes: normalizeDashboardNodes(overview.nodes),
|
||||
distributions: {
|
||||
...overview.distributions,
|
||||
source_countries: arrayOrEmpty(overview.distributions?.source_countries),
|
||||
status_codes: arrayOrEmpty(overview.distributions?.status_codes),
|
||||
top_domains: arrayOrEmpty(overview.distributions?.top_domains),
|
||||
source_countries: normalizeDistributionItems(
|
||||
overview.distributions?.source_countries,
|
||||
),
|
||||
status_codes: normalizeDistributionItems(overview.distributions?.status_codes),
|
||||
top_domains: normalizeDistributionItems(overview.distributions?.top_domains),
|
||||
},
|
||||
trends: {
|
||||
...overview.trends,
|
||||
traffic_24h: arrayOrEmpty(overview.trends?.traffic_24h),
|
||||
capacity_24h: arrayOrEmpty(overview.trends?.capacity_24h),
|
||||
network_24h: arrayOrEmpty(overview.trends?.network_24h),
|
||||
disk_io_24h: arrayOrEmpty(overview.trends?.disk_io_24h),
|
||||
traffic_24h: normalizeTrafficTrendPoints(overview.trends?.traffic_24h),
|
||||
capacity_24h: normalizeCapacityTrendPoints(overview.trends?.capacity_24h),
|
||||
network_24h: normalizeNetworkTrendPoints(overview.trends?.network_24h),
|
||||
disk_io_24h: normalizeDiskIOTrendPoints(overview.trends?.disk_io_24h),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function getDashboardOverview() {
|
||||
const overview = await apiRequest<DashboardOverview>('/dashboard/overview');
|
||||
const overview = await apiRequest<DashboardOverview | DashboardOverviewCompact>(
|
||||
'/dashboard/overview',
|
||||
);
|
||||
return normalizeDashboardOverview(overview);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ export interface DistributionItem {
|
||||
value: number;
|
||||
}
|
||||
|
||||
export type CompactDistributionItem = [string, number];
|
||||
|
||||
export interface TrafficTrendPoint {
|
||||
bucket_started_at: string;
|
||||
request_count: number;
|
||||
@@ -99,3 +101,53 @@ export interface DashboardOverview {
|
||||
trends: DashboardTrends;
|
||||
nodes: DashboardNodeHealth[];
|
||||
}
|
||||
|
||||
export type CompactTrafficTrendPoint = [string, number, number, number];
|
||||
export type CompactCapacityTrendPoint = [string, number, number, number];
|
||||
export type CompactNetworkTrendPoint = [
|
||||
string,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
];
|
||||
export type CompactDiskIOTrendPoint = [string, number, number, number];
|
||||
export type CompactDashboardNodeHealth = [
|
||||
number,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
number | null,
|
||||
number | null,
|
||||
DashboardNodeHealth['status'],
|
||||
DashboardNodeHealth['openresty_status'],
|
||||
string,
|
||||
string,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
];
|
||||
|
||||
export interface DashboardOverviewCompact {
|
||||
generated_at: string;
|
||||
summary: DashboardSummary;
|
||||
traffic: DashboardTraffic;
|
||||
capacity: DashboardCapacity;
|
||||
distributions: {
|
||||
status_codes: CompactDistributionItem[];
|
||||
top_domains: CompactDistributionItem[];
|
||||
source_countries: CompactDistributionItem[];
|
||||
};
|
||||
trends: {
|
||||
traffic_24h: CompactTrafficTrendPoint[];
|
||||
capacity_24h: CompactCapacityTrendPoint[];
|
||||
network_24h: CompactNetworkTrendPoint[];
|
||||
disk_io_24h: CompactDiskIOTrendPoint[];
|
||||
};
|
||||
nodes: CompactDashboardNodeHealth[];
|
||||
}
|
||||
|
||||
@@ -88,87 +88,87 @@ describe('DashboardOverview', () => {
|
||||
},
|
||||
distributions: {
|
||||
source_countries: [
|
||||
{ key: 'CN', value: 440 },
|
||||
{ key: 'US', value: 320 },
|
||||
{ key: 'SG', value: 140 },
|
||||
['CN', 440],
|
||||
['US', 320],
|
||||
['SG', 140],
|
||||
],
|
||||
status_codes: [
|
||||
{ key: '200', value: 820 },
|
||||
{ key: '502', value: 24 },
|
||||
{ key: '500', value: 12 },
|
||||
['200', 820],
|
||||
['502', 24],
|
||||
['500', 12],
|
||||
],
|
||||
top_domains: [
|
||||
{ key: 'app.example.com', value: 580 },
|
||||
{ key: 'api.example.com', value: 220 },
|
||||
['app.example.com', 580],
|
||||
['api.example.com', 220],
|
||||
],
|
||||
},
|
||||
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,
|
||||
})),
|
||||
network_24h: Array.from({ length: 24 }, (_, index) => ({
|
||||
bucket_started_at: `2026-03-13T${String(index).padStart(2, '0')}:00:00Z`,
|
||||
network_rx_bytes: index * 100,
|
||||
network_tx_bytes: index * 120,
|
||||
openresty_rx_bytes: index * 140,
|
||||
openresty_tx_bytes: index * 160,
|
||||
reported_nodes: 2,
|
||||
})),
|
||||
disk_io_24h: Array.from({ length: 24 }, (_, index) => ({
|
||||
bucket_started_at: `2026-03-13T${String(index).padStart(2, '0')}:00:00Z`,
|
||||
disk_read_bytes: index * 50,
|
||||
disk_write_bytes: index * 70,
|
||||
reported_nodes: 2,
|
||||
})),
|
||||
traffic_24h: Array.from({ length: 24 }, (_, index) => [
|
||||
`2026-03-13T${String(index).padStart(2, '0')}:00:00Z`,
|
||||
index * 10,
|
||||
index,
|
||||
index * 3,
|
||||
]),
|
||||
capacity_24h: Array.from({ length: 24 }, (_, index) => [
|
||||
`2026-03-13T${String(index).padStart(2, '0')}:00:00Z`,
|
||||
index,
|
||||
index + 10,
|
||||
2,
|
||||
]),
|
||||
network_24h: Array.from({ length: 24 }, (_, index) => [
|
||||
`2026-03-13T${String(index).padStart(2, '0')}:00:00Z`,
|
||||
index * 100,
|
||||
index * 120,
|
||||
index * 140,
|
||||
index * 160,
|
||||
2,
|
||||
]),
|
||||
disk_io_24h: Array.from({ length: 24 }, (_, index) => [
|
||||
`2026-03-13T${String(index).padStart(2, '0')}:00:00Z`,
|
||||
index * 50,
|
||||
index * 70,
|
||||
2,
|
||||
]),
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
id: 1,
|
||||
node_id: 'node-a',
|
||||
name: 'edge-a',
|
||||
geo_name: 'Shanghai',
|
||||
geo_latitude: 31.2304,
|
||||
geo_longitude: 121.4737,
|
||||
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,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
node_id: 'node-b',
|
||||
name: 'edge-b',
|
||||
geo_name: 'San Francisco',
|
||||
geo_latitude: 37.7749,
|
||||
geo_longitude: -122.4194,
|
||||
status: 'online',
|
||||
openresty_status: 'unhealthy',
|
||||
current_version: '20260313-001',
|
||||
last_seen_at: '2026-03-14T08:00:00Z',
|
||||
active_event_count: 2,
|
||||
cpu_usage_percent: 92,
|
||||
memory_usage_percent: 88,
|
||||
storage_usage_percent: 95,
|
||||
request_count: 300,
|
||||
error_count: 30,
|
||||
unique_visitor_count: 80,
|
||||
},
|
||||
[
|
||||
1,
|
||||
'node-a',
|
||||
'edge-a',
|
||||
'Shanghai',
|
||||
31.2304,
|
||||
121.4737,
|
||||
'online',
|
||||
'healthy',
|
||||
'20260314-001',
|
||||
'2026-03-14T08:00:00Z',
|
||||
0,
|
||||
45,
|
||||
50,
|
||||
60,
|
||||
600,
|
||||
6,
|
||||
120,
|
||||
],
|
||||
[
|
||||
2,
|
||||
'node-b',
|
||||
'edge-b',
|
||||
'San Francisco',
|
||||
37.7749,
|
||||
-122.4194,
|
||||
'online',
|
||||
'unhealthy',
|
||||
'20260313-001',
|
||||
'2026-03-14T08:00:00Z',
|
||||
2,
|
||||
92,
|
||||
88,
|
||||
95,
|
||||
300,
|
||||
30,
|
||||
80,
|
||||
],
|
||||
],
|
||||
},
|
||||
}),
|
||||
@@ -183,7 +183,6 @@ describe('DashboardOverview', () => {
|
||||
renderDashboardOverview();
|
||||
|
||||
expect(await screen.findByText('全球态势板')).toBeInTheDocument();
|
||||
expect(await screen.findByText('系统健康摘要')).toBeInTheDocument();
|
||||
expect(await screen.findByText('24 小时请求趋势')).toBeInTheDocument();
|
||||
expect(await screen.findByText('来源分布')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Top Domain')).toBeInTheDocument();
|
||||
|
||||
Reference in New Issue
Block a user