diff --git a/backend/src/api/agents.ts b/backend/src/api/agents.ts index 991e5c7..64c5145 100644 --- a/backend/src/api/agents.ts +++ b/backend/src/api/agents.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import { Bindings } from "../models/db"; -import { Agent } from "../models/agent"; +import { Agent, Metrics } from "../models/agent"; import { getAgents, getAgentDetail, @@ -11,6 +11,7 @@ import { registerAgentService, updateAgentStatusService, getAgentMetrics, + getLatestAgentMetrics, } from "../services/AgentService"; const agents = new Hono<{ @@ -55,11 +56,7 @@ agents.put("/:id", async (c) => { const payload = c.get("jwtPayload"); const updateData = await c.req.json(); - const result = await updateAgentService( - c.env.DB, - agentId, - updateData - ); + const result = await updateAgentService(c.env.DB, agentId, updateData); return c.json( { @@ -76,10 +73,7 @@ agents.delete("/:id", async (c) => { const agentId = Number(c.req.param("id")); const payload = c.get("jwtPayload"); - const result = await deleteAgentService( - c.env.DB, - agentId - ); + const result = await deleteAgentService(c.env.DB, agentId); return c.json( { @@ -165,8 +159,22 @@ agents.get("/:id/metrics", async (c) => { message: "获取客户端指标成功", }, 200 - ) -}) + ); +}); + +// 获取单个客户端的最新指标 +agents.get("/:id/metrics/latest", async (c) => { + const agentId = Number(c.req.param("id")); + const result = await getLatestAgentMetrics(c.env.DB, agentId); + return c.json( + { + success: result.success, + agent: result.results, + message: "获取客户端最新指标成功", + }, + 200 + ); +}); // 获取单个客户端 agents.get("/:id", async (c) => { @@ -184,5 +192,4 @@ agents.get("/:id", async (c) => { ); }); - export { agents }; diff --git a/backend/src/middlewares/auth.ts b/backend/src/middlewares/auth.ts index 45f63e8..27a15b6 100644 --- a/backend/src/middlewares/auth.ts +++ b/backend/src/middlewares/auth.ts @@ -20,7 +20,11 @@ export const jwtMiddleware = async (c: Context, next: Next) => { return next(); } // 获取 metrics 时暂时先不验证。 - if (c.req.path.endsWith("/metrics") && c.req.method === "GET") { + if ( + (c.req.path.endsWith("/metrics") || + c.req.path.endsWith("/metrics/latest")) && + c.req.method === "GET" + ) { return next(); } const middleware = jwt({ diff --git a/backend/src/repositories/agent.ts b/backend/src/repositories/agent.ts index bc19ff7..b93fb20 100644 --- a/backend/src/repositories/agent.ts +++ b/backend/src/repositories/agent.ts @@ -227,3 +227,11 @@ export async function getAgentMetrics(db: Bindings["DB"], agentId: number) { .bind(agentId) .all(); } + +// 获取指定客户端的最新指标 +export async function getLatestAgentMetrics(db: Bindings["DB"], agentId: number) { + return await db + .prepare("SELECT * FROM agent_metrics_24h WHERE agent_id =? ORDER BY timestamp DESC LIMIT 1") + .bind(agentId) + .all(); +} diff --git a/backend/src/services/AgentService.ts b/backend/src/services/AgentService.ts index faf5a96..8e3e8f7 100644 --- a/backend/src/services/AgentService.ts +++ b/backend/src/services/AgentService.ts @@ -475,4 +475,8 @@ export async function setAgentInactive(env: any, id: number) { export async function getAgentMetrics(db: Bindings["DB"], agentId: number) { return await AgentRepository.getAgentMetrics(db, agentId); +} + +export async function getLatestAgentMetrics(db: Bindings["DB"], agentId: number) { + return await AgentRepository.getLatestAgentMetrics(db, agentId); } \ No newline at end of file diff --git a/frontend/src/api/agents.ts b/frontend/src/api/agents.ts index 5a07feb..4b89414 100644 --- a/frontend/src/api/agents.ts +++ b/frontend/src/api/agents.ts @@ -87,3 +87,14 @@ export const getAgentMetrics = async ( }; } }; + +export const getLatestAgentMetrics = async ( + id: number +): Promise<{ + success: boolean; + agent?: MetricHistory; + message?: string; +}> => { + const response = await api.get(`/api/agents/${id}/metrics/latest`); + return response.data; +}; diff --git a/frontend/src/components/AgentCard.tsx b/frontend/src/components/AgentCard.tsx index bdc2d98..6b0dc43 100644 --- a/frontend/src/components/AgentCard.tsx +++ b/frontend/src/components/AgentCard.tsx @@ -1,5 +1,5 @@ import { Box, Card, Flex, Text } from "@radix-ui/themes"; -import { Tabs, TabsContent, TabsList, TabsTrigger,Badge } from "./ui"; +import { Tabs, TabsContent, TabsList, TabsTrigger, Badge } from "./ui"; import { GlobeIcon } from "@radix-ui/react-icons"; import MetricsChart from "./MetricsChart"; import { useTranslation } from "react-i18next"; diff --git a/frontend/src/components/AgentStatusBar.tsx b/frontend/src/components/AgentStatusBar.tsx new file mode 100644 index 0000000..9015265 --- /dev/null +++ b/frontend/src/components/AgentStatusBar.tsx @@ -0,0 +1,216 @@ +import React from "react"; +import { MetricHistory, AgentWithLatestMetrics } from "../types"; +import { Badge } from "./ui/badge"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faWindows, + faLinux, + faApple, + faAndroid, +} from "@fortawesome/free-brands-svg-icons"; +import { faLaptop } from "@fortawesome/free-solid-svg-icons"; + +interface AgentStatusBarProps { + latestMetric?: MetricHistory; + agent: AgentWithLatestMetrics; +} + +interface DiskMetric { + device: string; + mount_point: string; + total: number; + used: number; + free: number; + usage_rate: number; + fs_type: string; +} + +interface NetworkMetric { + interface: string; + bytes_sent: number; + bytes_recv: number; + packets_sent: number; + packets_recv: number; +} + +const formatBytes = (bytes: number | undefined, decimals = 2): string => { + if (bytes === undefined || bytes === 0) return "0 B"; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; +}; + +const formatPercent = (val: number | undefined, decimals = 2) => + val !== undefined ? `${val.toFixed(decimals)}%` : "-"; + +const ProgressBar = ({ + percent, + color, +}: { + percent: number; + color: string; +}) => ( +
+
+
+); + +const MetricCard = ({ + label, + value, + subValue, + percent, + color, +}: { + label: string; + value: string; + subValue?: string; + percent?: number; + color?: string; +}) => ( +
+ {label} + {value} + {subValue && {subValue}} + {percent !== undefined && color && ( + + )} +
+); + +const getOsIcon = (os: string | undefined) => { + if (!os) return faLaptop; + const osLower = os.toLowerCase(); + if (osLower.includes("windows")) return faWindows; + if (osLower.includes("mac") || osLower.includes("darwin")) return faApple; + if (osLower.includes("linux")) return faLinux; + if (osLower.includes("android")) return faAndroid; + if (osLower.includes("ios")) return faApple; + return faLaptop; +}; + +const AgentStatusBar: React.FC = ({ + latestMetric, + agent, +}) => { + // 计算存储总量和使用情况 + let totalStorage = 0; + let usedStorage = 0; + let storageUsageRate = 0; + + // 计算网络总量 + let totalUpload = 0; + let totalDownload = 0; + + if (latestMetric) { + // 解析并聚合所有磁盘数据 + if (latestMetric.disk_metrics) { + try { + const disks = JSON.parse(latestMetric.disk_metrics) as DiskMetric[]; + disks.forEach((disk) => { + totalStorage += disk.total; + usedStorage += disk.used; + }); + storageUsageRate = (usedStorage / totalStorage) * 100; + } catch (e) { + console.error("解析磁盘数据失败:", e); + } + } + + // 解析并聚合所有网络接口数据 + if (latestMetric.network_metrics) { + try { + const networks = JSON.parse( + latestMetric.network_metrics + ) as NetworkMetric[]; + networks.forEach((network) => { + totalUpload += network.bytes_sent; + totalDownload += network.bytes_recv; + }); + } catch (e) { + console.error("解析网络数据失败:", e); + } + } + } + + return ( +
+ {/* 顶部信息栏 */} +
+
+ + + + + {agent.name} + + + {agent.status === "active" ? "在线" : "离线"} + +
+
+ + {/* 主要指标 - 响应式网格布局 */} +
+ + + + +
+ + {/* 底部统计信息 */} +
+
+ 总上传:{formatBytes(totalUpload)} + 总下载:{formatBytes(totalDownload)} +
+
+
+ ); +}; + +export default AgentStatusBar; diff --git a/frontend/src/components/MonitorCard.tsx b/frontend/src/components/MonitorCard.tsx index be95e21..0e42906 100644 --- a/frontend/src/components/MonitorCard.tsx +++ b/frontend/src/components/MonitorCard.tsx @@ -7,7 +7,7 @@ import { import { Card, Badge } from "./ui"; import { MonitorWithDailyStatsAndStatusHistory } from "../types/monitors"; import { useTranslation } from "react-i18next"; -import StatusBar from "./StatusBar"; +import StatusBar from "./MonitorStatusBar"; import ResponseTimeChart from "./ResponseTimeChart"; interface MonitorCardProps { diff --git a/frontend/src/components/StatusBar.tsx b/frontend/src/components/MonitorStatusBar.tsx similarity index 100% rename from frontend/src/components/StatusBar.tsx rename to frontend/src/components/MonitorStatusBar.tsx diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx index 0205413..1d6d6b6 100644 --- a/frontend/src/components/ui/badge.tsx +++ b/frontend/src/components/ui/badge.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const badgeVariants = cva( "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", @@ -18,29 +18,42 @@ const badgeVariants = cva( outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", }, + color: { + default: "", + green: + "border-transparent bg-green-500/15 text-green-700 dark:text-green-400", + red: "border-transparent bg-red-500/15 text-red-700 dark:text-red-400", + yellow: + "border-transparent bg-yellow-500/15 text-yellow-700 dark:text-yellow-400", + amber: + "border-transparent bg-amber-500/15 text-amber-700 dark:text-amber-400", + gray: "border-transparent bg-gray-500/15 text-gray-700 dark:text-gray-400", + }, }, defaultVariants: { variant: "default", + color: "default", }, } -) +); function Badge({ className, variant, + color, asChild = false, ...props }: React.ComponentProps<"span"> & - VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span" + VariantProps & { asChild?: boolean; color?: string }) { + const Comp = asChild ? Slot : "span"; return ( - ) + ); } -export { Badge, badgeVariants } +export { Badge, badgeVariants }; diff --git a/frontend/src/i18n/zh-CN.ts b/frontend/src/i18n/zh-CN.ts index 1063fdf..1805de5 100644 --- a/frontend/src/i18n/zh-CN.ts +++ b/frontend/src/i18n/zh-CN.ts @@ -18,25 +18,18 @@ const zhCN = { "common.cancel": "取消", "common.delete": "删除", "common.edit": "编辑", - "common.create": "创建", + "common.back": "返回", - "common.search": "搜索", - "common.filter": "筛选", + "common.actions": "操作", "common.status": "状态", - "common.name": "名称", - "common.description": "描述", + "common.type": "类型", - "common.url": "URL", - "common.created": "创建时间", - "common.updated": "更新时间", - "common.yes": "是", + "common.no": "否", - "common.confirm": "确认", - "common.success": "成功", + "common.error": "错误", - "common.warning": "警告", - "common.info": "信息", + "common.retry": "重试", "common.refresh": "刷新", "common.deleting": "删除中...", @@ -45,25 +38,22 @@ const zhCN = { "common.loadingError": "加载出错", "common.notFound": "未找到", "common.backToList": "返回列表", - "common.success.deleted": "已成功删除", - "common.success.updated": "更新成功", + "common.error.delete": "删除失败,请重试", - "common.error.update": "更新失败", + "common.error.fetch": "获取数据失败", - "common.required": "必填", - "common.optional": "可选", + "common.copy": "复制", "common.copied": "已复制", "common.deleteConfirmation": "确认删除", "common.deleteConfirmMessage": "您确定要删除吗?此操作无法撤销。", "common.second": "秒", "common.seconds": "秒", - "common.minute": "分钟", - "common.minutes": "分钟", + "common.date": "日期", "common.close": "关闭", "common.unknown": "未知", - "common.comingSoon": "即将推出的功能", + "common.default": "默认", // 语言选择 @@ -259,46 +249,46 @@ const zhCN = { "agent.deleteError": "删除客户端失败", // 客户端监控指标 - "agent.metrics.cpu":{ - "title": "CPU", - "usage": "使用情况", - "cores": "核心数", - "model": "型号", + "agent.metrics.cpu": { + title: "CPU", + usage: "使用情况", + cores: "核心数", + model: "型号", }, "agent.metrics.memory": { - "title": "内存", - "total": "总内存", - "used": "已使用内存", - "free": "可用内存", - "usage": "使用情况", - "usageRate": "使用率", + title: "内存", + total: "总内存", + used: "已使用内存", + free: "可用内存", + usage: "使用情况", + usageRate: "使用率", }, "agent.metrics.disk": { - "title": "磁盘", - "usage": "使用情况", - "device": "设备", - "mountPoint": "挂载点", - "total": "总空间", - "used": "已使用空间", - "free": "可用空间", - "usageRate": "使用率", - "fsType": "文件系统类型" + title: "磁盘", + usage: "使用情况", + device: "设备", + mountPoint: "挂载点", + total: "总空间", + used: "已使用空间", + free: "可用空间", + usageRate: "使用率", + fsType: "文件系统类型", }, "agent.metrics.network": { - "title": "网络", - "rate": "速率", - "received": "接收", - "interface": "接口", - "sent": "发送", - "packetsReceived": "接收数据包", - "packetsSent": "发送数据包", + title: "网络", + rate: "速率", + received: "接收", + interface: "接口", + sent: "发送", + packetsReceived: "接收数据包", + packetsSent: "发送数据包", }, "agent.metrics.load": { - "title": "负载", + title: "负载", "1min": "1分钟负载", "5min": "5分钟负载", "15min": "15分钟负载", - "minute": "分钟", + minute: "分钟", }, // 创建/编辑客户端 @@ -368,8 +358,6 @@ const zhCN = { // 状态页 "statusPage.title": "系统状态", "statusPage.allOperational": "所有系统正常运行", - "statusPage.lastUpdated": "最后更新", - "statusPage.justNow": "刚刚", "statusPage.apiServices": "API服务状态", "statusPage.agentStatus": "客户端监控状态", @@ -394,39 +382,16 @@ const zhCN = { "statusPageConfig.configSaved": "状态页配置已保存", "statusPageConfig.saveError": "保存状态页配置失败", "statusPageConfig.fetchingConfig": "开始获取状态页配置...", - "statusPageConfig.configFromProperty": "从config属性获取的状态页配置:", - "statusPageConfig.tryingDirectResponse": - "API响应似乎直接包含配置数据,尝试直接使用", - "statusPageConfig.parseError": "尝试解析API响应失败:", - "statusPageConfig.receivedConfig": "获取到的状态页配置:", - "statusPageConfig.monitorListType": "监控项列表类型:", - "statusPageConfig.monitorCount": "监控项数量:", "statusPageConfig.configMonitor": "配置中的监控项", - "statusPageConfig.noValidConfig": "未获取到有效的状态页配置或配置为空", - "statusPageConfig.invalidResponse": "API响应无效", - "statusPageConfig.fetchingMonitors": "开始获取所有监控项...", + "statusPageConfig.monitorsResponse": "获取所有监控项响应:", - "statusPageConfig.foundMonitor": "找到配置中的监控项", - "statusPageConfig.originalStatus": "原始选中状态", - "statusPageConfig.processedStatus": "处理后的选中状态", - "statusPageConfig.notFoundMonitor": "未在配置中找到监控项", - "statusPageConfig.finalMonitorResult": "最终监控项处理结果", + "statusPageConfig.selectedStatus": "选中状态", - "statusPageConfig.processedMonitorList": "处理后的监控项列表", - "statusPageConfig.fetchingAgents": "开始获取所有客户端...", - "statusPageConfig.foundAgent": "找到配置中的客户端", - "statusPageConfig.notFoundAgent": "未在配置中找到客户端", - "statusPageConfig.finalAgentResult": "最终客户端处理结果", - "statusPageConfig.processedAgentList": "处理后的客户端列表", - "statusPageConfig.fetchMonitorsError": "获取监控数据失败", - "statusPageConfig.fetchDataError": "获取数据失败", - "statusPageConfig.configLoaded": "状态页配置加载完成,验证监控项选中状态:", + "statusPageConfig.invalidAgentId": "尝试更改无效的客户端ID:", - "statusPageConfig.savingConfig": "准备保存配置对象:", + "statusPageConfig.selectedMonitors": "选中的监控项:", - "statusPageConfig.selectedMonitorIds": "选中的监控项ID列表:", - "statusPageConfig.callingSaveApi": "开始调用保存配置API...", - "statusPageConfig.saveApiResponse": "保存配置API响应:", + "statusPageConfig.serviceRendering": "服务渲染", "statusPageConfig.agentRendering": "客户端渲染", "statusPageConfig.monitorStatusChange": "监控项状态变更", diff --git a/frontend/src/pages/agents/AgentDetail.tsx b/frontend/src/pages/agents/AgentDetail.tsx index 4ad12a2..68c8908 100644 --- a/frontend/src/pages/agents/AgentDetail.tsx +++ b/frontend/src/pages/agents/AgentDetail.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { Box, Flex, Heading, Text, Grid, Container } from "@radix-ui/themes"; -import { Button, Card, Badge, Avatar, AvatarFallback } from "@/components/ui"; +import { Button, Card, Badge } from "@/components/ui"; import { ArrowLeftIcon, @@ -15,6 +15,7 @@ import { DesktopIcon, GlobeIcon, Link2Icon, + PersonIcon, } from "@radix-ui/react-icons"; import { getAgent, deleteAgent, getAgentMetrics } from "../../api/agents"; import { Agent } from "../../types/agents"; @@ -233,169 +234,164 @@ const AgentDetail = () => { + + + {/* 系统信息卡片 */} + + + {t("agent.systemInfo")} + + + + + {t("agents.table.name")}: + + + {agent.name} + + + + + + + + {t("agent.os")}: + + + {agent.os || t("common.notFound")} + + + - - - - - {agent.name.charAt(0)} - - - {agent.name} - - - - - - {/* 系统信息卡片 */} - - - {t("agent.systemInfo")} + + + + + {t("agent.version")}: + + + {agent.version || t("common.notFound")} + + + - - - - - {t("agent.os")}: - - - {agent.os || t("common.notFound")} - - - + + + + + {t("agent.hostname")}: + + + {agent.hostname || t("common.notFound")} + + + - - - - - {t("agent.version")}: - - - {agent.version || t("common.notFound")} - - - - - - - - - {t("agent.hostname")}: - - - {agent.hostname || t("common.notFound")} - - - - - - - - - {t("agent.ipAddress")}: - - {agent.ip_addresses ? ( - (() => { - try { - const ipArray = JSON.parse( - String(agent.ip_addresses) + + + + + {t("agent.ipAddress")}: + + {agent.ip_addresses ? ( + (() => { + try { + const ipArray = JSON.parse(String(agent.ip_addresses)); + if (Array.isArray(ipArray) && ipArray.length > 0) { + return ( + + {ipArray[0]} + {ipArray.length > 1 + ? ` (+${ipArray.length - 1})` + : ""} + ); - if (Array.isArray(ipArray) && ipArray.length > 0) { - return ( - - {ipArray[0]} - {ipArray.length > 1 - ? ` (+${ipArray.length - 1})` - : ""} - - ); - } else { - return ( - - {String(agent.ip_addresses)} - - ); - } - } catch (e) { + } else { return ( {String(agent.ip_addresses)} ); } - })() - ) : ( - - {t("common.unknown")} - - )} - - - - - - - - {t("agent.uptime")}: - - - {formatUptime(agent)} - - - - - - - - - {t("agent.lastUpdated")}: - - - {formatDateTime(agent.updated_at)} - - - - - {/* 如果存在多个IP地址,展示完整列表 */} - {agent.ip_addresses && - (() => { - try { - const ipArray = JSON.parse(String(agent.ip_addresses)); - if (Array.isArray(ipArray) && ipArray.length > 1) { + } catch (e) { return ( - - - {ipArray.slice(1).map((ip, index) => ( - - {ip} - - ))} - - + + {String(agent.ip_addresses)} + ); } - return null; - } catch (e) { - return null; - } - })()} - - + })() + ) : ( + + {t("common.unknown")} + + )} + + - {/* Agent 资源信息卡片 */} - - - {t("agent.metrics")} - - - - - + + + + + {t("agent.uptime")}: + + + {formatUptime(agent)} + + + + + + + + + {t("agent.lastUpdated")}: + + + {formatDateTime(agent.updated_at)} + + + + + {/* 如果存在多个IP地址,展示完整列表 */} + {agent.ip_addresses && + (() => { + try { + const ipArray = JSON.parse(String(agent.ip_addresses)); + if (Array.isArray(ipArray) && ipArray.length > 1) { + return ( + + + {ipArray.slice(1).map((ip, index) => ( + + {ip} + + ))} + + + ); + } + return null; + } catch (e) { + return null; + } + })()} + + + + {/* Agent 资源信息卡片 */} + + + {t("agent.metrics")} + + + + ); diff --git a/frontend/src/pages/agents/AgentsList.tsx b/frontend/src/pages/agents/AgentsList.tsx index 22e3245..94d970f 100644 --- a/frontend/src/pages/agents/AgentsList.tsx +++ b/frontend/src/pages/agents/AgentsList.tsx @@ -37,10 +37,14 @@ import { ViewGridIcon, TrashIcon, } from "@radix-ui/react-icons"; -import { getAllAgents, deleteAgent, getAgentMetrics } from "../../api/agents"; -import AgentCard from "../../components/AgentCard"; +import { + getAllAgents, + deleteAgent, + getLatestAgentMetrics, +} from "../../api/agents"; +import AgentStatusBar from "../../components/AgentStatusBar"; import { useTranslation } from "react-i18next"; -import { Agent } from "../../types"; +import { AgentWithLatestMetrics } from "../../types"; // 定义客户端状态颜色映射 const statusColors: Record = { @@ -52,7 +56,7 @@ const statusColors: Record = { const AgentsList = () => { const navigate = useNavigate(); - const [agents, setAgents] = useState([]); + const [agents, setAgents] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); @@ -84,20 +88,25 @@ const AgentsList = () => { const agentsWithMetrics = await Promise.all( response.agents.map(async (agent) => { // 获取指定客户端的指标数据 - const metricsResponse = await getAgentMetrics(agent.id); + const metricsResponse = await getLatestAgentMetrics(agent.id); if (!metricsResponse.success) { console.error("获取指标数据失败:", metricsResponse.message); - return agent; // 返回原始客户端数据 + return { ...agent, metrics: undefined } as AgentWithLatestMetrics; } - const metrics = metricsResponse.agent; - console.log("获取到的 metrics 数据: ", metrics); - console.log("获取到的 agent 数据: ", agent); - return { ...agent, metrics }; + // 确保我们只取数组中的第一条记录(最新的) + const latestMetric = Array.isArray(metricsResponse.agent) + ? metricsResponse.agent[0] + : metricsResponse.agent; + + return { + ...agent, + metrics: latestMetric, + } as AgentWithLatestMetrics; }) ); - setAgents(agentsWithMetrics); + console.log("获取到的 agentsWithMetrics 数据: ", agentsWithMetrics); + setAgents(agentsWithMetrics as AgentWithLatestMetrics[]); } - console.log("获取到的 agent 数据: ", response); setLoading(false); }; @@ -143,7 +152,7 @@ const AgentsList = () => { {agents.map((agent) => ( - + { }; return ( - + - - {t("agent.form.editingClient", { name: formData.name })} - - - - -
- - - - {t("agent.form.name")}{" "} - - * - - - - - {t("agent.form.nameHelp")} - - - - - - - +
+ + + + + {t("agent.form.editingClient", { name: formData.name })} + - - + + +
+ + + + {t("agent.form.name")}{" "} + + * + + + + + {t("agent.form.nameHelp")} + + + + + + + + +
+
+
); }; diff --git a/frontend/src/pages/monitors/CreateMonitor.tsx b/frontend/src/pages/monitors/CreateMonitor.tsx index 5f2a8cd..98b15ab 100644 --- a/frontend/src/pages/monitors/CreateMonitor.tsx +++ b/frontend/src/pages/monitors/CreateMonitor.tsx @@ -16,6 +16,7 @@ import { SelectContent, SelectTrigger, SelectItem, + SelectValue, Table, TableCell, TableRow, @@ -69,15 +70,6 @@ const CreateMonitor = () => { ) => { const newHeaders = [...headers]; newHeaders[index][field] = value; - - // 如果最后一行有输入内容,添加新的空行 - if ( - index === headers.length - 1 && - (newHeaders[index].key || newHeaders[index].value) - ) { - newHeaders.push({ key: "", value: "" }); - } - setHeaders(newHeaders); }; @@ -151,229 +143,215 @@ const CreateMonitor = () => { const showBodyField = ["POST", "PUT", "PATCH"].includes(formData.method); return ( - -
- - - - {t("monitor.form.title.create")} - + + + + + {t("monitor.form.title.create")} - -
- -
- - - - - {t("monitor.form.name")} * - - - - - - - URL * - - - - - - - {t("monitor.form.method")} * - - - - - - - - {t("monitor.form.interval")} * - - - - {t("monitor.form.intervalMin")} - - - - - - {t("monitor.form.timeout")} * - - - - - - - - {t("monitor.form.expectedStatus")} * - - - - - - - {t("monitor.form.headers")} - - - - - - - {t("monitor.form.headerName")} - - - {t("monitor.form.headerValue")} - - - - - - {headers.map((header, index) => ( - - - - handleHeaderChange( - index, - "key", - e.target.value - ) - } - /> - - - - handleHeaderChange( - index, - "value", - e.target.value - ) - } - /> - - - removeHeader(index)} - > - - - - - ))} - -
- - - -
- - {t("monitor.form.headersHelp")} - -
- - {showBodyField && ( - - - {t("monitor.form.body")} - -