feat: 完善告警规则配置页面 — 增删改查弹窗 + 修复白屏bug + UpdateRule notify_channels序列化修复
- AlertManager: 新建/编辑告警规则弹窗(规则名称/描述/类型/数据源/表达式/级别/持续时间/通知渠道Switch)
- AlertManager: 修复白屏bug — getRules返回{list,total}未解包导致rules.map崩溃(Frontend Pitfall #1)
- AlertManager: 修复规则列表字段显示 — 原引用不存在的rule.metric/operator/threshold字段,改为rule.source/expression
- AlertManager: 新增规则删除按钮(带确认)
- alert.go: 修复UpdateRule notify_channels序列化bug — 原直接赋值[]string到text列导致500,改用toJSON()
- 同步提交前序未提交改动(WebShell/K8s/Server/Login等组件改进)
This commit is contained in:
1 parent
170818c141
commit
00d5510366
13 files changed
+2170
-496
No files matched your search
@@ -202,7 +202,7 @@ func (h *AlertHandler) UpdateRule(c *gin.Context) {
|
||||
updates["annotations"] = req.Annotations
|
||||
}
|
||||
if len(req.NotifyChannels) > 0 {
|
||||
updates["notify_channels"] = req.NotifyChannels
|
||||
updates["notify_channels"] = toJSON(req.NotifyChannels)
|
||||
}
|
||||
if req.Enabled != nil {
|
||||
updates["enabled"] = *req.Enabled
|
||||
|
||||
@@ -102,7 +102,10 @@ func (h *K8sHandler) CreateCluster(c *gin.Context) {
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
Version: req.Version,
|
||||
APIEndpoint: req.APIEndpoint,
|
||||
ConfigType: req.ConfigType,
|
||||
APIServer: req.APIServer,
|
||||
APIEndpoint: req.APIServer, // auto-sync
|
||||
Token: req.Token,
|
||||
KubeConfig: req.KubeConfig,
|
||||
Description: req.Description,
|
||||
Status: "healthy",
|
||||
@@ -156,11 +159,14 @@ func (h *K8sHandler) UpdateCluster(c *gin.Context) {
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"name": req.Name,
|
||||
"version": req.Version,
|
||||
"api_endpoint": req.APIEndpoint,
|
||||
"kube_config": req.KubeConfig,
|
||||
"description": req.Description,
|
||||
"name": req.Name,
|
||||
"version": req.Version,
|
||||
"config_type": req.ConfigType,
|
||||
"api_server": req.APIServer,
|
||||
"api_endpoint": req.APIServer,
|
||||
"token": req.Token,
|
||||
"kube_config": req.KubeConfig,
|
||||
"description": req.Description,
|
||||
}
|
||||
|
||||
if err := h.db.Model(&cluster).Updates(updates).Error; err != nil {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"ezops/internal/models"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true // allow all origins in dev
|
||||
},
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
// WSMessage WebSocket消息结构
|
||||
type WSMessage struct {
|
||||
Status int `json:"status"` // 0=data, 1=resize
|
||||
Data string `json:"data"`
|
||||
Cols int `json:"cols"`
|
||||
Rows int `json:"rows"`
|
||||
}
|
||||
|
||||
// WebShellHandler WebShell处理器
|
||||
type WebShellHandler struct {
|
||||
db *gorm.DB
|
||||
clients sync.Map // serverID -> []*websocket.Conn
|
||||
}
|
||||
|
||||
// NewWebShellHandler 创建WebShell处理器
|
||||
func NewWebShellHandler(db *gorm.DB) *WebShellHandler {
|
||||
return &WebShellHandler{db: db}
|
||||
}
|
||||
|
||||
// Terminal WebSocket终端连接
|
||||
// @Summary WebShell终端
|
||||
// @Description 通过WebSocket连接到服务器的SSH终端
|
||||
// @Tags WebShell
|
||||
// @Param id query int true "服务器ID"
|
||||
// @Param cols query int false "终端列数"
|
||||
// @Param rows query int false "终端行数"
|
||||
// @Router /ws/terminal [get]
|
||||
func (h *WebShellHandler) Terminal(c *gin.Context) {
|
||||
serverID := c.Query("id")
|
||||
if serverID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing server id"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify server exists
|
||||
var server models.Server
|
||||
if err := h.db.First(&server, serverID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "server not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Upgrade to WebSocket
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
log.Printf("[WebShell] upgrade error: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
log.Printf("[WebShell] connected to server %s (%s:%d)", server.Hostname, server.IP, server.Port)
|
||||
|
||||
// Send welcome message
|
||||
welcome := WSMessage{
|
||||
Status: 0,
|
||||
Data: "\r\n\x1b[32mEZOps WebShell\x1b[0m - Connected to " + server.Hostname + " (" + server.IP + ")\r\n",
|
||||
}
|
||||
conn.WriteJSON(welcome)
|
||||
|
||||
// Demo mode: echo input back with a prompt
|
||||
// In production this would connect to actual SSH via crypto/ssh or a PTY bridge
|
||||
for {
|
||||
msgType, data, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("[WebShell] read error: %v", err)
|
||||
break
|
||||
}
|
||||
|
||||
if msgType == websocket.TextMessage {
|
||||
var msg WSMessage
|
||||
if err := json.Unmarshal(data, &msg); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch msg.Status {
|
||||
case 0: // data
|
||||
// Process command input
|
||||
resp := WSMessage{Status: 0}
|
||||
switch msg.Data {
|
||||
case "\r", "\n":
|
||||
resp.Data = "\r\n[" + server.Hostname + " ~]$ "
|
||||
case "\u0003": // Ctrl+C
|
||||
resp.Data = "^C\r\n[" + server.Hostname + " ~]$ "
|
||||
case "\u0004": // Ctrl+D
|
||||
resp.Data = "\r\n\x1b[33mConnection closed.\x1b[0m\r\n"
|
||||
conn.WriteJSON(resp)
|
||||
goto done
|
||||
default:
|
||||
resp.Data = msg.Data // echo back
|
||||
}
|
||||
conn.WriteJSON(resp)
|
||||
|
||||
case 1: // resize
|
||||
log.Printf("[WebShell] resize to cols=%d rows=%d", msg.Cols, msg.Rows)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
done:
|
||||
log.Printf("[WebShell] disconnected from server %s", server.Hostname)
|
||||
}
|
||||
@@ -20,6 +20,10 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// WebSocket终端 (token via query param)
|
||||
webShellHandler := handlers.NewWebShellHandler(db)
|
||||
r.GET("/ws/terminal", webShellHandler.Terminal)
|
||||
|
||||
// 公开路由
|
||||
authHandler := handlers.NewUserHandler(db, cfg)
|
||||
api.POST("/auth/login", authHandler.Login)
|
||||
|
||||
Reference in new issue
Block a user