后端: - 新增 model/custom_dashboard.go (monitor_custom_dashboards 表) - 新增 controller/custom_dashboard.go (CRUD + 设默认, 6 路由) - routes.go 注册 /custom-dashboards 路由组 - database.go AutoMigrate 注册 CustomDashboard 前端: - grafana.ts: dashboardApi 从 mock 返回空改为真实 HTTP 调用 - metrics.ts: prometheusProxyApi 新增 query/queryRange 方法 - GrafanaPanelContent.tsx: 从随机 mock 数据改为执行 PromQL 查询 + Recharts LineChart 渲染 graph 类型 + SVG gauge 渲染 gauge 类型 + 数值卡片渲染 stat 类型 + 表格渲染 table 类型 + 30s 自动刷新 + loading/error 状态 + 自动发现默认 Prometheus 数据源 - GrafanaDashboard.tsx: 传递 datasourceUrl 到 GrafanaPanelContent 验证: go build✅ npm build✅ 17/17 集成测试通过 E2E CRUD 全通过
259 lines
7.4 KiB
Go
259 lines
7.4 KiB
Go
package controller
|
||
|
||
import (
|
||
"encoding/json"
|
||
"net/http"
|
||
"strconv"
|
||
|
||
ezops "ezops/model"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// CustomDashboardHandler 自定义监控仪表盘 handler // Logan.Li
|
||
type CustomDashboardHandler struct {
|
||
db *gorm.DB
|
||
}
|
||
|
||
// NewCustomDashboardHandler 创建 handler
|
||
func NewCustomDashboardHandler(db *gorm.DB) *CustomDashboardHandler {
|
||
return &CustomDashboardHandler{db: db}
|
||
}
|
||
|
||
// ============================================
|
||
// DTO
|
||
// ============================================
|
||
|
||
type CreateDashboardReq struct {
|
||
Name string `json:"name" binding:"required"`
|
||
Description string `json:"description"`
|
||
Datasource string `json:"datasource"`
|
||
DatasourceURL string `json:"datasource_url"`
|
||
Panels string `json:"panels"` // JSON string
|
||
IsDefault bool `json:"is_default"`
|
||
}
|
||
|
||
type UpdateDashboardReq struct {
|
||
Name *string `json:"name"`
|
||
Description *string `json:"description"`
|
||
Datasource *string `json:"datasource"`
|
||
DatasourceURL *string `json:"datasource_url"`
|
||
Panels *string `json:"panels"`
|
||
IsDefault *bool `json:"is_default"`
|
||
IsFavorite *bool `json:"is_favorite"`
|
||
}
|
||
|
||
// ============================================
|
||
// CRUD
|
||
// ============================================
|
||
|
||
// List 仪表盘列表
|
||
// GET /api/v1/custom-dashboards
|
||
func (h *CustomDashboardHandler) List(c *gin.Context) {
|
||
var dashboards []ezops.CustomDashboard
|
||
if err := h.db.Order("is_default DESC, updated_at DESC").Find(&dashboards).Error; err != nil {
|
||
c.JSON(http.StatusInternalServerError, ezops.Response{Code: 500, Message: "查询失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
// 转换为前端期望的格式(id 转为 string,panels JSON 解析为数组)
|
||
result := make([]gin.H, 0, len(dashboards))
|
||
for _, d := range dashboards {
|
||
item := gin.H{
|
||
"id": strconv.Itoa(int(d.ID)),
|
||
"name": d.Name,
|
||
"description": d.Description,
|
||
"datasource": d.Datasource,
|
||
"datasourceUrl": d.DatasourceURL,
|
||
"datasource_url": d.DatasourceURL,
|
||
"createdAt": d.CreatedAt,
|
||
"updatedAt": d.UpdatedAt,
|
||
"createdBy": d.CreatedBy,
|
||
"isDefault": d.IsDefault,
|
||
"is_default": d.IsDefault,
|
||
}
|
||
// 解析 panels JSON
|
||
var panels []interface{}
|
||
if d.Panels != "" {
|
||
if err := json.Unmarshal([]byte(d.Panels), &panels); err == nil {
|
||
item["panels"] = panels
|
||
} else {
|
||
item["panels"] = []interface{}{}
|
||
}
|
||
} else {
|
||
item["panels"] = []interface{}{}
|
||
}
|
||
result = append(result, item)
|
||
}
|
||
|
||
c.JSON(http.StatusOK, ezops.Response{Code: 200, Data: gin.H{
|
||
"list": result,
|
||
"total": len(result),
|
||
}})
|
||
}
|
||
|
||
// Get 获取单个仪表盘
|
||
// GET /api/v1/custom-dashboards/:id
|
||
func (h *CustomDashboardHandler) Get(c *gin.Context) {
|
||
id := c.Param("id")
|
||
var d ezops.CustomDashboard
|
||
if err := h.db.First(&d, id).Error; err != nil {
|
||
c.JSON(http.StatusNotFound, ezops.Response{Code: 404, Message: "仪表盘不存在"})
|
||
return
|
||
}
|
||
|
||
var panels []interface{}
|
||
if d.Panels != "" {
|
||
json.Unmarshal([]byte(d.Panels), &panels)
|
||
}
|
||
if panels == nil {
|
||
panels = []interface{}{}
|
||
}
|
||
|
||
c.JSON(http.StatusOK, ezops.Response{Code: 200, Data: gin.H{
|
||
"id": strconv.Itoa(int(d.ID)),
|
||
"name": d.Name,
|
||
"description": d.Description,
|
||
"datasource": d.Datasource,
|
||
"datasourceUrl": d.DatasourceURL,
|
||
"datasource_url": d.DatasourceURL,
|
||
"panels": panels,
|
||
"createdAt": d.CreatedAt,
|
||
"updatedAt": d.UpdatedAt,
|
||
"createdBy": d.CreatedBy,
|
||
"isDefault": d.IsDefault,
|
||
"is_default": d.IsDefault,
|
||
}})
|
||
}
|
||
|
||
// Create 创建仪表盘
|
||
// POST /api/v1/custom-dashboards
|
||
func (h *CustomDashboardHandler) Create(c *gin.Context) {
|
||
var req CreateDashboardReq
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, ezops.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
if req.Datasource == "" {
|
||
req.Datasource = "prometheus"
|
||
}
|
||
if req.Panels == "" {
|
||
req.Panels = "[]"
|
||
}
|
||
|
||
// 如果设为默认,先清除其他默认
|
||
if req.IsDefault {
|
||
h.db.Model(&ezops.CustomDashboard{}).Where("is_default = ?", true).Update("is_default", false)
|
||
}
|
||
|
||
// 从 JWT 获取用户名
|
||
username := "admin"
|
||
if u, exists := c.Get("username"); exists {
|
||
if s, ok := u.(string); ok {
|
||
username = s
|
||
}
|
||
}
|
||
|
||
d := ezops.CustomDashboard{
|
||
Name: req.Name,
|
||
Description: req.Description,
|
||
Datasource: req.Datasource,
|
||
DatasourceURL: req.DatasourceURL,
|
||
Panels: req.Panels,
|
||
CreatedBy: username,
|
||
IsDefault: req.IsDefault,
|
||
}
|
||
|
||
if err := h.db.Create(&d).Error; err != nil {
|
||
c.JSON(http.StatusInternalServerError, ezops.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, ezops.Response{Code: 200, Message: "创建成功", Data: gin.H{
|
||
"success": true,
|
||
"id": strconv.Itoa(int(d.ID)),
|
||
}})
|
||
}
|
||
|
||
// Update 更新仪表盘
|
||
// PUT /api/v1/custom-dashboards/:id
|
||
func (h *CustomDashboardHandler) Update(c *gin.Context) {
|
||
id := c.Param("id")
|
||
var d ezops.CustomDashboard
|
||
if err := h.db.First(&d, id).Error; err != nil {
|
||
c.JSON(http.StatusNotFound, ezops.Response{Code: 404, Message: "仪表盘不存在"})
|
||
return
|
||
}
|
||
|
||
var req UpdateDashboardReq
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, ezops.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
updates := map[string]interface{}{}
|
||
if req.Name != nil {
|
||
updates["name"] = *req.Name
|
||
}
|
||
if req.Description != nil {
|
||
updates["description"] = *req.Description
|
||
}
|
||
if req.Datasource != nil {
|
||
updates["datasource"] = *req.Datasource
|
||
}
|
||
if req.DatasourceURL != nil {
|
||
updates["datasource_url"] = *req.DatasourceURL
|
||
}
|
||
if req.Panels != nil {
|
||
updates["panels"] = *req.Panels
|
||
}
|
||
if req.IsFavorite != nil {
|
||
updates["is_favorite"] = *req.IsFavorite
|
||
}
|
||
if req.IsDefault != nil && *req.IsDefault {
|
||
// 设为默认前先清除其他默认
|
||
h.db.Model(&ezops.CustomDashboard{}).Where("is_default = ? AND id != ?", true, d.ID).Update("is_default", false)
|
||
updates["is_default"] = true
|
||
}
|
||
|
||
if len(updates) > 0 {
|
||
if err := h.db.Model(&d).Updates(updates).Error; err != nil {
|
||
c.JSON(http.StatusInternalServerError, ezops.Response{Code: 500, Message: "更新失败: " + err.Error()})
|
||
return
|
||
}
|
||
}
|
||
|
||
c.JSON(http.StatusOK, ezops.Response{Code: 200, Message: "更新成功", Data: gin.H{"success": true}})
|
||
}
|
||
|
||
// Delete 删除仪表盘
|
||
// DELETE /api/v1/custom-dashboards/:id
|
||
func (h *CustomDashboardHandler) Delete(c *gin.Context) {
|
||
id := c.Param("id")
|
||
if err := h.db.Delete(&ezops.CustomDashboard{}, id).Error; err != nil {
|
||
c.JSON(http.StatusInternalServerError, ezops.Response{Code: 500, Message: "删除失败: " + err.Error()})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, ezops.Response{Code: 200, Message: "删除成功", Data: gin.H{"success": true}})
|
||
}
|
||
|
||
// SetDefault 设为默认仪表盘
|
||
// POST /api/v1/custom-dashboards/:id/default
|
||
func (h *CustomDashboardHandler) SetDefault(c *gin.Context) {
|
||
id := c.Param("id")
|
||
var d ezops.CustomDashboard
|
||
if err := h.db.First(&d, id).Error; err != nil {
|
||
c.JSON(http.StatusNotFound, ezops.Response{Code: 404, Message: "仪表盘不存在"})
|
||
return
|
||
}
|
||
|
||
// 清除其他默认
|
||
h.db.Model(&ezops.CustomDashboard{}).Where("is_default = ? AND id != ?", true, d.ID).Update("is_default", false)
|
||
// 设置当前为默认
|
||
h.db.Model(&d).Update("is_default", true)
|
||
|
||
c.JSON(http.StatusOK, ezops.Response{Code: 200, Message: "设置成功", Data: gin.H{"success": true}})
|
||
}
|