diff --git a/backend/controller/agent.go b/backend/controller/agent.go index 2e23bdf..40a114e 100644 --- a/backend/controller/agent.go +++ b/backend/controller/agent.go @@ -61,29 +61,30 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) { // 存储到数据库 report := model.AgentReport{ - Hostname: req.Hostname, - CPUUsage: req.CPU.UsagePercent, - CPUIOwait: req.CPU.IOWait, - CPUCores: req.CPU.CoreCount, - Load1: req.CPU.Load1, - Load5: req.CPU.Load5, - Load15: req.CPU.Load15, - MemTotal: req.Memory.TotalGB, - MemUsed: req.Memory.UsedGB, - MemUsage: req.Memory.UsagePercent, + Hostname: req.Hostname, + IP: strings.TrimSpace(req.IP), // 使用 Agent 自报 IP,避免反向代理地址误关联。 // Logan.Li + CPUUsage: req.CPU.UsagePercent, + CPUIOwait: req.CPU.IOWait, + CPUCores: req.CPU.CoreCount, + Load1: req.CPU.Load1, + Load5: req.CPU.Load5, + Load15: req.CPU.Load15, + MemTotal: req.Memory.TotalGB, + MemUsed: req.Memory.UsedGB, + MemUsage: req.Memory.UsagePercent, MemAvailable: req.Memory.AvailableGB, MemSwapUsed: req.Memory.SwapUsedGB, - DiskInfo: string(diskJSON), - PortInfo: string(portJSON), - NetInfo: string(netJSON), - DiskIOInfo: string(dioJSON), + DiskInfo: string(diskJSON), + PortInfo: string(portJSON), + NetInfo: string(netJSON), + DiskIOInfo: string(dioJSON), // v2.0 新增 SwapInfo: string(swapJSON), ProcessTopInfo: string(processTopJSON), FDInfo: string(fdJSON), CSInfo: string(csJSON), HeartbeatInfo: string(heartbeatJSON), - ReportTime: reportTime, + ReportTime: reportTime, } if err := h.db.Create(&report).Error; err != nil { @@ -99,9 +100,9 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) { for _, d := range req.Disks { totalDiskGB += d.TotalGB } - memGB := math.Ceil(req.Memory.TotalGB) // 31.34 → 32, 62.79 → 63(物理内存向上取整) - memMB := int64(memGB * 1024) // GB → MB - diskGB := int64(totalDiskGB + 0.5) // 39.99 → 40 + memGB := math.Ceil(req.Memory.TotalGB) // 31.34 → 32, 62.79 → 63(物理内存向上取整) + memMB := int64(memGB * 1024) // GB → MB + diskGB := int64(totalDiskGB + 0.5) // 39.99 → 40 clientIP := c.ClientIP() @@ -277,7 +278,7 @@ func (h *AgentHandler) writeHotMetric(c *gin.Context, key string, value float64, func (h *AgentHandler) GetLatestReports(c *gin.Context) { var results []model.AgentLatestResponse threeMinAgo := time.Now().Add(-3 * time.Minute) - // 以 servers 表为主,LEFT JOIN 最近 3 分钟内的 Agent 上报 + // 以 servers 表为主,按资产名或 Agent 自报 IP 关联最近 3 分钟内的上报。 // Logan.Li // 未装 Agent 的主机也会显示(agent_status='not_installed'),打破"不显示→无法安装"的循环 h.db.Raw(` SELECT @@ -302,7 +303,8 @@ func (h *AgentHandler) GetLatestReports(c *gin.Context) { a.report_time, COALESCE(s.agent_version, '') as agent_version, CASE - WHEN a.hostname IS NOT NULL THEN COALESCE(s.agent_status, 'online') + WHEN a.id IS NOT NULL THEN 'online' + WHEN COALESCE(s.agent_version, '') <> '' OR COALESCE(s.agent_status, '') = 'online' THEN 'offline' ELSE 'not_installed' END as agent_status, COALESCE(s.os, '') as os, @@ -312,14 +314,13 @@ func (h *AgentHandler) GetLatestReports(c *gin.Context) { COALESCE(r.name, '') as region, CASE WHEN s.password != '' OR s.ssh_key != '' THEN true ELSE false END as has_auth FROM asset_servers s - LEFT JOIN ( - SELECT * FROM monitor_agent_reports - WHERE id IN ( - SELECT MAX(id) FROM monitor_agent_reports - WHERE report_time >= ? - GROUP BY hostname - ) - ) a ON a.hostname = s.hostname + LEFT JOIN monitor_agent_reports a ON a.id = ( + SELECT ar.id FROM monitor_agent_reports ar + WHERE ar.report_time >= ? + AND (ar.hostname = s.hostname OR (ar.ip <> '' AND ar.ip = s.ip)) + ORDER BY ar.report_time DESC, ar.id DESC + LIMIT 1 + ) LEFT JOIN asset_regions r ON r.id = s.region_id ORDER BY a.report_time DESC, s.id ASC `, threeMinAgo).Scan(&results) @@ -409,8 +410,16 @@ func (h *AgentHandler) GetHostHistory(c *gin.Context) { return } + var server model.Server + serverIP := "" + if h.db.Where("hostname = ?", hostname).Take(&server).Error == nil { + serverIP = strings.TrimSpace(server.IP) + } + var reports []model.AgentReport - h.db.Where("hostname = ? AND report_time >= ? AND report_time <= ?", hostname, since, until). + // 资产名可能与操作系统主机名不同,优先兼容 Agent 自报 IP。 // Logan.Li + h.db.Where("hostname = ? OR (ip <> '' AND ip = ?)", hostname, serverIP). + Where("report_time >= ? AND report_time <= ?", since, until). Order("report_time ASC"). Find(&reports) diff --git a/backend/model/agent.go b/backend/model/agent.go index 8808160..ff27fe5 100644 --- a/backend/model/agent.go +++ b/backend/model/agent.go @@ -6,33 +6,34 @@ import ( // AgentReport Agent 上报的原始指标数据 // Logan.Li type AgentReport struct { - ID uint `gorm:"primarykey" json:"id"` - Hostname string `gorm:"size:255;index;not null" json:"hostname"` - CPUUsage float64 `gorm:"type:decimal(6,2)" json:"cpu_usage"` - CPUIOwait float64 `gorm:"type:decimal(6,2)" json:"cpu_iowait"` - CPUCores int `json:"cpu_cores"` - Load1 float64 `gorm:"type:decimal(8,2)" json:"load1"` - Load5 float64 `gorm:"type:decimal(8,2)" json:"load5"` - Load15 float64 `gorm:"type:decimal(8,2)" json:"load15"` - MemTotal float64 `gorm:"type:decimal(10,2)" json:"mem_total"` - MemUsed float64 `gorm:"type:decimal(10,2)" json:"mem_used"` - MemUsage float64 `gorm:"type:decimal(6,2)" json:"mem_usage"` - MemAvailable float64 `gorm:"type:decimal(10,2)" json:"mem_available"` - MemSwapUsed float64 `gorm:"type:decimal(10,2)" json:"mem_swap_used"` - DiskInfo string `gorm:"type:text" json:"disk_info"` - PortInfo string `gorm:"type:text" json:"port_info"` - NetInfo string `gorm:"type:text" json:"net_info"` - DiskIOInfo string `gorm:"type:text" json:"disk_io_info"` + ID uint `gorm:"primarykey" json:"id"` + Hostname string `gorm:"size:255;index;not null" json:"hostname"` + IP string `gorm:"size:64;index" json:"ip"` // Agent 上报的本机 IP,用于资产别名关联。 // Logan.Li + CPUUsage float64 `gorm:"type:decimal(6,2)" json:"cpu_usage"` + CPUIOwait float64 `gorm:"type:decimal(6,2)" json:"cpu_iowait"` + CPUCores int `json:"cpu_cores"` + Load1 float64 `gorm:"type:decimal(8,2)" json:"load1"` + Load5 float64 `gorm:"type:decimal(8,2)" json:"load5"` + Load15 float64 `gorm:"type:decimal(8,2)" json:"load15"` + MemTotal float64 `gorm:"type:decimal(10,2)" json:"mem_total"` + MemUsed float64 `gorm:"type:decimal(10,2)" json:"mem_used"` + MemUsage float64 `gorm:"type:decimal(6,2)" json:"mem_usage"` + MemAvailable float64 `gorm:"type:decimal(10,2)" json:"mem_available"` + MemSwapUsed float64 `gorm:"type:decimal(10,2)" json:"mem_swap_used"` + DiskInfo string `gorm:"type:text" json:"disk_info"` + PortInfo string `gorm:"type:text" json:"port_info"` + NetInfo string `gorm:"type:text" json:"net_info"` + DiskIOInfo string `gorm:"type:text" json:"disk_io_info"` // ──── v2.0 新增字段 ──── - SwapInfo string `gorm:"type:text" json:"swap_info"` // Swap JSON - ProcessTopInfo string `gorm:"type:text" json:"process_top_info"` // 进程 TOP JSON - FDInfo string `gorm:"type:text" json:"fd_info"` // FD 句柄 JSON - CSInfo string `gorm:"type:text" json:"cs_info"` // 上下文切换 JSON - HeartbeatInfo string `gorm:"type:text" json:"heartbeat_info"` // Agent 心跳 JSON + SwapInfo string `gorm:"type:text" json:"swap_info"` // Swap JSON + ProcessTopInfo string `gorm:"type:text" json:"process_top_info"` // 进程 TOP JSON + FDInfo string `gorm:"type:text" json:"fd_info"` // FD 句柄 JSON + CSInfo string `gorm:"type:text" json:"cs_info"` // 上下文切换 JSON + HeartbeatInfo string `gorm:"type:text" json:"heartbeat_info"` // Agent 心跳 JSON // ────────────────────────── - ReportTime time.Time `json:"report_time"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ReportTime time.Time `json:"report_time"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (AgentReport) TableName() string { @@ -41,20 +42,20 @@ func (AgentReport) TableName() string { // AgentLatestResponse Agent 最新状态(含服务器元信息) type AgentLatestResponse struct { - ServerID uint `json:"server_id"` - Hostname string `json:"hostname"` - IP string `json:"ip"` - CPUUsage float64 `json:"cpu_usage"` - CPUCores int `json:"cpu_cores"` - MemUsage float64 `json:"mem_usage"` - MemTotal float64 `json:"mem_total"` - MemUsed float64 `json:"mem_used"` - DiskUsage float64 `json:"disk_usage"` - DiskTotal int64 `json:"disk_total"` - DiskInfo string `json:"disk_info"` - PortInfo string `json:"port_info"` - NetInfo string `json:"net_info"` - DiskIOInfo string `json:"disk_io_info"` + ServerID uint `json:"server_id"` + Hostname string `json:"hostname"` + IP string `json:"ip"` + CPUUsage float64 `json:"cpu_usage"` + CPUCores int `json:"cpu_cores"` + MemUsage float64 `json:"mem_usage"` + MemTotal float64 `json:"mem_total"` + MemUsed float64 `json:"mem_used"` + DiskUsage float64 `json:"disk_usage"` + DiskTotal int64 `json:"disk_total"` + DiskInfo string `json:"disk_info"` + PortInfo string `json:"port_info"` + NetInfo string `json:"net_info"` + DiskIOInfo string `json:"disk_io_info"` // ──── v2.0 新增 ──── SwapInfo string `json:"swap_info"` ProcessTopInfo string `json:"process_top_info"` @@ -74,23 +75,23 @@ type AgentLatestResponse struct { // AgentReportRequest Agent 上报请求体 type AgentReportRequest struct { - Hostname string `json:"hostname" binding:"required"` - IP string `json:"ip"` - Timestamp string `json:"timestamp"` - AgentVersion string `json:"agent_version"` - CPU CPUMetric `json:"cpu"` - Memory MemMetric `json:"memory"` - Swap *SwapMetric `json:"swap,omitempty"` - Disks []DiskMetric `json:"disks"` - Ports []PortMetric `json:"ports"` - Network NetworkStatModel `json:"network"` - DiskIO DiskIOStatModel `json:"disk_io"` - System *SystemStatModel `json:"system,omitempty"` + Hostname string `json:"hostname" binding:"required"` + IP string `json:"ip"` + Timestamp string `json:"timestamp"` + AgentVersion string `json:"agent_version"` + CPU CPUMetric `json:"cpu"` + Memory MemMetric `json:"memory"` + Swap *SwapMetric `json:"swap,omitempty"` + Disks []DiskMetric `json:"disks"` + Ports []PortMetric `json:"ports"` + Network NetworkStatModel `json:"network"` + DiskIO DiskIOStatModel `json:"disk_io"` + System *SystemStatModel `json:"system,omitempty"` // ──── v2.0 新增 ──── - ProcessTop []ProcessMetric `json:"process_top,omitempty"` - FD *FDMetric `json:"fd,omitempty"` - ContextSwitches *CSMetric `json:"context_switches,omitempty"` - Heartbeat HeartbeatMetric `json:"heartbeat"` + ProcessTop []ProcessMetric `json:"process_top,omitempty"` + FD *FDMetric `json:"fd,omitempty"` + ContextSwitches *CSMetric `json:"context_switches,omitempty"` + Heartbeat HeartbeatMetric `json:"heartbeat"` ProbeResults []HealthProbeResult `json:"probe_results,omitempty"` // v2.1 健康探针结果 } @@ -198,10 +199,10 @@ type DiskIOStatModel struct { // AgentPortPolicy 主机端口监控策略 // Logan.Li type AgentPortPolicy struct { - ID uint `gorm:"primarykey" json:"id"` - Hostname string `gorm:"size:255;uniqueIndex;not null" json:"hostname"` - PortFilterMode string `gorm:"size:20;default:whitelist;not null" json:"port_filter_mode"` // whitelist / blacklist - PortWhitelist string `gorm:"type:text" json:"port_whitelist"` // JSON 数组,如 [80,443,3306,6379] + ID uint `gorm:"primarykey" json:"id"` + Hostname string `gorm:"size:255;uniqueIndex;not null" json:"hostname"` + PortFilterMode string `gorm:"size:20;default:whitelist;not null" json:"port_filter_mode"` // whitelist / blacklist + PortWhitelist string `gorm:"type:text" json:"port_whitelist"` // JSON 数组,如 [80,443,3306,6379] } func (AgentPortPolicy) TableName() string { @@ -214,4 +215,3 @@ type AgentPortPolicyDTO struct { PortFilterMode string `json:"port_filter_mode"` PortWhitelist []int `json:"port_whitelist"` } - diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4473948..a4f812c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -63,7 +63,6 @@ const TraceManager = lazy(() => import('@/components/monitor/trace/TraceManager' const HostMonitor = lazy(() => import('@/pages/monitor/HostMonitor').then(m => ({ default: m.HostMonitor }))); const GrafanaDashboard = lazy(() => import('@/components/monitor/dashboard/GrafanaDashboard').then(m => ({ default: m.default }))); const MonitorCenter = lazy(() => import('@/pages/monitor/MonitorCenter').then(m => ({ default: m.MonitorCenter }))); -const HostMonitorDashboard = lazy(() => import('@/components/monitor/dashboard/HostMonitorDashboard')); const MetricsExplorer = lazy(() => import('@/pages/monitor/MetricsExplorer').then(m => ({ default: m.MetricsExplorer }))); const DatasourcePage = lazy(() => import('@/components/monitor/datasource/DatasourcePage').then(m => ({ default: m.default }))); const LogCenter = lazy(() => import('@/components/monitor/log-center/LogCenter')); @@ -187,7 +186,7 @@ function App() { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/layout/Breadcrumb.tsx b/frontend/src/components/layout/Breadcrumb.tsx index 7cdf438..7fa6845 100644 --- a/frontend/src/components/layout/Breadcrumb.tsx +++ b/frontend/src/components/layout/Breadcrumb.tsx @@ -50,7 +50,6 @@ const ROUTE_I18N_MAP: Record = { 'datasources': 'breadcrumb.datasources', 'dashboards': 'breadcrumb.dashboards', 'host-monitor': 'breadcrumb.hostMonitor', - 'host-dashboard': 'breadcrumb.hostDashboard', 'alerts': 'breadcrumb.alerts', 'approval': 'breadcrumb.approval', 'approval-resource': 'breadcrumb.approvalResource', diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 152720e..38fd112 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -5,7 +5,6 @@ import { cn } from '@/lib/utils'; import { useThemeStore, useMenuStore } from '@/store'; import { usePermission } from '@/hooks/usePermission'; import { - LayoutDashboard, Server, Cloud, @@ -115,7 +114,6 @@ const menuItems: MenuItem[] = [ children: [ { id: 'grafana', name: 'Dashboards', labelKey: 'menu.grafana', icon: Monitor, path: '/dashboards', permission: 'monitor:alert' }, { id: 'monitor-center', name: 'Resource Monitor', labelKey: 'menu.resourceMonitor', icon: Activity, path: '/monitor-center', permission: 'agent:monitor' }, - { id: 'host-dashboard', name: 'Host Dashboard', labelKey: 'menu.hostDashboard', icon: LayoutDashboard, path: '/host-dashboard', permission: 'agent:monitor' }, { id: 'trace', name: 'Tracing', labelKey: 'menu.trace', icon: GitBranch, path: '/trace', permission: 'monitor:alert' }, { id: 'alert-center', name: 'Alert Center', labelKey: 'menu.alertCenter', icon: Bell, path: '/alert-center', permission: 'monitor:alert' }, { id: 'datasources', name: 'Datasources', labelKey: 'menu.datasources', icon: Database, path: '/datasources', permission: 'monitor:alert' }, diff --git a/frontend/src/components/monitor/dashboard/HostMonitorDashboard.tsx b/frontend/src/components/monitor/dashboard/HostMonitorDashboard.tsx deleted file mode 100644 index 52a2b14..0000000 --- a/frontend/src/components/monitor/dashboard/HostMonitorDashboard.tsx +++ /dev/null @@ -1,497 +0,0 @@ -// 主机监控大屏 — Grafana 风格:统计卡片 + 资源条状图 + 可排序列表 // Logan.Li -// 参照: Grafana node-dashboard.json (表格+时序) / kubernetes-dashboard.json (bargauge+阈值) -import { useState, useEffect, useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; -import { - - Server, - Cpu, - HardDrive, - Activity, - Monitor, - Bell, - ArrowUpDown, - ArrowUp, - ArrowDown, - RefreshCw, - Search, - ChevronLeft, - ChevronRight, - Wifi, - WifiOff, -} from 'lucide-react'; - -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Badge } from '@/components/ui/badge'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { - Select, SelectContent, SelectItem, SelectTrigger, SelectValue -} from '@/components/ui/select'; -import { serverApi } from '@/api'; -import type { JsonData } from '@/types/common'; - -type SortField = 'hostname' | 'ip' | 'agent_version' | 'agent_status' | 'cpu_usage' | 'memory_usage' | 'disk_usage' | 'region'; -type SortDir = 'asc' | 'desc'; - -function getUsageBar(val: number) { - const v = val || 0; - const color = v > 85 ? 'bg-red-500' : v > 60 ? 'bg-amber-500' : 'bg-emerald-500'; - return ( -
-
-
-
- {v.toFixed(0)}% -
- ); -} - -export default function HostMonitorDashboard() { - const { t } = useTranslation(['monitor', 'common']); - const navigate = useNavigate(); - const [hosts, setHosts] = useState([]); - const [total, setTotal] = useState(0); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - const [search, setSearch] = useState(''); - const [loading, setLoading] = useState(true); - const [refreshing, setRefreshing] = useState(false); - - // 排序状态 - const [sortField, setSortField] = useState('hostname'); - const [sortDir, setSortDir] = useState('asc'); - - // 统计 - const [stats, setStats] = useState<{ online: number; offline: number; total: number }>({ online: 0, offline: 0, total: 0 }); - - const loadData = useCallback(async (isRefresh = false) => { - if (isRefresh) setRefreshing(true); - else setLoading(true); - try { - const params: JsonData = { page, page_size: pageSize }; - if (search.trim()) params.keyword = search.trim(); - const res = await serverApi.getServers(params); - const list = (res as JsonData)?.list || []; - setHosts(list); - setTotal((res as JsonData)?.total || 0); - - // 统计 - const online = list.filter((h: JsonData) => h.agent_status === 'online').length; - setStats({ online, offline: list.length - online, total: (res as JsonData)?.total || 0 }); - } catch { - setHosts([]); - setTotal(0); - } finally { - setLoading(false); - setRefreshing(false); - } - }, [page, pageSize, search]); - - useEffect(() => { loadData(); }, [loadData]); - - // 排序后的数据 - const sorted = [...hosts].sort((a: JsonData, b: JsonData) => { - let va: JsonData | undefined, vb: JsonData | undefined; - if (sortField === 'region') { - va = (a.region?.name || a.region?.Name || '').toLowerCase(); - vb = (b.region?.name || b.region?.Name || '').toLowerCase(); - } else if (sortField === 'agent_status') { - va = a.agent_status || 'offline'; - vb = b.agent_status || 'offline'; - } else if (sortField === 'agent_version') { - va = a.agent_version || ''; - vb = b.agent_version || ''; - } else { - va = a[sortField] || ''; - vb = b[sortField] || ''; - } - - if (typeof va === 'number' && typeof vb === 'number') { - return sortDir === 'asc' ? va - vb : vb - va; - } - const sa = String(va).toLowerCase(); - const sb = String(vb).toLowerCase(); - if (sa < sb) return sortDir === 'asc' ? -1 : 1; - if (sa > sb) return sortDir === 'asc' ? 1 : -1; - return 0; - }); - - const handleSort = (field: SortField) => { - if (sortField === field) { - setSortDir(d => d === 'asc' ? 'desc' : 'asc'); - } else { - setSortField(field); - setSortDir('asc'); - } - }; - - const SortIcon = ({ field }: { field: SortField }) => { - if (sortField !== field) return ; - return sortDir === 'asc' - ? - : ; - }; - - const totalPages = Math.ceil(total / pageSize); - - const handleSearch = () => { - setPage(1); - loadData(); - }; - - // 状态标签颜色 - const statusBadge = (status: string) => { - if (status === 'online') return {t('common:online')}; - if (status === 'running') return {t('common:running')}; - if (status === 'stopped') return {t('common:stopped')}; - return {status || '--'}; - }; - - return ( -
- {/* 统计卡片 — Grafana Quick Facts 风格 (参照 JVM Dashboard) */} -
- - -
-
-
{stats.total}
-
{t('monitor:hostDashboard.hostTotal')}
-
-
-
- - -
-
-
{stats.online}
-
{t('monitor:hostDashboard.agentOnline')}
-
-
-
- - -
-
-
{stats.offline}
-
{t('common:offline')}
-
-
-
- {(() => { - const onlines = hosts.filter((h: JsonData) => h.agent_status === 'online'); - const avgCpu = onlines.length > 0 ? onlines.reduce((s: number, h: JsonData) => s + (h.cpu_usage || 0), 0) / onlines.length : 0; - const avgMem = onlines.length > 0 ? onlines.reduce((s: number, h: JsonData) => s + (h.memory_usage || 0), 0) / onlines.length : 0; - const cpuColor = avgCpu > 85 ? 'dg-text-red' : avgCpu > 60 ? 'dg-text-yellow' : 'dg-text-green'; - const memColor = avgMem > 85 ? 'dg-text-red' : avgMem > 60 ? 'dg-text-yellow' : 'dg-text-green'; - return (<> - - -
-
-
{avgCpu.toFixed(1)}%
-
{t('monitor:hostDashboard.avgCpu')}
-
-
-
- - -
-
-
{avgMem.toFixed(1)}%
-
{t('monitor:hostDashboard.avgMemory')}
-
-
-
- ); - })()} -
- - {/* 资源条状图 — Grafana Bargauge 风格 (参照 K8s Node CPU/Memory Ratio) */} - {hosts.filter((h: JsonData) => h.agent_status === 'online').length > 0 && ( -
- {/* CPU 排行 */} - - - - - {t('monitor:hostDashboard.cpuTop5')} - {t('monitor:hostDashboard.threshold')} - - - - {[...hosts] - .filter((h: JsonData) => h.agent_status === 'online') - .sort((a: JsonData, b: JsonData) => (b.cpu_usage || 0) - (a.cpu_usage || 0)) - .slice(0, 5) - .map((h: JsonData) => { - const val = h.cpu_usage || 0; - return ( -
- {h.hostname} -
-
= 85 - ? 'linear-gradient(90deg, #ef4444, #f87171)' - : val >= 60 - ? 'linear-gradient(90deg, #f59e0b, #fbbf24)' - : 'linear-gradient(90deg, #10b981, #34d399)' - }} - /> -
- = 85 ? 'dg-text-red' : val >= 60 ? 'dg-text-yellow' : 'dg-text-green' - }`}>{val.toFixed(1)}% -
- ); - })} - - - - {/* 内存排行 */} - - - - - {t('monitor:hostDashboard.memTop5')} - {t('monitor:hostDashboard.threshold')} - - - - {[...hosts] - .filter((h: JsonData) => h.agent_status === 'online') - .sort((a: JsonData, b: JsonData) => (b.memory_usage || 0) - (a.memory_usage || 0)) - .slice(0, 5) - .map((h: JsonData) => { - const val = h.memory_usage || 0; - return ( -
- {h.hostname} -
-
= 85 - ? 'linear-gradient(90deg, #ef4444, #f87171)' - : val >= 60 - ? 'linear-gradient(90deg, #f59e0b, #fbbf24)' - : 'linear-gradient(90deg, #10b981, #34d399)' - }} - /> -
- = 85 ? 'dg-text-red' : val >= 60 ? 'dg-text-yellow' : 'dg-text-green' - }`}>{val.toFixed(1)}% -
- ); - })} - - -
- )} - - {/* 表格卡片 */} - - -
-
- - {t('monitor:hostDashboard.listTitle')} - {t('monitor:hostDashboard.realtime')} -
-
-
- - setSearch(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleSearch()} - /> -
- -
-
-
- - {loading ? ( -
- -

{t('common:loading')}

-
- ) : sorted.length === 0 ? ( -
- -

{t('monitor:hostDashboard.noData')}

-

{t('monitor:hostDashboard.noDataDesc')}

-
- ) : ( - <> -
- - - - {[ - { key: 'hostname', label: t('monitor:hostDashboard.colHostId'), w: '' }, - { key: 'agent_version', label: t('monitor:hostDashboard.colAgentVersion'), w: 'w-[100px]' }, - { key: 'agent_status', label: t('monitor:hostDashboard.colAgentStatus'), w: 'w-[90px]' }, - { key: 'cpu_usage', label: 'CPU', w: 'w-[130px]' }, - { key: 'memory_usage', label: t('monitor:hostMetric.memory'), w: 'w-[130px]' }, - { key: 'disk_usage', label: t('monitor:hostMetric.disk'), w: 'w-[130px]' }, - { key: 'region', label: t('monitor:hostDashboard.colRegion'), w: 'w-[120px]' }, - { key: 'ip', label: t('monitor:hostDashboard.colIp'), w: 'w-[140px]' }, - ].map(col => ( - - ))} - - - - - {sorted.map((h: JsonData) => ( - - {/* 主机名/ID */} - - {/* Agent 版本 */} - - {/* Agent 状态 */} - - {/* CPU 使用率 */} - - {/* 内存使用率 */} - - {/* 磁盘使用率 */} - - {/* 所在区域 */} - - {/* IP 地址 */} - - {/* 操作栏 */} - - - ))} - -
handleSort(col.key as SortField)} - > -
- {col.label} - -
-
{t('common:actions')}
-
-
-
-
{h.hostname}
-
ID: {h.id}
-
-
-
- {h.agent_version || '--'} - - {statusBadge(h.agent_status)} - -
- - {getUsageBar(h.cpu_usage)} -
-
-
- - {getUsageBar(h.memory_usage)} -
-
-
- - {getUsageBar(h.disk_usage)} -
-
- - {h.region?.name || h.region?.Name || (h.region ? h.region : '--')} - - - {h.ip || '--'} - -
- - -
-
-
- - {/* 分页 */} -
-
- {t('monitor:hostDashboard.perPage')} - - {t('monitor:hostDashboard.totalCount', { count: total })} -
-
- - - {page} / {Math.max(totalPages, 1)} - - -
-
- - )} -
-
-
- ); -} diff --git a/frontend/src/components/monitor/dashboard/grafana-dashboard/AgentTab.tsx b/frontend/src/components/monitor/dashboard/grafana-dashboard/AgentTab.tsx index d7a427d..2bea752 100644 --- a/frontend/src/components/monitor/dashboard/grafana-dashboard/AgentTab.tsx +++ b/frontend/src/components/monitor/dashboard/grafana-dashboard/AgentTab.tsx @@ -14,6 +14,8 @@ import { Trash2, Wifi, WifiOff, + Cpu, + HardDrive, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -140,6 +142,25 @@ export const AgentTab: React.FC = () => { const selectedHosts = useMemo(() => hosts.filter((h) => selected.has(String(h.server_id))), [hosts, selected]); const needInstall = useMemo(() => selectedHosts.filter((h) => h.agent_status !== 'online'), [selectedHosts]); const needUpgrade = useMemo(() => selectedHosts.filter((h) => h.agent_status === 'online'), [selectedHosts]); + // 全量概览基于接口完整结果,不受列表分页和当前排序影响。 // Logan.Li + const summary = useMemo(() => { + const online = hosts.filter((host) => host.agent_status === 'online'); + const offline = hosts.filter((host) => host.agent_status === 'offline'); + const notInstalled = hosts.filter((host) => host.agent_status === 'not_installed'); + const average = (field: 'cpu_usage' | 'mem_usage') => + online.length + ? online.reduce((total, host) => total + Number(host[field] || 0), 0) / online.length + : 0; + + return { + total: hosts.length, + online: online.length, + offline: offline.length, + notInstalled: notInstalled.length, + avgCPU: average('cpu_usage'), + avgMemory: average('mem_usage'), + }; + }, [hosts]); const openBatchDialog = () => { setBatchAck(false); @@ -338,6 +359,25 @@ export const AgentTab: React.FC = () => { onSubmit={handleBatchSubmit} /> +
+ {[ + { label: i18n.t('monitor.dashboard.agent.overview.totalHosts'), value: summary.total, icon: Server, tone: 'dg-text-blue' }, + { label: i18n.t('monitor.dashboard.agent.overview.online'), value: summary.online, icon: Wifi, tone: 'dg-text-green' }, + { label: i18n.t('monitor.dashboard.agent.overview.offline'), value: summary.offline, icon: WifiOff, tone: 'dg-text-orange' }, + { label: i18n.t('monitor.dashboard.agent.overview.notInstalled'), value: summary.notInstalled, icon: PackageOpen, tone: 'dg-text-yellow' }, + { label: i18n.t('monitor.dashboard.agent.overview.avgCpu'), value: `${summary.avgCPU.toFixed(1)}%`, icon: Cpu, tone: 'dg-text-blue' }, + { label: i18n.t('monitor.dashboard.agent.overview.avgMemory'), value: `${summary.avgMemory.toFixed(1)}%`, icon: HardDrive, tone: 'dg-text-purple' }, + ].map(({ label, value, icon: Icon, tone }) => ( +
+
+ {label} + +
+
{value}
+
+ ))} +
+
diff --git a/frontend/src/locales/en-US.json b/frontend/src/locales/en-US.json index 4d8c5da..5d333cd 100644 --- a/frontend/src/locales/en-US.json +++ b/frontend/src/locales/en-US.json @@ -4110,6 +4110,14 @@ "batchUninstall": "Batch Uninstall", "deploying": "Deploying", "chart": "Chart", + "overview": { + "totalHosts": "Total Hosts", + "online": "Online", + "offline": "Offline", + "notInstalled": "Not Installed", + "avgCpu": "Average CPU", + "avgMemory": "Average Memory" + }, "toast": { "installed": "Agent installation submitted, running in background…", "installFailed": "Installation failed", diff --git a/frontend/src/locales/zh-CN.json b/frontend/src/locales/zh-CN.json index ea8feff..099f19d 100644 --- a/frontend/src/locales/zh-CN.json +++ b/frontend/src/locales/zh-CN.json @@ -4110,6 +4110,14 @@ "batchUninstall": "批量卸载", "deploying": "部署中", "chart": "图表", + "overview": { + "totalHosts": "主机总数", + "online": "在线", + "offline": "离线", + "notInstalled": "未安装", + "avgCpu": "平均 CPU", + "avgMemory": "平均内存" + }, "toast": { "installed": "Agent 安装任务已提交,后台执行中…", "installFailed": "安装失败",