diff --git a/src/web/.gitignore b/src/web/.gitignore index 89416bc..fbc6e04 100644 --- a/src/web/.gitignore +++ b/src/web/.gitignore @@ -41,3 +41,5 @@ Thumbs.db # Misc *.log + +.vite/ diff --git a/src/web/Dockerfile b/src/web/Dockerfile deleted file mode 100644 index 0e761ee..0000000 --- a/src/web/Dockerfile +++ /dev/null @@ -1,36 +0,0 @@ -# ---- 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/README.md b/src/web/README.md index 7059a96..aa74af7 100644 --- a/src/web/README.md +++ b/src/web/README.md @@ -1,12 +1,12 @@ -# React + Vite +# Argus-web -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +架构:React + Vite + Mantine -Currently, two official plugins are available: -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +## 打包部署 +根目录下运行 +```bash +bash src/web/buld_tools/frontend/build.sh +``` -## Expanding the ESLint configuration -If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/src/web/build_tools/front_end/build.sh b/src/web/build_tools/front_end/build.sh deleted file mode 100644 index 20ba270..0000000 --- a/src/web/build_tools/front_end/build.sh +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 0c3b3bc..0000000 --- a/src/web/build_tools/front_end/nginx.conf +++ /dev/null @@ -1,13 +0,0 @@ -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/frontend/Dockerfile b/src/web/build_tools/frontend/Dockerfile new file mode 100644 index 0000000..4363188 --- /dev/null +++ b/src/web/build_tools/frontend/Dockerfile @@ -0,0 +1,89 @@ +# ========== 构建阶段 ========== +FROM node:20 AS builder + +# 设置工作目录 +WORKDIR /app/src/web + +# 复制依赖文件并安装 +COPY src/web/package*.json ./ + +RUN npm install + +# 复制源码并打包 +COPY src/web ./ +RUN npm run build + +# ========== 运行阶段 ========== +FROM ubuntu:24.04 + +USER root + +# 安装 nginx 和 supervisor +RUN apt-get update && \ + apt-get install -y nginx supervisor curl vim net-tools inetutils-ping ca-certificates passwd && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +ENV FRONTEND_BASE_PATH=/private/argus/web/frontend + +RUN mkdir -p ${FRONTEND_BASE_PATH} && \ + mkdir -p /private/argus/etc + +# 创建 web 用户(可自定义 UID/GID) +# 创建 web 用户组 +RUN groupadd -g 2015 web + +# 创建 web 用户并指定组 +RUN useradd -M -s /usr/sbin/nologin -u 2133 -g 2015 web + +RUN chown -R web:web ${FRONTEND_BASE_PATH} && \ + chown -R web:web /private/argus/etc && \ + chown -R web:web /usr/local/bin + +# 配置内网 apt 源 (如果指定了内网选项) +RUN if [ "$USE_INTRANET" = "true" ]; then \ + echo "Configuring intranet apt sources..." && \ + cp /etc/apt/sources.list /etc/apt/sources.list.bak && \ + echo "deb [trusted=yes] http://10.68.64.1/ubuntu2204/ jammy main" > /etc/apt/sources.list && \ + echo 'Acquire::https::Verify-Peer "false";' > /etc/apt/apt.conf.d/99disable-ssl-check && \ + echo 'Acquire::https::Verify-Host "false";' >> /etc/apt/apt.conf.d/99disable-ssl-check; \ + fi + + +# 配置部署时使用的 apt 源 +RUN if [ "$USE_INTRANET" = "true" ]; then \ + echo "deb [trusted=yes] https://10.92.132.52/mirrors/ubuntu2204/ jammy main" > /etc/apt/sources.list; \ + fi + +# 前端编译产物放到 nginx 目录 +COPY --from=builder /app/src/web/dist /usr/share/nginx/html + +# 复制 nginx 配置(保证 React 前端路由兼容) +COPY src/web/build_tools/frontend/nginx.conf /etc/nginx/nginx.conf +# COPY src/web/build_tools/frontend/conf.d/ /etc/nginx/conf.d/ + +# 复制 supervisor 配置 +COPY src/web/build_tools/frontend/supervisord.conf /etc/supervisor/conf.d/supervisord.conf + +# 创建 supervisor 日志目录 +RUN mkdir -p /var/log/supervisor + +# 复制启动脚本 +COPY src/web/build_tools/frontend/start-web-supervised.sh /usr/local/bin/start-web-supervised.sh +RUN chmod +x /usr/local/bin/start-web-supervised.sh + +# 复制 DNS 监控脚本 +COPY src/web/build_tools/frontend/dns-monitor.sh /usr/local/bin/dns-monitor.sh +RUN chmod +x /usr/local/bin/dns-monitor.sh + +# 复制健康检查脚本 +COPY src/web/build_tools/frontend/health-check.sh /usr/local/bin/health-check.sh +RUN chmod +x /usr/local/bin/health-check.sh + +# 暴露端口 +EXPOSE 80 + +# 保持 root 用户,由 supervisor 控制 user 切换 +USER root + +# 以 supervisor 为入口 +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/src/web/build_tools/frontend/build.sh b/src/web/build_tools/frontend/build.sh new file mode 100644 index 0000000..31bf953 --- /dev/null +++ b/src/web/build_tools/frontend/build.sh @@ -0,0 +1,4 @@ +docker pull node:20 +docker pull ubuntu:24.04 +docker build -f src/web/build_tools/frontend/Dockerfile -t argus-web:0.1.1 . +rm -f argus-web-0.1.1.tar && sudo docker image save argus-web:0.1.1 > argus-web-0.1.1.tar diff --git a/src/web/build_tools/frontend/dns-monitor.sh b/src/web/build_tools/frontend/dns-monitor.sh new file mode 100644 index 0000000..2890b47 --- /dev/null +++ b/src/web/build_tools/frontend/dns-monitor.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +# DNS监控脚本 - 每10秒检查dns.conf是否有变化 +# 如果有变化则执行update-dns.sh脚本 + +DNS_CONF="/private/argus/etc/dns.conf" +DNS_BACKUP="/tmp/dns.conf.backup" +UPDATE_SCRIPT="/private/argus/etc/update-dns.sh" +LOG_FILE="/var/log/supervisor/dns-monitor.log" + +# 确保日志文件存在 +touch "$LOG_FILE" + +log_message() { + echo "$(date '+%Y-%m-%d %H:%M:%S') [DNS-Monitor] $1" >> "$LOG_FILE" +} + +log_message "DNS监控脚本启动" + +while true; do + if [ -f "$DNS_CONF" ]; then + if [ -f "$DNS_BACKUP" ]; then + # 比较文件内容 + if ! cmp -s "$DNS_CONF" "$DNS_BACKUP"; then + log_message "检测到DNS配置变化" + + # 更新备份文件 + cp "$DNS_CONF" "$DNS_BACKUP" + + # 执行更新脚本 + if [ -x "$UPDATE_SCRIPT" ]; then + log_message "执行DNS更新脚本: $UPDATE_SCRIPT" + "$UPDATE_SCRIPT" >> "$LOG_FILE" 2>&1 + if [ $? -eq 0 ]; then + log_message "DNS更新脚本执行成功" + else + log_message "DNS更新脚本执行失败" + fi + else + log_message "警告: 更新脚本不存在或不可执行: $UPDATE_SCRIPT" + fi + fi + else + + # 第一次检测到配置文件,执行更新脚本 + if [ -x "$UPDATE_SCRIPT" ]; then + log_message "执行DNS更新脚本: $UPDATE_SCRIPT" + "$UPDATE_SCRIPT" >> "$LOG_FILE" 2>&1 + if [ $? -eq 0 ]; then + log_message "DNS更新脚本执行成功" + + # 第一次运行,创建备份并执行更新 + cp "$DNS_CONF" "$DNS_BACKUP" + log_message "创建DNS配置备份文件" + + else + log_message "DNS更新脚本执行失败" + fi + else + log_message "警告: 更新脚本不存在或不可执行: $UPDATE_SCRIPT" + fi + fi + else + log_message "警告: DNS配置文件不存在: $DNS_CONF" + fi + + sleep 10 +done diff --git a/src/web/build_tools/frontend/health-check.sh b/src/web/build_tools/frontend/health-check.sh new file mode 100644 index 0000000..849e1b4 --- /dev/null +++ b/src/web/build_tools/frontend/health-check.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -euo pipefail + +URL="http://127.0.0.1:80" + +echo "[INFO] Starting frontend health check loop for $URL..." + +while true; do + if curl -s --max-time 5 "$URL" > /dev/null; then + echo "[OK] $(date '+%Y-%m-%d %H:%M:%S') Frontend is healthy" + else + echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') Frontend health check failed" + exit 1 + fi + sleep 10 +done diff --git a/src/web/build_tools/frontend/nginx.conf b/src/web/build_tools/frontend/nginx.conf new file mode 100644 index 0000000..4d2cecb --- /dev/null +++ b/src/web/build_tools/frontend/nginx.conf @@ -0,0 +1,56 @@ +user web; +worker_processes auto; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + sendfile on; + + # React 前端服务 + server { + listen 80; + server_name web.argus.com; + + root /usr/share/nginx/html; + index index.html; + + # React 前端路由兼容 + location / { + try_files $uri /index.html; + } + } + + + # Master 服务,需要增加 CORS 支持 + server { + listen 80; + server_name master.argus.com; + + location / { + proxy_pass http://master.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; + + # CORS 支持 + add_header 'Access-Control-Allow-Origin' 'http://web.argus.com' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always; + + if ($request_method = OPTIONS) { + add_header 'Access-Control-Allow-Origin' 'http://web.argus.com' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always; + add_header 'Content-Length' 0; + add_header 'Content-Type' 'text/plain'; + return 204; + } + } +} + +} diff --git a/src/web/build_tools/frontend/start-web-supervised.sh b/src/web/build_tools/frontend/start-web-supervised.sh new file mode 100644 index 0000000..8e94777 --- /dev/null +++ b/src/web/build_tools/frontend/start-web-supervised.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -euo pipefail + +echo "[INFO] Starting React frontend under supervisor..." + +DNS_DIR="/private/argus/etc" +DNS_SCRIPT="${DNS_DIR}/update-dns.sh" +DOMAIN=web.argus.com +WEB_DOMAIN_FILE="${DNS_DIR}/${DOMAIN}" +RUNTIME_USER="${ARGUS_RUNTIME_USER:-argus}" +RUNTIME_UID="${ARGUS_BUILD_UID:-2133}" +RUNTIME_GID="${ARGUS_BUILD_GID:-2015}" + +mkdir -p "$DNS_DIR" +chown -R "$RUNTIME_UID:$RUNTIME_GID" "$DNS_DIR" 2>/dev/null || true + +if [[ -x "$DNS_SCRIPT" ]]; then + echo "[INFO] Running update-dns.sh before master starts" + # 若脚本存在则执行,保证容器使用 bind 作为 DNS + "$DNS_SCRIPT" || echo "[WARN] update-dns.sh execution failed" +else + echo "[WARN] DNS update script not found or not executable: $DNS_SCRIPT" +fi + + +# 记录容器 IP +IP=$(ifconfig | grep -A 1 eth0 | grep inet | awk '{print $2}' || true) +if [[ -n "${IP}" ]]; then + echo "current IP: ${IP}" + echo "${IP}" > "$WEB_DOMAIN_FILE" + chown "$RUNTIME_UID:$RUNTIME_GID" "$WEB_DOMAIN_FILE" 2>/dev/null || true +else + echo "[WARN] Failed to detect web IP via ifconfig" +fi + +echo "[INFO] Launching nginx..." + +# 启动 nginx 前台模式 +exec /usr/sbin/nginx -g "daemon off;" diff --git a/src/web/build_tools/frontend/supervisord.conf b/src/web/build_tools/frontend/supervisord.conf new file mode 100644 index 0000000..7e08949 --- /dev/null +++ b/src/web/build_tools/frontend/supervisord.conf @@ -0,0 +1,39 @@ +[supervisord] +nodaemon=true +logfile=/var/log/supervisor/supervisord.log +pidfile=/var/run/supervisord.pid +user=root + +[program:web] +command=/usr/local/bin/start-web-supervised.sh +user=root +stdout_logfile=/var/log/supervisor/web.log +stderr_logfile=/var/log/supervisor/web_error.log +autorestart=true +startretries=3 +startsecs=5 +stopwaitsecs=10 +killasgroup=true +stopasgroup=true + +[program:web-health] +command=/usr/local/bin/health-check.sh +user=web +stdout_logfile=/var/log/supervisor/web-health.log +stderr_logfile=/var/log/supervisor/web-health_error.log +autorestart=true +startretries=3 +startsecs=5 +stopwaitsecs=10 +killasgroup=true +stopasgroup=true + +[unix_http_server] +file=/var/run/supervisor.sock +chmod=0700 + +[supervisorctl] +serverurl=unix:///var/run/supervisor.sock + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface diff --git a/src/web/build_tools/proxy/Dockerfile b/src/web/build_tools/proxy/Dockerfile index 7162426..ceac538 100644 --- a/src/web/build_tools/proxy/Dockerfile +++ b/src/web/build_tools/proxy/Dockerfile @@ -1,17 +1,67 @@ -# 使用轻量级 Nginx 基础镜像 -FROM nginx:1.25-alpine +FROM ubuntu:24.04 -# 删除默认配置 -RUN rm -rf /etc/nginx/conf.d/* +USER root -# 复制自定义 Proxy 配置 -# (可以在构建时直接COPY进去,也可以运行时挂载) -COPY conf.d/ /etc/nginx/conf.d/ +# 安装 nginx 和 supervisor +RUN apt-get update && \ + apt-get install -y nginx supervisor curl vim net-tools inetutils-ping ca-certificates passwd && \ + apt-get clean && rm -rf /var/lib/apt/lists/* -# 日志目录(可选) -VOLUME ["/var/log/nginx"] +ENV FRONTEND_BASE_PATH=/private/argus/web/proxy + +RUN mkdir -p ${FRONTEND_BASE_PATH} && \ + mkdir -p /private/argus/etc + +# 创建 proxy 用户(可自定义 UID/GID) +# 创建 proxy 用户组 +RUN groupadd -g 2015 web_proxy + +# 创建 proxy 用户并指定组 +RUN useradd -M -s /usr/sbin/nologin -u 2133 -g 2015 web_proxy + +RUN chown -R web_proxy:web_proxy ${FRONTEND_BASE_PATH} && \ + chown -R web_proxy:web_proxy /private/argus/etc && \ + chown -R web_proxy:web_proxy /usr/local/bin + +# 配置内网 apt 源 (如果指定了内网选项) +RUN if [ "$USE_INTRANET" = "true" ]; then \ + echo "Configuring intranet apt sources..." && \ + cp /etc/apt/sources.list /etc/apt/sources.list.bak && \ + echo "deb [trusted=yes] http://10.68.64.1/ubuntu2204/ jammy main" > /etc/apt/sources.list && \ + echo 'Acquire::https::Verify-Peer "false";' > /etc/apt/apt.conf.d/99disable-ssl-check && \ + echo 'Acquire::https::Verify-Host "false";' >> /etc/apt/apt.conf.d/99disable-ssl-check; \ + fi + + +# 配置部署时使用的 apt 源 +RUN if [ "$USE_INTRANET" = "true" ]; then \ + echo "deb [trusted=yes] https://10.92.132.52/mirrors/ubuntu2204/ jammy main" > /etc/apt/sources.list; \ + fi + + +# 复制 nginx 配置(保证 React 前端路由兼容) +COPY src/web/build_tools/proxy/nginx.conf /etc/nginx/nginx.conf +COPY src/web/build_tools/proxy/conf.d/ /etc/nginx/conf.d/ + +# 复制 supervisor 配置 +COPY src/web/build_tools/proxy/supervisord.conf /etc/supervisor/conf.d/supervisord.conf + +# 创建 supervisor 日志目录 +RUN mkdir -p /var/log/supervisor + +# 复制启动脚本 +COPY src/web/build_tools/proxy/start-proxy-supervised.sh /usr/local/bin/start-proxy-supervised.sh +RUN chmod +x /usr/local/bin/start-proxy-supervised.sh + +# 复制 DNS 监控脚本 +COPY src/web/build_tools/proxy/dns-monitor.sh /usr/local/bin/dns-monitor.sh +RUN chmod +x /usr/local/bin/dns-monitor.sh # 暴露端口 -EXPOSE 80 443 +EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] +# 保持 root 用户,由 supervisor 控制 user 切换 +USER root + +# 以 supervisor 为入口 +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/src/web/build_tools/proxy/argus-proxy.tar.gz b/src/web/build_tools/proxy/argus-proxy.tar.gz deleted file mode 100644 index c46a22f..0000000 Binary files a/src/web/build_tools/proxy/argus-proxy.tar.gz and /dev/null differ diff --git a/src/web/build_tools/proxy/argus.nginx.conf b/src/web/build_tools/proxy/argus.nginx.conf deleted file mode 100644 index 0316133..0000000 --- a/src/web/build_tools/proxy/argus.nginx.conf +++ /dev/null @@ -1,71 +0,0 @@ -# 门户前端 (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 deleted file mode 100644 index 78f1acb..0000000 --- a/src/web/build_tools/proxy/build_image.sh +++ /dev/null @@ -1,2 +0,0 @@ -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/alert.conf b/src/web/build_tools/proxy/conf.d/alert.conf new file mode 100644 index 0000000..1f8987f --- /dev/null +++ b/src/web/build_tools/proxy/conf.d/alert.conf @@ -0,0 +1,8 @@ +server { + listen 80; + server_name alertmanager.alert.argus.com; + + location / { + proxy_pass http://alertmanager.alert.argus.com; + } +} diff --git a/src/web/build_tools/proxy/conf.d/argus.conf b/src/web/build_tools/proxy/conf.d/argus.conf deleted file mode 100644 index 786c293..0000000 --- a/src/web/build_tools/proxy/conf.d/argus.conf +++ /dev/null @@ -1,78 +0,0 @@ -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/build_tools/proxy/conf.d/log.conf b/src/web/build_tools/proxy/conf.d/log.conf new file mode 100644 index 0000000..97858cc --- /dev/null +++ b/src/web/build_tools/proxy/conf.d/log.conf @@ -0,0 +1,19 @@ +# Elasticsearch +server { + listen 80; + server_name es.log.argus.com; + + location / { + proxy_pass http://es.log.argus.com; + } +} + +# Kibana +server { + listen 80; + server_name kibana.log.argus.com; + + location / { + proxy_pass http://kibana.log.argus.com; + } +} diff --git a/src/web/build_tools/proxy/conf.d/master.conf b/src/web/build_tools/proxy/conf.d/master.conf new file mode 100644 index 0000000..1b6e61f --- /dev/null +++ b/src/web/build_tools/proxy/conf.d/master.conf @@ -0,0 +1,26 @@ +server { + listen 80; + server_name master.argus.com; + + location / { + proxy_pass http://master.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; + + # CORS 支持 + add_header 'Access-Control-Allow-Origin' 'http://web.argus.com' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always; + + if ($request_method = OPTIONS) { + add_header 'Access-Control-Allow-Origin' 'http://web.argus.com' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always; + add_header 'Content-Length' 0; + add_header 'Content-Type' 'text/plain'; + return 204; + } + } +} diff --git a/src/web/build_tools/proxy/conf.d/metric.conf b/src/web/build_tools/proxy/conf.d/metric.conf new file mode 100644 index 0000000..ec0dd4b --- /dev/null +++ b/src/web/build_tools/proxy/conf.d/metric.conf @@ -0,0 +1,19 @@ +# Prometheus +server { + listen 80; + server_name prometheus.metric.argus.com; + + location / { + proxy_pass http://prometheus.metric.argus.com; + } +} + +# Grafana +server { + listen 80; + server_name grafana.metric.argus.com; + + location / { + proxy_pass http://grafana.metric.argus.com; + } +} diff --git a/src/web/build_tools/proxy/conf.d/web.conf b/src/web/build_tools/proxy/conf.d/web.conf new file mode 100644 index 0000000..cc58b34 --- /dev/null +++ b/src/web/build_tools/proxy/conf.d/web.conf @@ -0,0 +1,8 @@ +server { + listen 80; + server_name web.argus.com; + + location / { + proxy_pass http://web.argus.com; + } +} diff --git a/src/web/build_tools/proxy/dns-monitor.sh b/src/web/build_tools/proxy/dns-monitor.sh new file mode 100644 index 0000000..2890b47 --- /dev/null +++ b/src/web/build_tools/proxy/dns-monitor.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +# DNS监控脚本 - 每10秒检查dns.conf是否有变化 +# 如果有变化则执行update-dns.sh脚本 + +DNS_CONF="/private/argus/etc/dns.conf" +DNS_BACKUP="/tmp/dns.conf.backup" +UPDATE_SCRIPT="/private/argus/etc/update-dns.sh" +LOG_FILE="/var/log/supervisor/dns-monitor.log" + +# 确保日志文件存在 +touch "$LOG_FILE" + +log_message() { + echo "$(date '+%Y-%m-%d %H:%M:%S') [DNS-Monitor] $1" >> "$LOG_FILE" +} + +log_message "DNS监控脚本启动" + +while true; do + if [ -f "$DNS_CONF" ]; then + if [ -f "$DNS_BACKUP" ]; then + # 比较文件内容 + if ! cmp -s "$DNS_CONF" "$DNS_BACKUP"; then + log_message "检测到DNS配置变化" + + # 更新备份文件 + cp "$DNS_CONF" "$DNS_BACKUP" + + # 执行更新脚本 + if [ -x "$UPDATE_SCRIPT" ]; then + log_message "执行DNS更新脚本: $UPDATE_SCRIPT" + "$UPDATE_SCRIPT" >> "$LOG_FILE" 2>&1 + if [ $? -eq 0 ]; then + log_message "DNS更新脚本执行成功" + else + log_message "DNS更新脚本执行失败" + fi + else + log_message "警告: 更新脚本不存在或不可执行: $UPDATE_SCRIPT" + fi + fi + else + + # 第一次检测到配置文件,执行更新脚本 + if [ -x "$UPDATE_SCRIPT" ]; then + log_message "执行DNS更新脚本: $UPDATE_SCRIPT" + "$UPDATE_SCRIPT" >> "$LOG_FILE" 2>&1 + if [ $? -eq 0 ]; then + log_message "DNS更新脚本执行成功" + + # 第一次运行,创建备份并执行更新 + cp "$DNS_CONF" "$DNS_BACKUP" + log_message "创建DNS配置备份文件" + + else + log_message "DNS更新脚本执行失败" + fi + else + log_message "警告: 更新脚本不存在或不可执行: $UPDATE_SCRIPT" + fi + fi + else + log_message "警告: DNS配置文件不存在: $DNS_CONF" + fi + + sleep 10 +done diff --git a/src/web/build_tools/proxy/nginx.conf b/src/web/build_tools/proxy/nginx.conf new file mode 100644 index 0000000..0e630a0 --- /dev/null +++ b/src/web/build_tools/proxy/nginx.conf @@ -0,0 +1,27 @@ +user web_proxy; +worker_processes auto; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + sendfile on; + + # 使用系统 resolv.conf(由 update-dns.sh 动态更新) + resolver $(awk '/^nameserver/ {print $2}' /etc/resolv.conf | tr '\n' ' ') valid=30s ipv6=off; + + # 启用访问日志 + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # 反向代理默认头部 + 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; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/src/web/build_tools/proxy/start-proxy-supervised.sh b/src/web/build_tools/proxy/start-proxy-supervised.sh new file mode 100644 index 0000000..3c2fc0f --- /dev/null +++ b/src/web/build_tools/proxy/start-proxy-supervised.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -euo pipefail + +echo "[INFO] Starting proxy under supervisor..." + +DNS_DIR="/private/argus/etc" +DNS_SCRIPT="${DNS_DIR}/update-dns.sh" +RUNTIME_USER="${ARGUS_RUNTIME_USER:-argus}" +RUNTIME_UID="${ARGUS_BUILD_UID:-2133}" +RUNTIME_GID="${ARGUS_BUILD_GID:-2015}" + +mkdir -p "$DNS_DIR" +chown -R "$RUNTIME_UID:$RUNTIME_GID" "$DNS_DIR" 2>/dev/null || true + +if [[ -x "$DNS_SCRIPT" ]]; then + echo "[INFO] Running update-dns.sh before master starts" + # 若脚本存在则执行,保证容器使用 bind 作为 DNS + "$DNS_SCRIPT" || echo "[WARN] update-dns.sh execution failed" +else + echo "[WARN] DNS update script not found or not executable: $DNS_SCRIPT" +fi + + +echo "[INFO] Launching nginx..." + +# 启动 nginx 前台模式 +exec /usr/sbin/nginx -g "daemon off;" diff --git a/src/web/build_tools/proxy/supervisord.conf b/src/web/build_tools/proxy/supervisord.conf new file mode 100644 index 0000000..57bdfc5 --- /dev/null +++ b/src/web/build_tools/proxy/supervisord.conf @@ -0,0 +1,39 @@ +[supervisord] +nodaemon=true +logfile=/var/log/supervisor/supervisord.log +pidfile=/var/run/supervisord.pid +user=root + +[program:proxy] +command=/usr/local/bin/start-proxy-supervised.sh +user=root +stdout_logfile=/var/log/supervisor/web-proxy.log +stderr_logfile=/var/log/supervisor/web-proxy_error.log +autorestart=true +startretries=3 +startsecs=5 +stopwaitsecs=10 +killasgroup=true +stopasgroup=true + +[program:dns-monitor] +command=/usr/local/bin/dns-monitor.sh +user=root +stdout_logfile=/var/log/supervisor/dns-monitor.log +stderr_logfile=/var/log/supervisor/dns-monitor_error.log +autorestart=true +startretries=3 +startsecs=5 +stopwaitsecs=10 +killasgroup=true +stopasgroup=true + +[unix_http_server] +file=/var/run/supervisor.sock +chmod=0700 + +[supervisorctl] +serverurl=unix:///var/run/supervisor.sock + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface diff --git a/src/web/package-lock.json b/src/web/package-lock.json index 044f187..eb76c2e 100644 --- a/src/web/package-lock.json +++ b/src/web/package-lock.json @@ -8,6 +8,7 @@ "name": "argus-web", "version": "0.0.0", "dependencies": { + "@emotion/react": "^11.14.0", "@mantine/core": "^8.3.1", "@mantine/hooks": "^8.3.1", "@mantine/notifications": "^8.3.1", @@ -33,7 +34,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -89,7 +89,6 @@ "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.28.3", @@ -123,7 +122,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -133,7 +131,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -175,7 +172,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -185,7 +181,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -219,7 +214,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.4" @@ -276,7 +270,6 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -291,7 +284,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -310,7 +302,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -320,6 +311,126 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", @@ -1025,7 +1136,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1047,7 +1157,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1057,14 +1166,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1510,6 +1617,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.1.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", @@ -1614,6 +1727,21 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1669,7 +1797,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1765,6 +1892,31 @@ "node": ">=18" } }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1790,7 +1942,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1834,6 +1985,15 @@ "dev": true, "license": "ISC" }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/esbuild": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", @@ -1890,7 +2050,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -2129,6 +2288,12 @@ "node": ">=16.0.0" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2182,6 +2347,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2237,6 +2411,27 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2251,7 +2446,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -2274,6 +2468,27 @@ "node": ">=0.8.19" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2327,7 +2542,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -2343,6 +2557,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2394,6 +2614,12 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2456,7 +2682,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -2555,7 +2780,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -2564,6 +2788,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2584,11 +2826,25 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -2851,11 +3107,30 @@ "react-dom": ">=16.6.0" } }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -2947,6 +3222,15 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2970,6 +3254,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2983,6 +3273,18 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", @@ -3283,6 +3585,21 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/src/web/package.json b/src/web/package.json index 1a79550..0f50bf3 100644 --- a/src/web/package.json +++ b/src/web/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@emotion/react": "^11.14.0", "@mantine/core": "^8.3.1", "@mantine/hooks": "^8.3.1", "@mantine/notifications": "^8.3.1", diff --git a/src/web/src/components/NodeConfigCard.jsx b/src/web/src/components/NodeConfigCard.jsx index 6282aa0..17b44da 100644 --- a/src/web/src/components/NodeConfigCard.jsx +++ b/src/web/src/components/NodeConfigCard.jsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import { Card, Text, Group, TextInput, Stack, ActionIcon } from "@mantine/core"; import { IconEdit, IconX, IconCheck, IconPlus, IconTrash } from "@tabler/icons-react"; import { apiRequest } from "../config/request"; -import { EXTERNAL_API } from "../config/api"; +import { MASTER_API } from "../config/api"; export default function NodeConfigCard({ nodeId, config = {}, onSaved }) { const [editing, setEditing] = useState(false); @@ -37,12 +37,23 @@ export default function NodeConfigCard({ nodeId, config = {}, onSaved }) { const handleSave = async () => { setSaving(true); try { - const configObj = Object.fromEntries(configList); - await apiRequest(`${EXTERNAL_API.MASTER_NODES}/${nodeId}`, { + let finalList = [...configList]; + // 如果有未点击“+”的新配置,补充进去 + if (newKey && !finalList.find(([k]) => k === newKey)) { + finalList = [...finalList, [newKey, newValue]]; + setNewKey(""); + setNewValue(""); + } + + const configObj = Object.fromEntries(finalList); + + await apiRequest(MASTER_API.CONFIG(nodeId), { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ config: configObj }), }); + + setConfigList(finalList); // 更新 state,保持 UI 同步 setEditing(false); onSaved && onSaved(); } finally { @@ -50,6 +61,7 @@ export default function NodeConfigCard({ nodeId, config = {}, onSaved }) { } }; + return ( @@ -106,8 +118,14 @@ export default function NodeConfigCard({ nodeId, config = {}, onSaved }) { placeholder="新增 Value" value={newValue} onChange={(e) => setNewValue(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && addConfig()} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + addConfig(); + } + }} /> + diff --git a/src/web/src/components/NodeDetailDrawer.jsx b/src/web/src/components/NodeDetailDrawer.jsx index 4884e6e..455d8aa 100644 --- a/src/web/src/components/NodeDetailDrawer.jsx +++ b/src/web/src/components/NodeDetailDrawer.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import { Drawer, Text, @@ -9,10 +9,12 @@ import { Divider, ThemeIcon, Stack, + ActionIcon, } from "@mantine/core"; +import { IconRefresh } from "@tabler/icons-react"; import { healthStatus } from "../config/status"; import { apiRequest } from "../config/request"; -import { EXTERNAL_API } from "../config/api"; +import { MASTER_API } from "../config/api"; import NodeConfigCard from "./NodeConfigCard"; import NodeLabelCard from "./NodeLabelCard"; @@ -23,11 +25,11 @@ export default function NodeDetailDrawer({ opened, nodeId, onClose }) { const [node, setNode] = useState(null); const [loading, setLoading] = useState(false); - const fetchNodeDetail = async () => { - if (!nodeId) return; + const fetchNodeDetail = async (id) => { + if (!id) return; setLoading(true); try { - const res = await apiRequest(`${EXTERNAL_API.MASTER_NODES}/${nodeId}`); + const res = await apiRequest(MASTER_API.DETAIL(id)); setNode(res); } finally { setLoading(false); @@ -35,7 +37,7 @@ export default function NodeDetailDrawer({ opened, nodeId, onClose }) { }; useEffect(() => { - if (opened && nodeId) fetchNodeDetail(); + if (opened && nodeId) fetchNodeDetail(nodeId); }, [opened, nodeId]); return ( @@ -48,7 +50,7 @@ export default function NodeDetailDrawer({ opened, nodeId, onClose }) { padding="lg" overlayProps={{ backgroundOpacity: 0.4, blur: 4 }} > - {loading ? ( + {loading && !node ? (
@@ -64,25 +66,36 @@ export default function NodeDetailDrawer({ opened, nodeId, onClose }) { paddingBottom: 8, }} > - - - {healthStatus(node.status).icon} - + + + + {healthStatus(node.status).icon} + - - {node.name} - - {node.type} - {node.status} - - 最后更新时间: {new Date(node.last_updated).toLocaleString()} - + {node.name} + {node.type} + {node.status} + + 最近上报时间: {new Date(node.last_report).toLocaleString()} + + + + {/* 刷新按钮固定在右侧 */} + fetchNodeDetail(node.id)} + disabled={loading} + > + + + @@ -90,10 +103,10 @@ export default function NodeDetailDrawer({ opened, nodeId, onClose }) { {/* 配置信息 */} - + fetchNodeDetail(node.id)} /> {/* 标签信息 */} - + fetchNodeDetail(node.id)} /> {/* 元数据 */} @@ -105,6 +118,7 @@ export default function NodeDetailDrawer({ opened, nodeId, onClose }) { 注册时间: {new Date(node.register_time).toLocaleString()} 最近上报时间: {new Date(node.last_report).toLocaleString()} + 最后更新时间: {new Date(node.last_updated).toLocaleString()} diff --git a/src/web/src/components/NodeHealthCard.jsx b/src/web/src/components/NodeHealthCard.jsx index 0186772..2039320 100644 --- a/src/web/src/components/NodeHealthCard.jsx +++ b/src/web/src/components/NodeHealthCard.jsx @@ -1,14 +1,55 @@ -import { Card, Text, Stack } from "@mantine/core"; +import { useState } from "react"; +import { Card, Text, Stack, Group, ActionIcon, Badge, Popover } from "@mantine/core"; +import { IconInfoCircle } from "@tabler/icons-react"; export default function NodeHealthCard({ node }) { const health = node.health || {}; + const renderHealthItem = (moduleName, data) => { + const status = data?.status || "unknown"; + const color = status === "healthy" ? "green" : status === "unhealthy" ? "red" : "gray"; + const [opened, setOpened] = useState(false); + + return ( + + {moduleName} + {status} + {(data?.error || data?.timestamp) && ( + setOpened(false)} + position="bottom" + withArrow + shadow="sm" + > + + setOpened((o) => !o)}> + + + + + + {data.error && Error: {data.error}} + {data.timestamp && ( + + Updated: {new Date(data.timestamp).toLocaleString()} + + )} + + + + )} + + ); + }; + return ( 健康信息 - 日志: {health.log || "无"} - 指标: {health.metric || "无"} + {Object.entries(health).map(([moduleName, data]) => + renderHealthItem(moduleName, data) + )} ); diff --git a/src/web/src/components/NodeLabelCard.jsx b/src/web/src/components/NodeLabelCard.jsx index 2a6fbd0..6783a6f 100644 --- a/src/web/src/components/NodeLabelCard.jsx +++ b/src/web/src/components/NodeLabelCard.jsx @@ -2,7 +2,7 @@ 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"; +import { MASTER_API } from "../config/api"; export default function NodeLabelCard({ nodeId, labels = [], onSaved }) { const [editing, setEditing] = useState(false); @@ -12,7 +12,7 @@ export default function NodeLabelCard({ nodeId, labels = [], onSaved }) { const [saving, setSaving] = useState(false); const randomColor = () => { - const colors = ["red","pink","grape","violet","indigo","blue","cyan","teal","green","lime","yellow","orange","gray"]; + const colors = ["red", "pink", "grape", "violet", "indigo", "blue", "cyan", "teal", "green", "lime", "yellow", "orange", "gray"]; return colors[Math.floor(Math.random() * colors.length)]; }; @@ -42,11 +42,19 @@ export default function NodeLabelCard({ nodeId, labels = [], onSaved }) { const handleSave = async () => { setSaving(true); try { - await apiRequest(`${EXTERNAL_API.MASTER_NODES}/${nodeId}`, { + let finalTags = [...tagList]; + if (newTag && !finalTags.includes(newTag)) { + finalTags = [...finalTags, newTag]; + setNewTag(""); // 清空输入框 + } + + await apiRequest(MASTER_API.CONFIG(nodeId), { method: "PUT", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ label: tagList }), + body: JSON.stringify({ label: finalTags }), }); + + setTagList(finalTags); setEditing(false); onSaved && onSaved(); } finally { @@ -54,6 +62,7 @@ export default function NodeLabelCard({ nodeId, labels = [], onSaved }) { } }; + return ( @@ -79,7 +88,18 @@ export default function NodeLabelCard({ nodeId, labels = [], onSaved }) { ))} - setNewTag(e.target.value)} onKeyDown={(e) => e.key === "Enter" && addTag()} /> + setNewTag(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); // 阻止默认提交行为 + addTag(); + } + }} + /> + diff --git a/src/web/src/components/NodeTable.jsx b/src/web/src/components/NodeTable.jsx index 8c816a8..a3320de 100644 --- a/src/web/src/components/NodeTable.jsx +++ b/src/web/src/components/NodeTable.jsx @@ -1,11 +1,10 @@ import { useState, useEffect } from "react"; -import { Card, Table, Button, Loader, Center, TextInput, Select, Group, Anchor, Text } from "@mantine/core"; +import { Card, Table, Button, Loader, Center, Group, Anchor, Text } from "@mantine/core"; import { Link } from "react-router-dom"; import NodeStatus from "./NodeStatus"; import PaginationControl from "./PaginationControl"; import { apiRequest } from "../config/request"; -import { EXTERNAL_API } from "../config/api"; -import { statusOptions } from "../config/status"; +import { MASTER_API } from "../config/api"; export function NodeTable({ withSearch = false, @@ -17,15 +16,10 @@ export function NodeTable({ viewMoreLink, }) { const [nodes, setNodes] = useState([]); - const [totalCount, setTotalCount] = useState(0); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(5); const [loading, setLoading] = useState(false); - // 搜索条件 - const [searchName, setSearchName] = useState(""); - const [searchStatus, setSearchStatus] = useState(""); - // 拉取节点数据(仅 NodePage 使用) const fetchNodes = async (params = {}) => { if (!withPagination && !withSearch) return; // Dashboard 只用 clusterData @@ -33,14 +27,11 @@ export function NodeTable({ 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, + limit: params.pageSize || pageSize, }).toString(); - const result = await apiRequest(`${EXTERNAL_API.MASTER_NODES}?${query}`); - setNodes(result.data); - setTotalCount(result.total || 0); + const result = await apiRequest(`${MASTER_API.LIST}?${query}`); + setNodes(result); } finally { setLoading(false); } @@ -51,8 +42,7 @@ export function NodeTable({ if (withPagination || withSearch) { fetchNodes(); } else if (clusterData) { - setNodes(clusterData.nodes || []); - setTotalCount(clusterData.total_nodes || 0); + setNodes(clusterData || []); } }, [clusterData]); @@ -96,21 +86,6 @@ export function NodeTable({ {/* 搜索区域 */} {withSearch && (
- setSearchName(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && fetchNodes({ page: 1, name: searchName, status: searchStatus })} - style={{ width: 200 }} - /> - onPageSizeChange(Number(val))} + value={pageSizeValue} + onChange={(val) => { + if (val) onPageSizeChange(Number(val)); + }} style={{ width: 100 }} /> - + + + + 第 {page} 页 + + +
); } diff --git a/src/web/src/config/api.js b/src/web/src/config/api.js index 391c7be..3aeb0ce 100644 --- a/src/web/src/config/api.js +++ b/src/web/src/config/api.js @@ -1,15 +1,30 @@ -export const EXTERNAL_API = { - 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", -} +// config/api.js -// proxy location定位到具体位置。 -// proxy需要单独的机器,nginx配置。提供对外的endpoint,通过算力平台映射。 +// Master 节点相关 API +export const MASTER_API = { + // 节点列表 + LIST: "http://master.argus.com/api/v1/master/nodes", + + // 节点详情(需要 nodeId) + DETAIL: (nodeId) => `http://master.argus.com/api/v1/master/nodes/${nodeId}`, + + // 节点配置(需要 nodeId) + CONFIG: (nodeId) => `http://master.argus.com/api/v1/master/nodes/${nodeId}/config`, + + // 节点统计信息 + STATISTICS: "http://master.argus.com/api/v1/master/nodes/statistics", +}; + +// 其他外部 API +export const EXTERNAL_API = { + ALERTS_INFOS: "http://localhost:9093/api/v2/alerts", +}; + +// 外部服务 Host export const EXTERNAL_HOST = { - ALERTS: "http://localhost:9093", - GRAFANA: "http://grafana.metric.argus.com", - PROMETHEUS: "http://prometheus.metric.argus.com", - ES: "http://es.log.argus.com", - KIBANA: "http://kibana.log.argus.com", -} + ALERTS: "http://localhost:9093", + GRAFANA: "http://grafana.metric.argus.com", + PROMETHEUS: "http://prometheus.metric.argus.com", + ES: "http://es.log.argus.com", + KIBANA: "http://kibana.log.argus.com", +}; diff --git a/src/web/src/config/request.js b/src/web/src/config/request.js index 34ddc83..c178ba6 100644 --- a/src/web/src/config/request.js +++ b/src/web/src/config/request.js @@ -35,76 +35,13 @@ export async function apiRequest(url, options = {}, successMsg) { return data; } catch (err) { console.log("API 请求错误:", err); - // notifications.show({ - // title: "操作失败", - // message: err.message || "接口调用失败", - // color: "red", - // }); - // throw err; // 继续抛出错误,方便上层处理 + notifications.show({ + title: "操作失败", + message: err.message || "接口调用失败", + color: "red", + }); + throw err; // 继续抛出错误,方便上层处理 } - // 返回 mock 数据 - if (url.includes("/api/v1/master/nodes")) { - if (url.includes("/statistics")) { - return { - "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": { - // !!! 预留字段,KV形式非固定key, web配置下发给agent用的 - "setting1": "value1", - "setting2": "value2", - "setting3": "value3", - "setting4": "value4" - }, - "meta_data": { - // 元数据: 容器生命周期内不变 - "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": [ - // 用户或运维人员绑定到节点上的标签,业务属性, tag - "gpu", "exp001" - ], - "health": { // agent收集到各端上模块的health描述文件 - "log": "", //字符串,转义,防止换行、引号 - "metric": "" - }, - "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: "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" }, - ] - }; - } - - return []; - -} + +} \ No newline at end of file diff --git a/src/web/src/pages/Dashboard.jsx b/src/web/src/pages/Dashboard.jsx index 3bb9cf5..01916cd 100644 --- a/src/web/src/pages/Dashboard.jsx +++ b/src/web/src/pages/Dashboard.jsx @@ -5,6 +5,7 @@ import { HealthCard } from "../components/HealthCard"; import { AlertStats } from "../components/AlertStats"; import { apiRequest } from "../config/request"; import { EXTERNAL_API } from "../config/api"; +import { MASTER_API } from "../config/api"; export default function Dashboard() { const [cluster, setCluster] = useState(null); @@ -28,15 +29,12 @@ export default function Dashboard() { setLoading(true); try { const [clusterRes, healthRes, alertsRes] = await Promise.all([ - apiRequest(EXTERNAL_API.MASTER_NODES), - apiRequest(EXTERNAL_API.MASTER_NODES_STATISTICS), + apiRequest(MASTER_API.LIST), + apiRequest(MASTER_API.STATISTICS), apiRequest(EXTERNAL_API.ALERTS_INFOS), ]); - setCluster({ - total_nodes: clusterRes?.total || 0, - nodes: clusterRes?.data || [], - }); + setCluster(clusterRes || []); setHealth({ total: healthRes?.total || 0, diff --git a/src/web/src/pages/NodePage.jsx b/src/web/src/pages/NodePage.jsx index 120966f..af8a257 100644 --- a/src/web/src/pages/NodePage.jsx +++ b/src/web/src/pages/NodePage.jsx @@ -1,12 +1,12 @@ import { useState } from "react"; import { Grid } from "@mantine/core"; import { apiRequest } from "../config/request"; -import { EXTERNAL_API } from "../config/api"; +import { MASTER_API } from "../config/api"; import { NodeTable } from "../components/NodeTable"; import NodeDetailDrawer from "../components/NodeDetailDrawer"; export default function NodePage() { - const [selectedNode, setSelectedNode] = useState(null); + const [selectedNodeId, setSelectedNodeId] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); const [detailLoading, setDetailLoading] = useState(false); @@ -15,8 +15,8 @@ export default function NodePage() { setDetailLoading(true); setDrawerOpen(true); try { - const result = await apiRequest(`${EXTERNAL_API.MASTER_NODES}/${id}`); - setSelectedNode(result); + const result = await apiRequest(MASTER_API.DETAIL(id)); + setSelectedNodeId(result.id); } finally { setDetailLoading(false); } @@ -38,7 +38,7 @@ export default function NodePage() { setDrawerOpen(false)} - nodeId={selectedNode} + nodeId={selectedNodeId} loading={detailLoading} />