From 14a48f81a46e75f755acf0d7d9612fe497d602cd Mon Sep 17 00:00:00 2001 From: dushixiang Date: Wed, 26 Aug 2026 20:20:42 +0800 Subject: [PATCH] feat: support disabling agents --- internal/app.go | 2 + internal/handler/agent_enabled_test.go | 81 +++++++++++++++++++ internal/handler/agent_handler.go | 26 ++++++ internal/handler/agent_query_handler.go | 15 ++++ internal/handler/agent_ws_handler.go | 43 ++++++---- internal/models/agent.go | 37 ++++----- internal/repo/agent_repo.go | 18 ++++- internal/service/agent_service.go | 30 +++++++ .../service/alert_rule_disabled_agent_test.go | 55 +++++++++++++ internal/service/alert_rule_service.go | 11 ++- web/src/api/agent.ts | 5 ++ web/src/pages/Agents/AgentList.tsx | 65 ++++++++++++--- web/src/types/index.ts | 1 + 13 files changed, 344 insertions(+), 45 deletions(-) create mode 100644 internal/handler/agent_enabled_test.go create mode 100644 internal/service/alert_rule_disabled_agent_test.go diff --git a/internal/app.go b/internal/app.go index 33f1eb3..fbeea74 100644 --- a/internal/app.go +++ b/internal/app.go @@ -208,6 +208,8 @@ func setupApi(app *orz.App, components *AppComponents) error { adminApi.GET("/agents/:id", components.AgentHandler.GetForAdmin) adminApi.GET("/agents/:id/metrics/latest", components.AgentHandler.GetAdminLatestMetrics) adminApi.PUT("/agents/:id", components.AgentHandler.UpdateInfo) + adminApi.POST("/agents/:id/enable", components.AgentHandler.Enable) + adminApi.POST("/agents/:id/disable", components.AgentHandler.Disable) adminApi.POST("/agents/batch/tags", components.AgentHandler.BatchUpdateTags) adminApi.POST("/agents/batch/visibility", components.AgentHandler.BatchUpdateVisibility) adminApi.DELETE("/agents/:id", components.AgentHandler.Delete) diff --git a/internal/handler/agent_enabled_test.go b/internal/handler/agent_enabled_test.go new file mode 100644 index 0000000..795c208 --- /dev/null +++ b/internal/handler/agent_enabled_test.go @@ -0,0 +1,81 @@ +package handler + +import ( + "context" + "encoding/json" + "testing" + + "github.com/glebarez/sqlite" + "github.com/pika-monitor/pika/internal/models" + "github.com/pika-monitor/pika/internal/protocol" + "github.com/pika-monitor/pika/internal/repo" + "github.com/pika-monitor/pika/internal/service" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func TestHandleWebSocketMessageIgnoresDisabledAgent(t *testing.T) { + database, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatalf("open database: %v", err) + } + if err := database.AutoMigrate(&models.Agent{}); err != nil { + t.Fatalf("migrate agent: %v", err) + } + + agent := models.Agent{ID: "agent-disabled", Enabled: true} + if err := database.Create(&agent).Error; err != nil { + t.Fatalf("create agent: %v", err) + } + if err := database.Model(&models.Agent{}).Where("id = ?", agent.ID).Update("enabled", false).Error; err != nil { + t.Fatalf("disable agent: %v", err) + } + + h := &AgentHandler{ + logger: zap.NewNop(), + agentService: &service.AgentService{ + AgentRepo: repo.NewAgentRepo(database), + }, + } + invalidMetrics := json.RawMessage(`{`) + if err := h.handleWebSocketMessage(context.Background(), agent.ID, string(protocol.MessageTypeMetrics), invalidMetrics); err != nil { + t.Fatalf("disabled agent data should be ignored, got error: %v", err) + } + + if err := database.Model(&models.Agent{}).Where("id = ?", agent.ID).Update("enabled", true).Error; err != nil { + t.Fatalf("enable agent: %v", err) + } + if err := h.handleWebSocketMessage(context.Background(), agent.ID, string(protocol.MessageTypeMetrics), invalidMetrics); err == nil { + t.Fatal("enabled agent data should reach the metrics parser") + } +} + +func TestDisabledAgentStatusCannotBeRefreshed(t *testing.T) { + database, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatalf("open database: %v", err) + } + if err := database.AutoMigrate(&models.Agent{}); err != nil { + t.Fatalf("migrate agent: %v", err) + } + + agent := models.Agent{ID: "agent-status", Enabled: true, Status: 1} + if err := database.Create(&agent).Error; err != nil { + t.Fatalf("create agent: %v", err) + } + agentService := &service.AgentService{AgentRepo: repo.NewAgentRepo(database)} + if err := agentService.UpdateAgentEnabled(context.Background(), agent.ID, false); err != nil { + t.Fatalf("disable agent: %v", err) + } + if err := agentService.UpdateAgentStatus(context.Background(), agent.ID, 1); err != nil { + t.Fatalf("refresh disabled agent status: %v", err) + } + + var saved models.Agent + if err := database.First(&saved, "id = ?", agent.ID).Error; err != nil { + t.Fatalf("load agent: %v", err) + } + if saved.Enabled || saved.Status != 0 { + t.Fatalf("disabled agent status changed unexpectedly: enabled=%v status=%d", saved.Enabled, saved.Status) + } +} diff --git a/internal/handler/agent_handler.go b/internal/handler/agent_handler.go index 0dd2c73..1aa3054 100644 --- a/internal/handler/agent_handler.go +++ b/internal/handler/agent_handler.go @@ -1,13 +1,39 @@ package handler import ( + "sync" + + "github.com/go-orz/orz" "github.com/gorilla/websocket" + "github.com/labstack/echo/v5" "github.com/pika-monitor/pika/internal/service" ws "github.com/pika-monitor/pika/internal/websocket" "go.uber.org/zap" ) +// Enable 启用探针数据处理。 +func (h *AgentHandler) Enable(c *echo.Context) error { + return h.updateEnabled(c, true) +} + +// Disable 禁用探针数据处理和告警。 +func (h *AgentHandler) Disable(c *echo.Context) error { + return h.updateEnabled(c, false) +} + +func (h *AgentHandler) updateEnabled(c *echo.Context, enabled bool) error { + h.enabledMu.Lock() + defer h.enabledMu.Unlock() + + agentID := c.Param("id") + if err := h.agentService.UpdateAgentEnabled(c.Request().Context(), agentID, enabled); err != nil { + return err + } + return orz.Ok(c, orz.Map{}) +} + type AgentHandler struct { + enabledMu sync.RWMutex logger *zap.Logger agentService *service.AgentService trafficService *service.TrafficService diff --git a/internal/handler/agent_query_handler.go b/internal/handler/agent_query_handler.go index 2069525..137f06b 100644 --- a/internal/handler/agent_query_handler.go +++ b/internal/handler/agent_query_handler.go @@ -1,6 +1,8 @@ package handler import ( + "encoding/json" + "github.com/go-orz/orz" "github.com/labstack/echo/v5" "github.com/pika-monitor/pika/internal/models" @@ -32,6 +34,19 @@ func (h *AgentHandler) Get(c *echo.Context) error { agent.Hostname = "" } + if !isAuthenticated { + payload, err := json.Marshal(agent) + if err != nil { + return err + } + var publicAgent map[string]any + if err := json.Unmarshal(payload, &publicAgent); err != nil { + return err + } + delete(publicAgent, "enabled") + return orz.Ok(c, publicAgent) + } + return orz.Ok(c, agent) } diff --git a/internal/handler/agent_ws_handler.go b/internal/handler/agent_ws_handler.go index f9b7b78..7562d63 100644 --- a/internal/handler/agent_ws_handler.go +++ b/internal/handler/agent_ws_handler.go @@ -28,7 +28,9 @@ func (h *AgentHandler) HandleWebSocket(c *echo.Context) error { } // 注册探针 - 使用独立的context,不依赖HTTP请求的context + h.enabledMu.RLock() agent, err := h.agentService.RegisterAgent(context.Background(), c.RealIP(), ®isterReq.AgentInfo, registerReq.ApiKey) + h.enabledMu.RUnlock() if err != nil { // 发送注册失败响应 h.sendRegisterError(conn, err.Error()) @@ -47,20 +49,22 @@ func (h *AgentHandler) HandleWebSocket(c *echo.Context) error { return err } - // 下发防篡改配置 - if err := h.sendTamperConfig(conn, agent.ID); err != nil { - h.logger.Error("failed to send tamper config", zap.Error(err)) - // 配置下发失败不中断连接,只记录日志 - } - // 下发SSH登录监控配置 - if err := h.sendSSHLoginConfig(conn, agent.ID); err != nil { - h.logger.Error("failed to send ssh login config", zap.Error(err)) - // 配置下发失败不中断连接,只记录日志 - } - // 下发公网 IP 采集配置 - if err := h.sendPublicIPConfig(conn, agent.ID); err != nil { - h.logger.Error("failed to send public ip config", zap.Error(err)) - // 配置下发失败不中断连接,只记录日志 + if agent.Enabled { + // 下发防篡改配置 + if err := h.sendTamperConfig(conn, agent.ID); err != nil { + h.logger.Error("failed to send tamper config", zap.Error(err)) + // 配置下发失败不中断连接,只记录日志 + } + // 下发SSH登录监控配置 + if err := h.sendSSHLoginConfig(conn, agent.ID); err != nil { + h.logger.Error("failed to send ssh login config", zap.Error(err)) + // 配置下发失败不中断连接,只记录日志 + } + // 下发公网 IP 采集配置 + if err := h.sendPublicIPConfig(conn, agent.ID); err != nil { + h.logger.Error("failed to send public ip config", zap.Error(err)) + // 配置下发失败不中断连接,只记录日志 + } } // 创建客户端并注册到管理器 @@ -76,6 +80,17 @@ func (h *AgentHandler) HandleWebSocket(c *echo.Context) error { // handleWebSocketMessage 处理WebSocket消息 func (h *AgentHandler) handleWebSocketMessage(ctx context.Context, agentID string, messageType string, data json.RawMessage) error { + h.enabledMu.RLock() + defer h.enabledMu.RUnlock() + + enabled, err := h.agentService.IsAgentEnabled(ctx, agentID) + if err != nil { + return err + } + if !enabled { + return nil + } + switch protocol.MessageType(messageType) { case protocol.MessageTypeMetrics: return h.handleMetricsMessage(ctx, agentID, data) diff --git a/internal/models/agent.go b/internal/models/agent.go index 468125c..43df931 100644 --- a/internal/models/agent.go +++ b/internal/models/agent.go @@ -9,24 +9,25 @@ import ( // Agent 探针信息 type Agent struct { - ID string `gorm:"primaryKey" json:"id"` // 探针ID (UUID) - Name string `gorm:"index" json:"name"` // 探针名称 - Hostname string `gorm:"index" json:"hostname,omitempty"` // 主机名 - IP string `gorm:"index" json:"ip,omitempty"` // 连接 IP 地址 - IPv4 string `gorm:"index" json:"ipv4,omitempty"` // 公网 IPv4 地址 - IPv6 string `gorm:"index" json:"ipv6,omitempty"` // 公网 IPv6 地址 - OS string `json:"os"` // 操作系统 - Arch string `json:"arch"` // 架构 - Version string `json:"version"` // 探针版本 - Tags datatypes.JSONSlice[string] `json:"tags"` // 标签 - ExpireTime int64 `json:"expireTime"` // 到期时间(时间戳毫秒) - Status int `json:"status"` // 状态: 0-离线, 1-在线 - Visibility string `gorm:"default:public" json:"visibility"` // 可见性: public-匿名可见, private-登录可见 - Weight int `gorm:"default:0;index" json:"weight"` // 展示顺序(由拖动排序维护,数字越大越靠前) - Remark string `json:"remark"` // 备注信息 - LastSeenAt int64 `gorm:"index" json:"lastSeenAt"` // 最后上线时间(时间戳毫秒) - CreatedAt int64 `json:"createdAt"` // 创建时间(时间戳毫秒) - UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"` // 更新时间(时间戳毫秒) + ID string `gorm:"primaryKey" json:"id"` // 探针ID (UUID) + Name string `gorm:"index" json:"name"` // 探针名称 + Hostname string `gorm:"index" json:"hostname,omitempty"` // 主机名 + IP string `gorm:"index" json:"ip,omitempty"` // 连接 IP 地址 + IPv4 string `gorm:"index" json:"ipv4,omitempty"` // 公网 IPv4 地址 + IPv6 string `gorm:"index" json:"ipv6,omitempty"` // 公网 IPv6 地址 + OS string `json:"os"` // 操作系统 + Arch string `json:"arch"` // 架构 + Version string `json:"version"` // 探针版本 + Tags datatypes.JSONSlice[string] `json:"tags"` // 标签 + ExpireTime int64 `json:"expireTime"` // 到期时间(时间戳毫秒) + Enabled bool `gorm:"default:true;not null;index" json:"enabled"` // 是否接收并处理探针数据 + Status int `json:"status"` // 状态: 0-离线, 1-在线 + Visibility string `gorm:"default:public" json:"visibility"` // 可见性: public-匿名可见, private-登录可见 + Weight int `gorm:"default:0;index" json:"weight"` // 展示顺序(由拖动排序维护,数字越大越靠前) + Remark string `json:"remark"` // 备注信息 + LastSeenAt int64 `gorm:"index" json:"lastSeenAt"` // 最后上线时间(时间戳毫秒) + CreatedAt int64 `json:"createdAt"` // 创建时间(时间戳毫秒) + UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"` // 更新时间(时间戳毫秒) // 流量统计相关字段 TrafficStats datatypes.JSONType[TrafficStatsData] `json:"trafficStats,omitempty"` // 流量统计 diff --git a/internal/repo/agent_repo.go b/internal/repo/agent_repo.go index 1a0f865..fd6d6e0 100644 --- a/internal/repo/agent_repo.go +++ b/internal/repo/agent_repo.go @@ -32,15 +32,27 @@ func (r *AgentRepo) UpdateStatus(ctx context.Context, agentID string, status int return r.db.WithContext(ctx). Model(&models.Agent{}). - Where("id = ?", agentID). + Where("id = ? AND enabled = ?", agentID, true). Updates(m).Error } +// IsEnabled 查询探针是否允许接收并处理数据。 +func (r *AgentRepo) IsEnabled(ctx context.Context, agentID string) (bool, error) { + var agent models.Agent + if err := r.db.WithContext(ctx). + Select("id", "enabled"). + Where("id = ?", agentID). + First(&agent).Error; err != nil { + return false, err + } + return agent.Enabled, nil +} + // FindOnlineAgents 查找所有在线探针 func (r *AgentRepo) FindOnlineAgents(ctx context.Context) ([]models.Agent, error) { var agents []models.Agent err := r.db.WithContext(ctx). - Where("status = ?", 1). + Where("status = ? AND enabled = ?", 1, true). Find(&agents).Error return agents, err } @@ -136,7 +148,7 @@ func (r *AgentRepo) GetStatistics(ctx context.Context) (total int64, online int6 // 获取在线数量 err = r.db.WithContext(ctx). Model(&models.Agent{}). - Where("status = ?", 1). + Where("status = ? AND enabled = ?", 1, true). Count(&online).Error return total, online, err diff --git a/internal/service/agent_service.go b/internal/service/agent_service.go index fc49d97..225b431 100644 --- a/internal/service/agent_service.go +++ b/internal/service/agent_service.go @@ -66,6 +66,14 @@ func (s *AgentService) RegisterAgent(ctx context.Context, ip string, info *proto // 这样即使主机名变化,也能正确识别 existingAgent, err := s.AgentRepo.FindById(ctx, info.ID) if err == nil { + // 已禁用的探针仅维持连接,不用注册报文刷新任何主机数据或在线状态。 + if !existingAgent.Enabled { + s.logger.Info("disabled agent reconnected; registration data ignored", + zap.String("agentID", existingAgent.ID), + zap.String("hostname", existingAgent.Hostname)) + return &existingAgent, nil + } + // 更新现有探针信息(允许主机名、名称等变化) now := time.Now().UnixMilli() existingAgent.Hostname = info.Hostname @@ -117,6 +125,7 @@ func (s *AgentService) RegisterAgent(ctx context.Context, ip string, info *proto OS: info.OS, Arch: info.Arch, Version: info.Version, + Enabled: true, Status: 1, LastSeenAt: now, CreatedAt: now, @@ -141,6 +150,27 @@ func (s *AgentService) UpdateAgentStatus(ctx context.Context, agentID string, st return s.AgentRepo.UpdateStatus(ctx, agentID, status, time.Now().UnixMilli()) } +// IsAgentEnabled 查询探针是否允许接收并处理数据。 +func (s *AgentService) IsAgentEnabled(ctx context.Context, agentID string) (bool, error) { + return s.AgentRepo.IsEnabled(ctx, agentID) +} + +// UpdateAgentEnabled 更新探针启用状态。禁用时立即标记离线,后续状态更新由仓储层原子拦截。 +func (s *AgentService) UpdateAgentEnabled(ctx context.Context, agentID string, enabled bool) error { + if _, err := s.AgentRepo.FindById(ctx, agentID); err != nil { + return err + } + + updates := map[string]interface{}{ + "enabled": enabled, + "updated_at": time.Now().UnixMilli(), + } + if !enabled { + updates["status"] = 0 + } + return s.AgentRepo.UpdateColumnsById(ctx, agentID, updates) +} + // UpdatePublicIP 更新探针的公网 IP 信息 func (s *AgentService) UpdatePublicIP(ctx context.Context, agentID string, ipv4 string, ipv6 string) error { updates := map[string]interface{}{ diff --git a/internal/service/alert_rule_disabled_agent_test.go b/internal/service/alert_rule_disabled_agent_test.go new file mode 100644 index 0000000..a32acae --- /dev/null +++ b/internal/service/alert_rule_disabled_agent_test.go @@ -0,0 +1,55 @@ +package service + +import ( + "context" + "testing" + + "github.com/glebarez/sqlite" + "github.com/pika-monitor/pika/internal/models" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func TestResolveForAgentsExcludesDisabledAgents(t *testing.T) { + database, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatalf("open database: %v", err) + } + if err := database.AutoMigrate(&models.Agent{}, &models.AlertRule{}); err != nil { + t.Fatalf("migrate models: %v", err) + } + + agents := []models.Agent{ + {ID: "enabled-agent", Enabled: true}, + {ID: "disabled-agent", Enabled: true}, + } + if err := database.Create(&agents).Error; err != nil { + t.Fatalf("create agents: %v", err) + } + if err := database.Model(&models.Agent{}).Where("id = ?", "disabled-agent").Update("enabled", false).Error; err != nil { + t.Fatalf("disable agent: %v", err) + } + + rule := models.AlertRule{ + ID: "all-agents-rule", + Name: "all agents", + Priority: 1, + Enabled: true, + TargetType: models.AlertRuleTargetAll, + } + if err := database.Create(&rule).Error; err != nil { + t.Fatalf("create alert rule: %v", err) + } + + alertRuleService := NewAlertRuleService(zap.NewNop(), database, nil) + configs, err := alertRuleService.ResolveForAgents(context.Background(), []string{"enabled-agent", "disabled-agent"}) + if err != nil { + t.Fatalf("resolve alert rules: %v", err) + } + if configs["enabled-agent"] == nil { + t.Fatal("enabled agent should match the alert rule") + } + if configs["disabled-agent"] != nil { + t.Fatal("disabled agent must not match any alert rule") + } +} diff --git a/internal/service/alert_rule_service.go b/internal/service/alert_rule_service.go index 9f8d620..ac1698b 100644 --- a/internal/service/alert_rule_service.go +++ b/internal/service/alert_rule_service.go @@ -229,17 +229,26 @@ func (s *AlertRuleService) ResolveForAgents(ctx context.Context, agentIDs []stri return result, nil } - // 查询主机信息用于标签匹配 + // 查询主机信息用于启用状态检查和标签匹配。禁用主机不命中任何告警规则, + // 从而统一阻止指标、离线、到期和监控等所有类型的告警。 agentTagMap := make(map[string][]string, len(agentIDs)) + enabledAgents := make(map[string]struct{}, len(agentIDs)) agents, err := s.agentRepo.FindByIdIn(ctx, agentIDs) if err != nil { return nil, err } for _, agent := range agents { + if !agent.Enabled { + continue + } + enabledAgents[agent.ID] = struct{}{} agentTagMap[agent.ID] = agent.Tags } for _, agentID := range agentIDs { + if _, enabled := enabledAgents[agentID]; !enabled { + continue + } for i := range rules { if !ruleMatchesAgent(&rules[i], agentID, agentTagMap[agentID]) { continue diff --git a/web/src/api/agent.ts b/web/src/api/agent.ts index 69011eb..392b4fd 100644 --- a/web/src/api/agent.ts +++ b/web/src/api/agent.ts @@ -483,6 +483,11 @@ export const deleteAgent = (agentId: string) => { return del(`/admin/agents/${agentId}`); }; +// 启用或禁用探针数据处理 +export const updateAgentEnabled = (agentId: string, enabled: boolean) => { + return post(`/admin/agents/${agentId}/${enabled ? 'enable' : 'disable'}`); +}; + // 获取所有探针的标签 export interface GetTagsResponse { tags: string[]; diff --git a/web/src/pages/Agents/AgentList.tsx b/web/src/pages/Agents/AgentList.tsx index f12e671..e9387a1 100644 --- a/web/src/pages/Agents/AgentList.tsx +++ b/web/src/pages/Agents/AgentList.tsx @@ -4,7 +4,7 @@ import {Link, useNavigate} from 'react-router-dom'; import type {MenuProps} from 'antd'; import {App, Button, Dropdown, Form, Input, Select, Space, Tag} from 'antd'; import type {ColumnsType} from 'antd/es/table'; -import {Edit, Eye, EyeOff, FileWarning, GripVertical, Lock, MoreVertical, Plus, RefreshCw, Shield, Tags, Trash2} from 'lucide-react'; +import {Edit, Eye, EyeOff, FileWarning, GripVertical, Lock, MoreVertical, Play, Plus, PowerOff, RefreshCw, Shield, Tags, Trash2} from 'lucide-react'; import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; import type {DragEndEvent} from '@dnd-kit/core'; import {DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors} from '@dnd-kit/core'; @@ -18,7 +18,7 @@ import { } from '@dnd-kit/sortable'; import {CSS} from '@dnd-kit/utilities'; import dayjs from 'dayjs'; -import {deleteAgent, getTags, listAgentsByAdmin, updateAgentOrder} from '@/api/agent.ts'; +import {deleteAgent, getTags, listAgentsByAdmin, updateAgentEnabled, updateAgentOrder} from '@/api/agent.ts'; import type {Agent} from '@/types'; import {getErrorMessage} from '@/lib/utils'; import {PageHeader} from '@/components/PageHeader'; @@ -146,6 +146,17 @@ const AgentList = () => { }, }); + const enabledMutation = useMutation({ + mutationFn: ({agentId, enabled}: {agentId: string; enabled: boolean}) => updateAgentEnabled(agentId, enabled), + onSuccess: (_response, variables) => { + messageApi.success(variables.enabled ? '主机已启用' : '主机已禁用'); + queryClient.invalidateQueries({queryKey: ['admin', 'agents']}); + }, + onError: (error: unknown, variables) => { + messageApi.error(getErrorMessage(error, variables.enabled ? '启用主机失败' : '禁用主机失败')); + }, + }); + const orderMutation = useMutation({ mutationFn: (agentIds: string[]) => updateAgentOrder(agentIds), onMutate: async (agentIds) => { @@ -219,6 +230,23 @@ const AgentList = () => { }); }; + const handleEnabledChange = (agent: Agent) => { + if (!agent.enabled) { + enabledMutation.mutate({agentId: agent.id, enabled: true}); + return; + } + + modal.confirm({ + title: '禁用主机', + content: `确定要禁用主机「${agent.name || agent.hostname}」吗?禁用期间接收到的数据将被忽略,也不会触发告警。`, + okText: '确认禁用', + cancelText: '取消', + okButtonProps: {danger: true}, + centered: true, + onOk: () => enabledMutation.mutateAsync({agentId: agent.id, enabled: false}), + }); + }; + const handleBatchTags = () => { if (selectedRowKeys.length === 0) { messageApi.warning('请先选择要操作的探针'); @@ -270,9 +298,11 @@ const AgentList = () => { } // 状态过滤 - if (status) { + if (status === 'disabled') { + result = result.filter((agent: Agent) => !agent.enabled); + } else if (status) { const statusValue = status === 'online' ? 1 : 0; - result = result.filter((agent: Agent) => agent.status === statusValue); + result = result.filter((agent: Agent) => agent.enabled && agent.status === statusValue); } // 标签过滤 @@ -356,11 +386,16 @@ const AgentList = () => { dataIndex: 'status', key: 'status', width: 80, - render: (_, record) => ( - - {record.status === 1 ? '在线' : '离线'} - - ), + render: (_, record) => { + if (!record.enabled) { + return 已禁用; + } + return ( + + {record.status === 1 ? '在线' : '离线'} + + ); + }, }, { title: '可见性', @@ -554,6 +589,17 @@ const AgentList = () => { { type: 'divider', }, + { + key: 'enabled', + label: record.enabled ? '禁用主机' : '启用主机', + icon: record.enabled ? : , + danger: record.enabled, + disabled: enabledMutation.isPending, + onClick: () => handleEnabledChange(record), + }, + { + type: 'divider', + }, { key: 'delete', label: '删除探针', @@ -621,6 +667,7 @@ const AgentList = () => { options={[ {label: '在线', value: 'online'}, {label: '离线', value: 'offline'}, + {label: '已禁用', value: 'disabled'}, ]} /> diff --git a/web/src/types/index.ts b/web/src/types/index.ts index 97c3347..45d60c2 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -26,6 +26,7 @@ export interface Agent { version: string; tags?: string[]; // 标签 expireTime?: number; // 到期时间(时间戳毫秒) + enabled: boolean; // 是否接收并处理探针数据 status: number; visibility?: string; // 可见性: public-匿名可见, private-登录可见 weight?: number; // 展示顺序(由拖动排序维护)