diff --git a/src/web/Dockerfile b/src/web/Dockerfile new file mode 100644 index 0000000..0e761ee --- /dev/null +++ b/src/web/Dockerfile @@ -0,0 +1,36 @@ +# ---- 1. 构建阶段 ---- +FROM node:20-alpine AS build + +# 设置工作目录 +WORKDIR /app + +# 复制依赖清单 +COPY package*.json ./ + +# 安装依赖 +RUN npm install + +# 复制全部源码 +COPY . . + +# 构建生产环境代码 +RUN npm run build + + +# ---- 2. 部署阶段 ---- +FROM nginx:alpine + +# 删除默认配置 +RUN rm /etc/nginx/conf.d/default.conf + +# 复制你自己的 nginx 配置 +COPY build_tools/front_end/nginx.conf /etc/nginx/conf.d/default.conf + +# 将打包好的 dist 文件放到 nginx 的静态目录 +COPY --from=build /app/dist /usr/share/nginx/html + +# 暴露 80 端口 +EXPOSE 80 + +# 启动 Nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/src/web/app.py b/src/web/app.py deleted file mode 100644 index 5b4dd28..0000000 --- a/src/web/app.py +++ /dev/null @@ -1,39 +0,0 @@ -from flask import Flask, jsonify -import requests - -app = Flask(__name__) - -EXTERNAL_APIS = { - "nodes": "http://master.argus.com/api/v1/nodes", - "alerts": "http://alertmanager.argus.com/api/v2/alerts" -} - - -def fetch_external(api_name): - """通用的外部接口请求函数""" - try: - url = EXTERNAL_APIS[api_name] - response = requests.get(url, timeout=5) - response.raise_for_status() - return {"status": "success", "data": response.json()} - except requests.exceptions.RequestException as e: - return {"status": "error", "message": str(e)} - - -@app.route("/api/v1/web/health", methods=["GET"]) -def get_health(): - return jsonify(fetch_external("health")) - - -@app.route("/api/v1/web/nodes", methods=["GET"]) -def get_nodes(): - return jsonify(fetch_external("nodes")) - - -@app.route("/api/v1/web/alerts", methods=["GET"]) -def get_alerts(): - return jsonify(fetch_external("alerts")) - - -if __name__ == "__main__": - app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/src/web/build_tools/front_end/build.sh b/src/web/build_tools/front_end/build.sh new file mode 100644 index 0000000..20ba270 --- /dev/null +++ b/src/web/build_tools/front_end/build.sh @@ -0,0 +1,5 @@ +docker pull node:20-alpine +docker pull nginx:alpine +cd ../.. +docker build -t portal-frontend . +rm -f portal-frontend.tar.gz && sudo docker image save portal-frontend:latest | gzip > portal-frontend.tar.gz \ No newline at end of file diff --git a/src/web/build_tools/front_end/nginx.conf b/src/web/build_tools/front_end/nginx.conf new file mode 100644 index 0000000..0c3b3bc --- /dev/null +++ b/src/web/build_tools/front_end/nginx.conf @@ -0,0 +1,13 @@ +server { + listen 80; + server_name web.argus.com; + + root /usr/share/nginx/html; + index index.html; + + # React 前端路由兼容 + location / { + try_files $uri /index.html; + } + +} diff --git a/src/web/build_tools/proxy/Dockerfile b/src/web/build_tools/proxy/Dockerfile new file mode 100644 index 0000000..7162426 --- /dev/null +++ b/src/web/build_tools/proxy/Dockerfile @@ -0,0 +1,17 @@ +# 使用轻量级 Nginx 基础镜像 +FROM nginx:1.25-alpine + +# 删除默认配置 +RUN rm -rf /etc/nginx/conf.d/* + +# 复制自定义 Proxy 配置 +# (可以在构建时直接COPY进去,也可以运行时挂载) +COPY conf.d/ /etc/nginx/conf.d/ + +# 日志目录(可选) +VOLUME ["/var/log/nginx"] + +# 暴露端口 +EXPOSE 80 443 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/src/web/build_tools/proxy/argus-proxy.tar.gz b/src/web/build_tools/proxy/argus-proxy.tar.gz new file mode 100644 index 0000000..c46a22f Binary files /dev/null and b/src/web/build_tools/proxy/argus-proxy.tar.gz differ diff --git a/src/web/build_tools/proxy/argus.nginx.conf b/src/web/build_tools/proxy/argus.nginx.conf new file mode 100644 index 0000000..0316133 --- /dev/null +++ b/src/web/build_tools/proxy/argus.nginx.conf @@ -0,0 +1,71 @@ +# 门户前端 (React 静态资源通过内网 Nginx 或 Node.js 服务暴露) +server { + listen 80; + server_name web.argus.com; + + location / { + proxy_pass http://web.argus.com; # 门户前端内部服务 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} + +# Grafana +server { + listen 80; + server_name grafana.metric.argus.com; + + location / { + proxy_pass http://grafana.metric.argus.com; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} + +# Prometheus +server { + listen 80; + server_name prometheus.metric.argus.com; + + location / { + proxy_pass http://prometheus.metric.argus.com; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} + +# Elasticsearch +server { + listen 80; + server_name es.log.argus.com; + + location / { + proxy_pass http://es.log.argus.com; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} + +# Kibana +server { + listen 80; + server_name kibana.log.argus.com; + + location / { + proxy_pass http://kibana.log.argus.com; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} + +# Alertmanager +server { + listen 80; + server_name alertmanager.alert.argus.com; + + location / { + proxy_pass http://alertmanager.alert.argus.com; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} diff --git a/src/web/build_tools/proxy/build_image.sh b/src/web/build_tools/proxy/build_image.sh new file mode 100644 index 0000000..78f1acb --- /dev/null +++ b/src/web/build_tools/proxy/build_image.sh @@ -0,0 +1,2 @@ +docker build -t argus-proxy:latest . +rm -f argus-proxy.tar.gz && sudo docker image save argus-proxy:latest | gzip > argus-proxy.tar.gz diff --git a/src/web/build_tools/proxy/conf.d/argus.conf b/src/web/build_tools/proxy/conf.d/argus.conf new file mode 100644 index 0000000..786c293 --- /dev/null +++ b/src/web/build_tools/proxy/conf.d/argus.conf @@ -0,0 +1,78 @@ +server { + listen 80; + server_name web.argus.com; + + # 门户网站(React 前端),通过 proxy 转发到内部服务 + location / { + proxy_pass http://portal-frontend:80; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +server { + listen 80; + server_name grafana.metric.argus.com; + + location / { + proxy_pass http://grafana.metric.argus.com; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +server { + listen 80; + server_name prometheus.metric.argus.com; + + location / { + proxy_pass http://prometheus.metric.argus.com; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +server { + listen 80; + server_name es.log.argus.com; + + location / { + proxy_pass http://es.log.argus.com; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +server { + listen 80; + server_name kibana.log.argus.com; + + location / { + proxy_pass http://kibana.log.argus.com; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +server { + listen 80; + server_name alertmanager.alert.argus.com; + + location / { + proxy_pass http://alertmanager.alert.argus.com; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/src/web/portal-frontend.tar.gz b/src/web/portal-frontend.tar.gz new file mode 100644 index 0000000..281203a Binary files /dev/null and b/src/web/portal-frontend.tar.gz differ diff --git a/src/web/src/components/AlertCard.jsx b/src/web/src/components/AlertCard.jsx deleted file mode 100644 index 5e06cac..0000000 --- a/src/web/src/components/AlertCard.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Card, Group, Text, Anchor } from "@mantine/core"; - -export function AlertCard({ alerts, link }) { - const alertTypes = [ - { label: "严重告警", count: alerts.critical_alerts, color: "red" }, - { label: "主要告警", count: alerts.major_alerts, color: "yellow" }, - { label: "次要告警", count: alerts.minor_alerts, color: "blue" }, - ]; - - return ( - - 告警监控 - - - {alerts.total_alerts} 待处理告警 - - - {alertTypes.map((a) => ( - - - - {a.label} - - {a.count} - - ))} - - ); -} diff --git a/src/web/src/components/AlertFilters.jsx b/src/web/src/components/AlertFilters.jsx new file mode 100644 index 0000000..73a8a6f --- /dev/null +++ b/src/web/src/components/AlertFilters.jsx @@ -0,0 +1,38 @@ +import { Group, Select } from "@mantine/core"; + +export function AlertFilters({ filters, setFilters, nodeOptions }) { + return ( + + setFilters((f) => ({ ...f, state: value }))} + data={[ + { value: "all", label: "全部" }, + { value: "active", label: "Active" }, + { value: "resolved", label: "Resolved" }, + ]} + w={150} + /> + setConfig({ ...config, log_level: value })} - /> - - ) : ( - - 最大连接数: {config.max_connections} - 超时时间: {config.timeout} - 日志级别: {config.log_level} - - )} - + const updateConfig = (index, key, value) => { + setConfigList((prev) => + prev.map((item, i) => (i === index ? [key, value] : item)) ); + }; + + const addConfig = () => { + if (newKey && !configList.find(([k]) => k === newKey)) { + setConfigList((prev) => [...prev, [newKey, newValue]]); + setNewKey(""); + setNewValue(""); + } + }; + + const handleSave = async () => { + setSaving(true); + try { + const configObj = Object.fromEntries(configList); + await apiRequest(`${EXTERNAL_API.MASTER_NODES}/${nodeId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ config: configObj }), + }); + setEditing(false); + onSaved && onSaved(); + } finally { + setSaving(false); + } + }; + + return ( + + + 配置信息 + + {editing ? ( + <> + + + + setEditing(false)}> + + + + ) : ( + setEditing(true)}> + + + )} + + + + {editing ? ( + + {configList.map(([key, value], idx) => ( + + updateConfig(idx, e.target.value, value)} + /> + updateConfig(idx, key, e.target.value)} + /> + removeConfig(idx)}> + + + + ))} + + setNewKey(e.target.value)} + /> + setNewValue(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && addConfig()} + /> + + + + + + ) : ( + + {configList.length > 0 ? ( + configList.map(([key, value], idx) => ( + + {key}: + {String(value)} + + )) + ) : ( + 暂无配置 + )} + + )} + + ); } diff --git a/src/web/src/components/NodeDetailDrawer.jsx b/src/web/src/components/NodeDetailDrawer.jsx index 072d12e..4884e6e 100644 --- a/src/web/src/components/NodeDetailDrawer.jsx +++ b/src/web/src/components/NodeDetailDrawer.jsx @@ -6,16 +6,14 @@ import { Center, ScrollArea, Group, - Badge, - Stack, Divider, ThemeIcon, + Stack, } from "@mantine/core"; import { healthStatus } from "../config/status"; import { apiRequest } from "../config/request"; import { EXTERNAL_API } from "../config/api"; -// 子组件 import NodeConfigCard from "./NodeConfigCard"; import NodeLabelCard from "./NodeLabelCard"; import NodeMetaCard from "./NodeMetaCard"; @@ -25,7 +23,6 @@ export default function NodeDetailDrawer({ opened, nodeId, onClose }) { const [node, setNode] = useState(null); const [loading, setLoading] = useState(false); - // 获取节点详情 const fetchNodeDetail = async () => { if (!nodeId) return; setLoading(true); @@ -38,9 +35,7 @@ export default function NodeDetailDrawer({ opened, nodeId, onClose }) { }; useEffect(() => { - if (opened && nodeId) { - fetchNodeDetail(); - } + if (opened && nodeId) fetchNodeDetail(); }, [opened, nodeId]); return ( @@ -54,52 +49,69 @@ export default function NodeDetailDrawer({ opened, nodeId, onClose }) { overlayProps={{ backgroundOpacity: 0.4, blur: 4 }} > {loading ? ( -
- -
-) : node ? ( -
- {/* 固定头部基础信息 */} -
- - - {healthStatus(node.status).icon} - +
+ +
+ ) : node ? ( +
+ {/* 固定头部基础信息 */} +
+ + + {healthStatus(node.status).icon} + - {node.name} - {node.type} - - {node.status} - - - -
+ + {node.name} + + {node.type} + {node.status} + + 最后更新时间: {new Date(node.last_updated).toLocaleString()} + + + +
- {/* 滚动内容 */} - - - - - - + {/* 滚动内容 */} + + + {/* 配置信息 */} + - - 最后更新时间: {new Date(node.last_updated).toLocaleString()} - - - -
-) : ( - 暂无数据 -)} + {/* 标签信息 */} + + + {/* 元数据 */} + + + {/* 健康信息 */} + + + {/* 其他基础信息展示 */} + + 注册时间: {new Date(node.register_time).toLocaleString()} + 最近上报时间: {new Date(node.last_report).toLocaleString()} + + + +
+ ) : ( + 暂无数据 + )} ); } diff --git a/src/web/src/components/NodeHealthCard.jsx b/src/web/src/components/NodeHealthCard.jsx index c80aa1b..0186772 100644 --- a/src/web/src/components/NodeHealthCard.jsx +++ b/src/web/src/components/NodeHealthCard.jsx @@ -1,39 +1,14 @@ -import { Card, Text, Group, Stack, Badge } from "@mantine/core"; -import { IconCheck, IconAlertTriangle, IconX } from "@tabler/icons-react"; - -// 状态映射:颜色 + 图标 -const healthMap = { - activate: { color: "green", icon: IconCheck }, - error: { color: "red", icon: IconX }, - warning: { color: "yellow", icon: IconAlertTriangle }, - inactive: { color: "gray", icon: IconX }, -}; +import { Card, Text, Stack } from "@mantine/core"; export default function NodeHealthCard({ node }) { - if (!node || !node.health) return null; - - const items = Object.entries(node.health); // [['agent', 'activate'], ...] + const health = node.health || {}; return ( - - 健康状态 + + 健康信息 - {items.map(([component, status]) => { - const { color, icon: Icon } = healthMap[status] || healthMap.inactive; - return ( - - {/* 组件名 */} - - {component} - - - {/* 状态标签 */} - } variant="filled"> - {status} - - - ); - })} + 日志: {health.log || "无"} + 指标: {health.metric || "无"} ); diff --git a/src/web/src/components/NodeLabelCard.jsx b/src/web/src/components/NodeLabelCard.jsx index 508f552..2a6fbd0 100644 --- a/src/web/src/components/NodeLabelCard.jsx +++ b/src/web/src/components/NodeLabelCard.jsx @@ -1,29 +1,54 @@ -import { useState } from "react"; -import { Card, Text, Group, TextInput, Stack, ActionIcon } from "@mantine/core"; -import { IconEdit, IconX, IconCheck } from "@tabler/icons-react"; +import { useState, useEffect } from "react"; +import { Card, Text, Group, TextInput, Stack, ActionIcon, Badge } from "@mantine/core"; +import { IconEdit, IconX, IconCheck, IconPlus, IconTrash } from "@tabler/icons-react"; import { apiRequest } from "../config/request"; import { EXTERNAL_API } from "../config/api"; -export default function NodeLabelCard({ node, onSaved }) { +export default function NodeLabelCard({ nodeId, labels = [], onSaved }) { const [editing, setEditing] = useState(false); - const [label, setLabel] = useState(node.label || {}); + const [tagList, setTagList] = useState([]); + const [tagColors, setTagColors] = useState([]); + const [newTag, setNewTag] = useState(""); const [saving, setSaving] = useState(false); + const randomColor = () => { + const colors = ["red","pink","grape","violet","indigo","blue","cyan","teal","green","lime","yellow","orange","gray"]; + return colors[Math.floor(Math.random() * colors.length)]; + }; + + useEffect(() => { + const arr = Array.isArray(labels) ? labels : []; + setTagList(arr); + setTagColors(arr.map(() => randomColor())); + }, [labels]); + + const removeTag = (index) => { + setTagList((prev) => prev.filter((_, i) => i !== index)); + setTagColors((prev) => prev.filter((_, i) => i !== index)); + }; + + const updateTag = (index, value) => { + setTagList((prev) => prev.map((t, i) => (i === index ? value : t))); + }; + + const addTag = () => { + if (newTag && !tagList.includes(newTag)) { + setTagList((prev) => [...prev, newTag]); + setTagColors((prev) => [...prev, randomColor()]); + setNewTag(""); + } + }; + const handleSave = async () => { setSaving(true); try { - const res = await apiRequest( - `${EXTERNAL_API.MASTER_NODES}/${node.id}`, - { - method: "PUT", - body: JSON.stringify({ label }), - }, - "标签已更新" - ); - if (res) { - setEditing(false); - onSaved && onSaved(); - } + await apiRequest(`${EXTERNAL_API.MASTER_NODES}/${nodeId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ label: tagList }), + }); + setEditing(false); + onSaved && onSaved(); } finally { setSaving(false); } @@ -36,31 +61,34 @@ export default function NodeLabelCard({ node, onSaved }) { {editing ? ( <> - - - - setEditing(false)}> - - + + setEditing(false)}> ) : ( - setEditing(true)}> - - + setEditing(true)}> )}
{editing ? ( - - setLabel({ ...label, tag: e.target.value })} - /> + + {tagList.map((tag, idx) => ( + + updateTag(idx, e.target.value)} /> + removeTag(idx)}> + + ))} + + setNewTag(e.target.value)} onKeyDown={(e) => e.key === "Enter" && addTag()} /> + + ) : ( - 标签: {label.tag} + + {tagList.length > 0 ? tagList.map((tag, idx) => ( + {tag} + )) : 暂无标签} + )} ); diff --git a/src/web/src/components/NodeMetaCard.jsx b/src/web/src/components/NodeMetaCard.jsx index 025da48..ad69e83 100644 --- a/src/web/src/components/NodeMetaCard.jsx +++ b/src/web/src/components/NodeMetaCard.jsx @@ -4,13 +4,17 @@ export default function NodeMetaCard({ node }) { const meta = node.meta_data || {}; return ( - + 元数据信息 - 主机: {meta.host} - IP: {meta.ip} - 区域: {meta.region} - 可用区: {meta.zone} + 主机名: {meta.hostname} + IP: {meta.ip} + 环境: {meta.env} + 用户: {meta.user} + 实例: {meta.instance} + CPU 数量: {meta.cpu_number} + 内存: {(meta.memory_in_bytes / 1024 / 1024).toFixed(2)} MB + GPU 数量: {meta.gpu_number} ); diff --git a/src/web/src/components/NodeModal.jsx b/src/web/src/components/NodeModal.jsx deleted file mode 100644 index 98b9e51..0000000 --- a/src/web/src/components/NodeModal.jsx +++ /dev/null @@ -1,142 +0,0 @@ -import { useState, useEffect } from "react"; -import { Modal, TextInput, Select, Button } from "@mantine/core"; -import { notifications } from "@mantine/notifications"; -import { EXTERNAL_API } from "../config/api"; -import { statusOptions } from "../config/status"; -import { apiRequest } from "../config/request"; - -export default function NodeModal({ opened, onClose, node, onSaved }) { - const [form, setForm] = useState({ name: "", status: "healthy", load: 0 }); - const [loading, setLoading] = useState(false); - const [errors, setErrors] = useState({ name: "", load: "" }); - - // 初始化表单 - useEffect(() => { - setForm(node ? { ...node } : { name: "", status: "healthy", load: 0 }); - setErrors({ name: "", load: "" }); - }, [node]); - - // 实时校验 - const validateField = (field, value) => { - switch (field) { - case "name": - setErrors((prev) => ({ - ...prev, - name: value.trim() ? "" : "名称不能为空", - })); - break; - case "load": - const loadNum = Number(value); - setErrors((prev) => ({ - ...prev, - load: - isNaN(loadNum) || loadNum < 0 || loadNum > 100 - ? "负载必须是 0~100 的数字" - : "", - })); - break; - default: - break; - } - }; - - const handleSave = async () => { - const name = form.name.trim(); - const load = Number(form.load); - let hasError = false; - - if (!name) { - setErrors((prev) => ({ ...prev, name: "名称不能为空" })); - notifications.show({ - title: "校验失败", - message: "名称不能为空 ❌", - color: "red", - }); - hasError = true; - } - if (isNaN(load) || load < 0 || load > 100) { - setErrors((prev) => ({ ...prev, load: "负载必须是 0~100 的数字" })); - notifications.show({ - title: "校验失败", - message: "负载必须是 0~100 的数字 ❌", - color: "red", - }); - hasError = true; - } - if (hasError) return; - - const method = node ? "PUT" : "POST"; - const url = node ? `${EXTERNAL_API.MASTER_NODES}/${node.id}` : EXTERNAL_API.MASTER_NODES; - - setLoading(true); - try { - await apiRequest(url, { - method, - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ ...form, name, load }), - }); - notifications.show({ - title: "成功", - message: node ? "节点已更新 ✅" : "节点已新增 ✅", - color: "green", - autoClose: 3000, - }); - - setTimeout(() => { - onClose(); - onSaved(); - }, 3000); - } catch (err) { - notifications.show({ - title: "错误", - message: `操作失败: ${err.message || "未知错误"}`, - color: "red", - }); - } finally { - setLoading(false); - } - }; - - return ( - - { - const value = e.target.value; - setForm((prev) => ({ ...prev, name: value })); - validateField("name", value); - }} - mb="sm" - error={errors.name} - required - /> - + + + + )} + + {loading ? ( +
+ ) : ( + <> + + + + ID + 名称 + 状态 + 类型 + 版本 + {withActions && 操作} + + + {rows} +
+ + {withPagination && ( + { + setPage(p); + fetchNodes({ page: p }); + }} + onPageSizeChange={(size) => { + setPageSize(size); + setPage(1); + fetchNodes({ page: 1, pageSize: size }); + }} + /> + )} + + )} +
+ ); +} diff --git a/src/web/src/config/api.js b/src/web/src/config/api.js index 8b7cf6a..391c7be 100644 --- a/src/web/src/config/api.js +++ b/src/web/src/config/api.js @@ -1,17 +1,13 @@ -export const NODES_API_URL = "http://master.argus.com/api/v1/nodes"; - -export const INTERNAL_API = { - CLUSTER: "/api/v1/web/cluster", - HEALTH: "/api/v1/web/health", - ALERTS: "/api/v1/web/alerts", -} - export const EXTERNAL_API = { - MASTER_NODES: "http://master.argus.com/api/v1/nodes", + MASTER_NODES: "http://master.argus.com/api/v1/master/nodes", + MASTER_NODES_STATISTICS: "http://master.argus.com/api/v1/master/nodes/statistics", + ALERTS_INFOS: "http://localhost:9093/api/v2/alerts", } +// proxy location定位到具体位置。 +// proxy需要单独的机器,nginx配置。提供对外的endpoint,通过算力平台映射。 export const EXTERNAL_HOST = { - ALERTS: "http://alertmanager.alert.argus.com", + ALERTS: "http://localhost:9093", GRAFANA: "http://grafana.metric.argus.com", PROMETHEUS: "http://prometheus.metric.argus.com", ES: "http://es.log.argus.com", diff --git a/src/web/src/config/request.js b/src/web/src/config/request.js index db8db51..34ddc83 100644 --- a/src/web/src/config/request.js +++ b/src/web/src/config/request.js @@ -34,59 +34,73 @@ export async function apiRequest(url, options = {}, successMsg) { return data; } catch (err) { - notifications.show({ - title: "操作失败", - message: err.message || "接口调用失败", - color: "red", - }); + console.log("API 请求错误:", err); + // notifications.show({ + // title: "操作失败", + // message: err.message || "接口调用失败", + // color: "red", + // }); // throw err; // 继续抛出错误,方便上层处理 } // 返回 mock 数据 - if (url.includes("/api/v1/nodes")) { - if (/\/api\/v1\/nodes\/[^\/]+$/.test(url)) { + if (url.includes("/api/v1/master/nodes")) { + if (url.includes("/statistics")) { return { - "id": "node1", - "name": "Node 1", + "total": 30, + "status_statistics": [ + { "status": "online", "count": 20 }, + { "status": "offline", "count": 10 }, + ] + }; + } + if (/\/api\/v1\/master\/nodes\/[^\/]+$/.test(url)) { + return { + "id": "A1", // master分配的唯一ID, A代表Agent,数字1开始按顺序编号c + "name": "Node 1", // agent上报时提供的hostname "status": "online", "config": { - // !!! 格式待定, 下发给agent用的 - "max_connections": 100, - "timeout": 30, - "log_level": "info" + // !!! 预留字段,KV形式非固定key, web配置下发给agent用的 + "setting1": "value1", + "setting2": "value2", + "setting3": "value3", + "setting4": "value4" }, "meta_data": { - // 元数据: host, ip, - "host": "192.168.1.1", - "ip": "192.168.1.1", - "region": "shanghai", - "zone": "shanghai-1", + // 元数据: 容器生命周期内不变 + "hostname": "dev-yyrshare-nbnyx10-cp2f-pod-0", + "ip": "177.177.74.223", + "env": "dev", // 环境名, 从hostname中提取第一段 + "user": "yyrshare", // 账户名,从hostname中提取第二段 + "instance": "nbnyx10", // 容器示例名,从hostname中提取第三段 + "cpu_number": 16, + "memory_in_bytes": 2015040000, + "gpu_number": 8 }, - "label": { + "label": [ // 用户或运维人员绑定到节点上的标签,业务属性, tag - "tag": "value1", + "gpu", "exp001" + ], + "health": { // agent收集到各端上模块的health描述文件 + "log": "", //字符串,转义,防止换行、引号 + "metric": "" }, - "health": { - "agent": "activate", - "log": "error", - "metric": "activate", - }, - "last_updated": "2023-10-05T12:00:00Z", - "type": "worker" + "register_time": "2023-10-03T12:00:00Z", // 节点注册时间 + "last_report": "2023-10-03T12:00:00Z", // 最近一次上报时间 + "last_updated": "2023-10-05T12:00:00Z", // 更新NodeObject落库时间戳 + "type": "agent" // 缺省为agent,未来可能有新的节点类型 } + } return { "total": 30, "data": [ - { id: "node1", name: "节点A", status: "healthy", type: "agent", version: "1.0.0" }, - { id: "node2", name: "节点B", status: "warning", type: "agent", version: "1.0.0"}, - { id: "node3", name: "节点C", status: "error", type: "agent", version: "1.0.0"}, - { id: "node4", name: "节点D", status: "healthy", type: "agent", version: "1.0.0"}, - { id: "node5", name: "节点E", status: "warning", type: "agent", version: "1.0.0"}, - { id: "node6", name: "节点F", status: "error", type: "agent", version: "1.0.0"}, - { id: "node7", name: "节点G", status: "healthy", type: "agent", version: "1.0.0"}, - { id: "node8", name: "节点H", status: "warning", type: "agent", version: "1.0.0"}, + { id: "node1", name: "节点A", status: "online", type: "agent", version: "1.0.0" }, + { id: "node2", name: "节点B", status: "online", type: "agent", version: "1.0.0" }, + { id: "node3", name: "节点C", status: "offline", type: "agent", version: "1.0.0" }, + { id: "node4", name: "节点D", status: "online", type: "agent", version: "1.0.0" }, + { id: "node5", name: "节点E", status: "online", type: "agent", version: "1.0.0" }, ] }; } diff --git a/src/web/src/config/status.js b/src/web/src/config/status.js index ccf4964..d6a1a48 100644 --- a/src/web/src/config/status.js +++ b/src/web/src/config/status.js @@ -7,9 +7,8 @@ import { } from "@tabler/icons-react"; export const statusMap = { - healthy: { label: "Healthy", color: "green", labelCn: "健康" }, - warning: { label: "Warning", color: "orange", labelCn: "警告" }, - error: { label: "Error", color: "red", labelCn: "错误" }, + online: { label: "Online", color: "green"}, + offline: { label: "Offline", color: "red"}, }; export const statusOptions = Object.entries(statusMap).map(([value, { label }]) => ({ diff --git a/src/web/src/config/utils.js b/src/web/src/config/utils.js new file mode 100644 index 0000000..f5a8f05 --- /dev/null +++ b/src/web/src/config/utils.js @@ -0,0 +1,15 @@ +export function formatRelativeTime(dateStr) { + if (!dateStr) return "-"; + const date = new Date(dateStr); + const now = new Date(); + const diffMs = now - date; + const diffSec = Math.floor(diffMs / 1000); + const diffMin = Math.floor(diffSec / 60); + const diffHour = Math.floor(diffMin / 60); + const diffDay = Math.floor(diffHour / 24); + + if (diffSec < 60) return `${diffSec} 秒前`; + if (diffMin < 60) return `${diffMin} 分钟前`; + if (diffHour < 24) return `${diffHour} 小时前`; + return `${diffDay} 天前`; +} diff --git a/src/web/src/pages/Alerts.jsx b/src/web/src/pages/Alerts.jsx index 898bc08..641cd21 100644 --- a/src/web/src/pages/Alerts.jsx +++ b/src/web/src/pages/Alerts.jsx @@ -1,18 +1,160 @@ -import { Grid, Stack, Title } from "@mantine/core"; -import EntryCard from "../components/EntryCard"; -import { alertsEntries } from "../config/entries"; +import { useEffect, useState, useMemo } from "react"; +import { Stack, Title, Loader, Center, Group, Button, Badge, ActionIcon } from "@mantine/core"; +import { IconRefresh } from "@tabler/icons-react"; +import { apiRequest } from "../config/request"; +import { EXTERNAL_API } from "../config/api"; +import { AlertStats } from "../components/AlertStats"; +import { AlertFilters } from "../components/AlertFilters"; +import { AlertTable } from "../components/AlertTable"; +import { formatRelativeTime } from "../config/utils"; +import { EXTERNAL_HOST } from "../config/api"; export default function Alerts() { + const [alerts, setAlerts] = useState([]); + const [stats, setStats] = useState({ critical: 0, warning: 0, info: 0 }); + const [loading, setLoading] = useState(true); + const [filters, setFilters] = useState({ severity: "all", state: "all", instance: "all" }); + const [page, setPage] = useState(1); + const pageSize = 10; + const [sortConfig, setSortConfig] = useState({ key: "startsAt", direction: "desc" }); + + async function fetchAlerts() { + setLoading(true); + const data = await apiRequest(EXTERNAL_API.ALERTS_INFOS); + if (data && Array.isArray(data)) { + setAlerts(data); + const counts = { critical: 0, warning: 0, info: 0 }; + data.forEach((alert) => { + const sev = alert.labels?.severity || "info"; + if (sev === "critical") counts.critical++; + else if (sev === "warning") counts.warning++; + else counts.info++; + }); + setStats(counts); + } + setLoading(false); + } + + useEffect(() => { + fetchAlerts(); + const timer = setInterval(fetchAlerts, 30000); + return () => clearInterval(timer); + }, []); + + // 节点选项 + const nodeOptions = useMemo(() => { + const nodes = Array.from(new Set(alerts.map((a) => a.labels?.instance).filter(Boolean))); + return [{ value: "all", label: "全部" }, ...nodes.map((n) => ({ value: n, label: n }))]; + }, [alerts]); + + // 过滤 & 排序 & 分页逻辑 + const filteredAlerts = useMemo(() => { + return alerts.filter((alert) => { + const sev = alert.labels?.severity || "info"; + const state = alert.status?.state || "active"; + const instance = alert.labels?.instance || ""; + return ( + (filters.severity === "all" || filters.severity === sev) && + (filters.state === "all" || filters.state === state) && + (filters.instance === "all" || filters.instance === instance) + ); + }); + }, [alerts, filters]); + + const sortedAlerts = useMemo(() => { + const sorted = [...filteredAlerts]; + if (sortConfig.key) { + sorted.sort((a, b) => { + let valA, valB; + if (sortConfig.key === "severity") { + const map = { critical: 3, warning: 2, info: 1 }; + valA = map[a.labels?.severity] || 0; + valB = map[b.labels?.severity] || 0; + } else if (["startsAt", "endsAt", "updatedAt"].includes(sortConfig.key)) { + valA = new Date(a[sortConfig.key]).getTime() || 0; + valB = new Date(b[sortConfig.key]).getTime() || 0; + } else if (sortConfig.key === "instance") { + valA = a.labels?.instance || ""; + valB = b.labels?.instance || ""; + } else { + valA = a.labels?.alertname || ""; + valB = b.labels?.alertname || ""; + } + if (valA < valB) return sortConfig.direction === "asc" ? -1 : 1; + if (valA > valB) return sortConfig.direction === "asc" ? 1 : -1; + return 0; + }); + } + return sorted; + }, [filteredAlerts, sortConfig]); + + const paginatedAlerts = useMemo(() => { + const start = (page - 1) * pageSize; + return sortedAlerts.slice(start, start + pageSize); + }, [sortedAlerts, page]); + + // 颜色 & Badge + const getRowColor = (alert) => { + if (alert.status?.state === "resolved") return "gray.1"; + const sev = alert.labels?.severity; + if (sev === "critical") return "red.0"; + if (sev === "warning") return "orange.0"; + if (sev === "info") return "blue.0"; + return undefined; + }; + const getSeverityColor = (sev) => { + if (sev === "critical") return "red"; + if (sev === "warning") return "orange"; + if (sev === "info") return "blue"; + return "gray"; + }; + const getStateBadge = (state) => ( + + {state} + + ); + const handleSort = (key) => { + setSortConfig((prev) => ({ + key, + direction: prev.key === key && prev.direction === "asc" ? "desc" : "asc", + })); + }; + return ( - 告警详情 - - {alertsEntries.map((entry) => ( - - - - ))} - + + 告警详情 + + + + + + + + + + {loading ? ( +
+ +
+ ) : ( + + )}
); } diff --git a/src/web/src/pages/Dashboard.jsx b/src/web/src/pages/Dashboard.jsx index 561cd94..3bb9cf5 100644 --- a/src/web/src/pages/Dashboard.jsx +++ b/src/web/src/pages/Dashboard.jsx @@ -1,10 +1,10 @@ import { useEffect, useState } from "react"; import { Grid, Text } from "@mantine/core"; -import { ClusterTable } from "../components/DashboardNodeTable"; +import { NodeTable } from "../components/NodeTable"; import { HealthCard } from "../components/HealthCard"; -import { AlertCard } from "../components/AlertCard"; +import { AlertStats } from "../components/AlertStats"; import { apiRequest } from "../config/request"; -import { INTERNAL_API, EXTERNAL_API, EXTERNAL_HOST } from "../config/api"; +import { EXTERNAL_API } from "../config/api"; export default function Dashboard() { const [cluster, setCluster] = useState(null); @@ -12,14 +12,25 @@ export default function Dashboard() { const [alerts, setAlerts] = useState(null); const [loading, setLoading] = useState(true); + const countAlerts = (data) => { + const stats = { critical: 0, warning: 0, info: 0 }; + data?.forEach((alert) => { + const severity = alert.labels?.severity || "info"; + if (severity === "critical") stats.critical++; + else if (severity === "warning") stats.warning++; + else stats.info++; + }); + return stats; + }; + useEffect(() => { async function fetchData() { setLoading(true); try { const [clusterRes, healthRes, alertsRes] = await Promise.all([ apiRequest(EXTERNAL_API.MASTER_NODES), - apiRequest(INTERNAL_API.HEALTH), - apiRequest(INTERNAL_API.ALERTS), + apiRequest(EXTERNAL_API.MASTER_NODES_STATISTICS), + apiRequest(EXTERNAL_API.ALERTS_INFOS), ]); setCluster({ @@ -28,17 +39,11 @@ export default function Dashboard() { }); setHealth({ - healthy_nodes: healthRes?.healthy_nodes || 0, - warning_nodes: healthRes?.warning_nodes || 0, - error_nodes: healthRes?.error_nodes || 0, + total: healthRes?.total || 0, + status_statistics: healthRes?.status_statistics || [], }); - setAlerts({ - total_alerts: alertsRes?.total_alerts || 0, - critical_alerts: alertsRes?.critical_alerts || 0, - major_alerts: alertsRes?.major_alerts || 0, - minor_alerts: alertsRes?.minor_alerts || 0, - }); + setAlerts(countAlerts(alertsRes?.data || [])); } catch (err) { console.error("获取 Dashboard 数据失败:", err); } finally { @@ -59,9 +64,12 @@ export default function Dashboard() { return ( - + - + + + + ); } diff --git a/src/web/src/pages/NodePage.jsx b/src/web/src/pages/NodePage.jsx index 7ac8d00..120966f 100644 --- a/src/web/src/pages/NodePage.jsx +++ b/src/web/src/pages/NodePage.jsx @@ -1,58 +1,14 @@ -import { useState, useEffect } from "react"; -import { - Table, - Button, - TextInput, - Select, - Loader, - Center, - Text, - Grid, -} from "@mantine/core"; -import { EXTERNAL_API } from "../config/api"; -import PaginationControl from "../components/PaginationControl"; -import NodeStatus from "../components/NodeStatus"; -import NodeModal from "../components/NodeModal"; -import NodeDetailDrawer from "../components/NodeDetailDrawer"; +import { useState } from "react"; +import { Grid } from "@mantine/core"; import { apiRequest } from "../config/request"; +import { EXTERNAL_API } from "../config/api"; +import { NodeTable } from "../components/NodeTable"; +import NodeDetailDrawer from "../components/NodeDetailDrawer"; export default function NodePage() { - const [nodes, setNodes] = useState([]); - const [totalCount, setTotalCount] = useState(0); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(5); - const [modalOpen, setModalOpen] = useState(false); - const [editNode, setEditNode] = useState(null); - const [loading, setLoading] = useState(false); - - // 查询条件 - const [searchName, setSearchName] = useState(""); - const [searchStatus, setSearchStatus] = useState(""); - - // 详情相关 const [selectedNode, setSelectedNode] = useState(null); - const [detailLoading, setDetailLoading] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false); - - // 获取节点数据(支持查询和分页) - const fetchNodes = async (params = {}) => { - setLoading(true); - try { - // 构建查询参数 - const query = new URLSearchParams({ - page: params.page || page, - pageSize: params.pageSize || pageSize, - name: params.name !== undefined ? params.name : searchName, - status: params.status !== undefined ? params.status : searchStatus, - }).toString(); - - const result = await apiRequest(`${EXTERNAL_API.MASTER_NODES}?${query}`); - setNodes(result.data); - setTotalCount(result.total || 0); - } finally { - setLoading(false); - } - }; + const [detailLoading, setDetailLoading] = useState(false); // 获取节点详情 const fetchNodeDetail = async (id) => { @@ -65,124 +21,17 @@ export default function NodePage() { setDetailLoading(false); } }; - useEffect(() => { - fetchNodes(); - }, []); - - // 搜索 - const handleSearch = () => { - setPage(1); // 搜索时分页重置 - fetchNodes({ page: 1, name: searchName, status: searchStatus }); - }; return ( - {/* 左侧:表格 */} - - {/* 顶部操作区 */} -
- {/* 搜索区 */} -
- setSearchName(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") handleSearch(); - }} - style={{ width: 200 }} - /> -