From ed38aa1d795276457939cf219e4d616701d62d59 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 18 Mar 2026 15:37:14 +0800 Subject: [PATCH] =?UTF-8?q?[=E5=8A=9F=E8=83=BD]=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BB=AA=E8=A1=A8=E6=9D=BF=E6=A6=82=E8=A7=88=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E7=BB=93=E6=9E=84=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=8E=8B=E7=BC=A9?= =?UTF-8?q?=E5=92=8C=E8=A7=84=E8=8C=83=E5=8C=96=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openflare_agent/agent.json | 2 +- openflare_server/controller/dashboard.go | 150 +++++++++++- openflare_server/router/api_phase2_test.go | 4 +- .../web/features/dashboard/api/overview.ts | 219 ++++++++++++++++-- .../web/features/dashboard/types.ts | 52 +++++ .../tests/unit/dashboard-overview.test.tsx | 145 ++++++------ 6 files changed, 482 insertions(+), 90 deletions(-) diff --git a/openflare_agent/agent.json b/openflare_agent/agent.json index c32dee4b..7ab7d46f 100644 --- a/openflare_agent/agent.json +++ b/openflare_agent/agent.json @@ -1,6 +1,6 @@ { "server_url": "http://127.0.0.1:3000", - "agent_token": "89d0efbf7bcc8fa53fe48889dce4d045", + "agent_token": "2380de64b00e99093e16590beb91e1a0", "data_dir": "./data", "openresty_container_name": "openflare-openresty", "openresty_docker_image": "openresty/openresty:alpine", diff --git a/openflare_server/controller/dashboard.go b/openflare_server/controller/dashboard.go index 9792109b..8dab9d82 100644 --- a/openflare_server/controller/dashboard.go +++ b/openflare_server/controller/dashboard.go @@ -6,6 +6,29 @@ import ( "github.com/gin-gonic/gin" ) +type dashboardOverviewPayload struct { + GeneratedAt any `json:"generated_at"` + Summary service.DashboardSummary `json:"summary"` + Traffic service.DashboardTraffic `json:"traffic"` + Capacity service.DashboardCapacity `json:"capacity"` + Distributions dashboardDistributionsPayload `json:"distributions"` + Trends dashboardTrendsPayload `json:"trends"` + Nodes [][]any `json:"nodes"` +} + +type dashboardDistributionsPayload struct { + StatusCodes [][]any `json:"status_codes"` + TopDomains [][]any `json:"top_domains"` + SourceCountries [][]any `json:"source_countries"` +} + +type dashboardTrendsPayload struct { + Traffic24h [][]any `json:"traffic_24h"` + Capacity24h [][]any `json:"capacity_24h"` + Network24h [][]any `json:"network_24h"` + DiskIO24h [][]any `json:"disk_io_24h"` +} + // GetDashboardOverview godoc // @Summary Get dashboard overview // @Tags Dashboard @@ -20,5 +43,130 @@ func GetDashboardOverview(c *gin.Context) { respondFailure(c, err.Error()) return } - respondSuccess(c, view) + respondSuccess(c, compressDashboardOverview(view)) +} + +func compressDashboardOverview(view *service.DashboardOverviewView) *dashboardOverviewPayload { + if view == nil { + return &dashboardOverviewPayload{ + Distributions: dashboardDistributionsPayload{ + StatusCodes: [][]any{}, + TopDomains: [][]any{}, + SourceCountries: [][]any{}, + }, + Trends: dashboardTrendsPayload{ + Traffic24h: [][]any{}, + Capacity24h: [][]any{}, + Network24h: [][]any{}, + DiskIO24h: [][]any{}, + }, + Nodes: [][]any{}, + } + } + return &dashboardOverviewPayload{ + GeneratedAt: view.GeneratedAt, + Summary: view.Summary, + Traffic: view.Traffic, + Capacity: view.Capacity, + Distributions: dashboardDistributionsPayload{ + StatusCodes: compressDistributionItems(view.Distributions.StatusCodes), + TopDomains: compressDistributionItems(view.Distributions.TopDomains), + SourceCountries: compressDistributionItems(view.Distributions.SourceCountries), + }, + Trends: dashboardTrendsPayload{ + Traffic24h: compressTrafficTrendPoints(view.Trends.Traffic24h), + Capacity24h: compressCapacityTrendPoints(view.Trends.Capacity24h), + Network24h: compressNetworkTrendPoints(view.Trends.Network24h), + DiskIO24h: compressDiskIOTrendPoints(view.Trends.DiskIO24h), + }, + Nodes: compressDashboardNodes(view.Nodes), + } +} + +func compressDistributionItems(items []service.DistributionItem) [][]any { + rows := make([][]any, 0, len(items)) + for _, item := range items { + rows = append(rows, []any{item.Key, item.Value}) + } + return rows +} + +func compressTrafficTrendPoints(points []service.TrafficTrendPoint) [][]any { + rows := make([][]any, 0, len(points)) + for _, point := range points { + rows = append(rows, []any{ + point.BucketStartedAt, + point.RequestCount, + point.ErrorCount, + point.UniqueVisitorCount, + }) + } + return rows +} + +func compressCapacityTrendPoints(points []service.CapacityTrendPoint) [][]any { + rows := make([][]any, 0, len(points)) + for _, point := range points { + rows = append(rows, []any{ + point.BucketStartedAt, + point.AverageCPUUsagePercent, + point.AverageMemoryUsagePercent, + point.ReportedNodes, + }) + } + return rows +} + +func compressNetworkTrendPoints(points []service.NetworkTrendPoint) [][]any { + rows := make([][]any, 0, len(points)) + for _, point := range points { + rows = append(rows, []any{ + point.BucketStartedAt, + point.NetworkRxBytes, + point.NetworkTxBytes, + point.OpenrestyRxBytes, + point.OpenrestyTxBytes, + point.ReportedNodes, + }) + } + return rows +} + +func compressDiskIOTrendPoints(points []service.DiskIOTrendPoint) [][]any { + rows := make([][]any, 0, len(points)) + for _, point := range points { + rows = append(rows, []any{ + point.BucketStartedAt, + point.DiskReadBytes, + point.DiskWriteBytes, + point.ReportedNodes, + }) + } + return rows +} + +func compressDashboardNodes(nodes []service.DashboardNodeHealth) [][]any { + rows := make([][]any, 0, len(nodes)) + for _, node := range nodes { + rows = append(rows, []any{ + node.ID, + node.NodeID, + node.Name, + node.GeoName, + node.GeoLatitude, + node.GeoLongitude, + node.Status, + node.OpenrestyStatus, + node.CurrentVersion, + node.LastSeenAt, + node.ActiveEventCount, + node.CPUUsagePercent, + node.MemoryUsagePercent, + node.StorageUsagePercent, + node.RequestCount, + node.ErrorCount, + node.UniqueVisitorCount, + }) + } + return rows } diff --git a/openflare_server/router/api_phase2_test.go b/openflare_server/router/api_phase2_test.go index 659b1384..b17b6faf 100644 --- a/openflare_server/router/api_phase2_test.go +++ b/openflare_server/router/api_phase2_test.go @@ -170,7 +170,9 @@ func TestPhase2AgentLifecycle(t *testing.T) { createRouteAndPublishVersion(t, engine, adminToken) dashboardResp := performJSONRequest(t, engine, adminToken, http.MethodGet, "/api/dashboard/overview", nil) - var dashboard service.DashboardOverviewView + var dashboard struct { + Summary service.DashboardSummary `json:"summary"` + } decodeResponseData(t, dashboardResp, &dashboard) if dashboard.Summary.TotalNodes != 0 { t.Fatalf("expected empty dashboard node summary before node registration, got %+v", dashboard.Summary) diff --git a/openflare_server/web/features/dashboard/api/overview.ts b/openflare_server/web/features/dashboard/api/overview.ts index ffba93f0..17d2c5da 100644 --- a/openflare_server/web/features/dashboard/api/overview.ts +++ b/openflare_server/web/features/dashboard/api/overview.ts @@ -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(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 | null | undefined, +): DistributionItem[] { + return arrayOrEmpty(items).map((item) => + isCompactDistributionItem(item) + ? { key: String(item[0] ?? ''), value: Number(item[1] ?? 0) } + : item, + ); +} + +function normalizeTrafficTrendPoints( + items: + | Array + | 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 | 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('/dashboard/overview'); + const overview = await apiRequest( + '/dashboard/overview', + ); return normalizeDashboardOverview(overview); } diff --git a/openflare_server/web/features/dashboard/types.ts b/openflare_server/web/features/dashboard/types.ts index 944cf24a..a84e1139 100644 --- a/openflare_server/web/features/dashboard/types.ts +++ b/openflare_server/web/features/dashboard/types.ts @@ -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[]; +} diff --git a/openflare_server/web/tests/unit/dashboard-overview.test.tsx b/openflare_server/web/tests/unit/dashboard-overview.test.tsx index de80acad..a4986149 100644 --- a/openflare_server/web/tests/unit/dashboard-overview.test.tsx +++ b/openflare_server/web/tests/unit/dashboard-overview.test.tsx @@ -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();