feat: Zabbix 数据源管理 — 完整 CRUD + 双 Tab 数据源页面
后端: - models/zabbix_datasource.go: Zabbix 数据源 model (15字段, 参照 Grafana) - handlers/zabbix_datasource.go: CRUD + 测试连接 (调用 Zabbix API Auth) - routes.go: /zabbix-datasources 路由组 - database.go: AutoMigrate 前端: - api.ts: zabbixDatasourceApi - ZabbixDatasourceManager.tsx: 表单 (URL/用户名/密码/Trends设置/DB直连) - DatasourcePage.tsx: Prometheus + Zabbix 双 Tab 包装 - App.tsx: /datasources 路由更新
This commit is contained in:
1 parent
d2201fc8c0
commit
c51d0b8b8b
8 files changed
+761
-2
No files matched your search
@@ -0,0 +1,205 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"ezops/internal/models"
|
||||||
|
"ezops/pkg/zabbix"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ZabbixDSHandler Zabbix 数据源管理 handler // Logan.Li
|
||||||
|
type ZabbixDSHandler struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewZabbixDSHandler 创建 Zabbix 数据源 handler
|
||||||
|
func NewZabbixDSHandler(db *gorm.DB) *ZabbixDSHandler {
|
||||||
|
return &ZabbixDSHandler{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// CRUD
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// List 数据源列表
|
||||||
|
// GET /api/v1/zabbix-datasources
|
||||||
|
func (h *ZabbixDSHandler) List(c *gin.Context) {
|
||||||
|
var sources []models.ZabbixDataSource
|
||||||
|
if err := h.db.Order("is_default DESC, id ASC").Find(&sources).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if sources == nil {
|
||||||
|
sources = []models.ZabbixDataSource{}
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: sources})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取单个数据源
|
||||||
|
// GET /api/v1/zabbix-datasources/:id
|
||||||
|
func (h *ZabbixDSHandler) Get(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
var ds models.ZabbixDataSource
|
||||||
|
if err := h.db.First(&ds, id).Error; err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "数据源不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: ds})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建数据源
|
||||||
|
// POST /api/v1/zabbix-datasources
|
||||||
|
func (h *ZabbixDSHandler) Create(c *gin.Context) {
|
||||||
|
var req models.CreateZabbixDSReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.IsDefault {
|
||||||
|
h.db.Model(&models.ZabbixDataSource{}).Where("is_default = ?", true).Update("is_default", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
ds := models.ZabbixDataSource{
|
||||||
|
Name: req.Name,
|
||||||
|
URL: req.URL,
|
||||||
|
Username: req.Username,
|
||||||
|
Password: req.Password,
|
||||||
|
TrendsAfter: req.TrendsAfter,
|
||||||
|
TrendsRange: req.TrendsRange,
|
||||||
|
TrendsCacheTTL: req.TrendsCacheTTL,
|
||||||
|
TrendsTimeout: req.TrendsTimeout,
|
||||||
|
DirectDB: req.DirectDB,
|
||||||
|
DisableAcknowledges: req.DisableAcknowledges,
|
||||||
|
DisableDataAlignment: req.DisableDataAlignment,
|
||||||
|
IsDefault: req.IsDefault,
|
||||||
|
Status: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ds.TrendsCacheTTL <= 0 {
|
||||||
|
ds.TrendsCacheTTL = 3600
|
||||||
|
}
|
||||||
|
if ds.TrendsTimeout <= 0 {
|
||||||
|
ds.TrendsTimeout = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.db.Create(&ds).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: ds, Message: "创建成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新数据源
|
||||||
|
// PUT /api/v1/zabbix-datasources/:id
|
||||||
|
func (h *ZabbixDSHandler) Update(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
var ds models.ZabbixDataSource
|
||||||
|
if err := h.db.First(&ds, id).Error; err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "数据源不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req models.UpdateZabbixDSReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := map[string]interface{}{}
|
||||||
|
if req.Name != nil { updates["name"] = *req.Name }
|
||||||
|
if req.URL != nil { updates["url"] = *req.URL }
|
||||||
|
if req.Username != nil { updates["username"] = *req.Username }
|
||||||
|
if req.Password != nil && *req.Password != "" { updates["password"] = *req.Password }
|
||||||
|
if req.TrendsAfter != nil { updates["trends_after"] = *req.TrendsAfter }
|
||||||
|
if req.TrendsRange != nil { updates["trends_range"] = *req.TrendsRange }
|
||||||
|
if req.TrendsCacheTTL != nil { updates["trends_cache_ttl"] = *req.TrendsCacheTTL }
|
||||||
|
if req.TrendsTimeout != nil { updates["trends_timeout"] = *req.TrendsTimeout }
|
||||||
|
if req.DirectDB != nil { updates["direct_db"] = *req.DirectDB }
|
||||||
|
if req.DisableAcknowledges != nil { updates["disable_acknowledges"] = *req.DisableAcknowledges }
|
||||||
|
if req.DisableDataAlignment != nil { updates["disable_data_alignment"] = *req.DisableDataAlignment }
|
||||||
|
if req.IsDefault != nil {
|
||||||
|
updates["is_default"] = *req.IsDefault
|
||||||
|
if *req.IsDefault {
|
||||||
|
h.db.Model(&models.ZabbixDataSource{}).Where("is_default = ? AND id != ?", true, id).Update("is_default", false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.Status != nil { updates["status"] = *req.Status }
|
||||||
|
|
||||||
|
if len(updates) > 0 {
|
||||||
|
if err := h.db.Model(&ds).Updates(updates).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除数据源
|
||||||
|
// DELETE /api/v1/zabbix-datasources/:id
|
||||||
|
func (h *ZabbixDSHandler) Delete(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if err := h.db.Delete(&models.ZabbixDataSource{}, id).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefault 设为默认
|
||||||
|
// POST /api/v1/zabbix-datasources/:id/default
|
||||||
|
func (h *ZabbixDSHandler) SetDefault(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
h.db.Model(&models.ZabbixDataSource{}).Where("is_default = ?", true).Update("is_default", false)
|
||||||
|
if err := h.db.Model(&models.ZabbixDataSource{}).Where("id = ?", id).Update("is_default", true).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "设置失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已设为默认"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConnection 测试 Zabbix 连接
|
||||||
|
// POST /api/v1/zabbix-datasources/test
|
||||||
|
func (h *ZabbixDSHandler) TestConnection(c *gin.Context) {
|
||||||
|
var req models.TestZabbixDSReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := zabbix.NewClient(req.URL, req.Username, req.Password,
|
||||||
|
zabbix.WithHTTPTimeout(10*time.Second),
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := client.Auth(ctx); err != nil {
|
||||||
|
c.JSON(http.StatusOK, models.Response{
|
||||||
|
Code: 200,
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "认证失败: " + err.Error(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token := client.GetAuthToken()
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{
|
||||||
|
Code: 200,
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"message": "连接成功,Zabbix 认证通过",
|
||||||
|
"session_id": token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -521,6 +521,19 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
promDSGroup.POST("/test", promDSHandler.TestConnection)
|
promDSGroup.POST("/test", promDSHandler.TestConnection)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zabbix 数据源管理 — CRUD + 连接测试 // Logan.Li
|
||||||
|
zabbixDSHandler := handlers.NewZabbixDSHandler(db)
|
||||||
|
zabbixDSGroup := authorized.Group("/zabbix-datasources")
|
||||||
|
{
|
||||||
|
zabbixDSGroup.GET("", zabbixDSHandler.List)
|
||||||
|
zabbixDSGroup.GET("/:id", zabbixDSHandler.Get)
|
||||||
|
zabbixDSGroup.POST("", zabbixDSHandler.Create)
|
||||||
|
zabbixDSGroup.PUT("/:id", zabbixDSHandler.Update)
|
||||||
|
zabbixDSGroup.DELETE("/:id", zabbixDSHandler.Delete)
|
||||||
|
zabbixDSGroup.POST("/:id/default", zabbixDSHandler.SetDefault)
|
||||||
|
zabbixDSGroup.POST("/test", zabbixDSHandler.TestConnection)
|
||||||
|
}
|
||||||
|
|
||||||
// 站点拨测 — HTTP/TCP/DNS/PING 主动监控 // Logan.Li
|
// 站点拨测 — HTTP/TCP/DNS/PING 主动监控 // Logan.Li
|
||||||
siteHandler := handlers.NewSiteHandler(db)
|
siteHandler := handlers.NewSiteHandler(db)
|
||||||
siteGroup := authorized.Group("/site")
|
siteGroup := authorized.Group("/site")
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// ZabbixDataSource Zabbix 数据源配置(参考 Grafana 界面设计) // Logan.Li
|
||||||
|
type ZabbixDataSource struct {
|
||||||
|
ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"`
|
||||||
|
Name string `gorm:"type:varchar(128);not null;uniqueIndex;comment:数据源名称" json:"name"`
|
||||||
|
URL string `gorm:"type:varchar(512);not null;comment:Zabbix API地址" json:"url"`
|
||||||
|
Username string `gorm:"type:varchar(128);not null;comment:用户名" json:"username"`
|
||||||
|
Password string `gorm:"type:varchar(256);not null;comment:密码" json:"password,omitempty"`
|
||||||
|
|
||||||
|
// Trends 设置
|
||||||
|
TrendsAfter int `gorm:"default:0;comment:趋势数据起始时间(天前)" json:"trends_after"`
|
||||||
|
TrendsRange int `gorm:"default:0;comment:趋势数据范围(秒)" json:"trends_range"`
|
||||||
|
TrendsCacheTTL int `gorm:"default:3600;comment:趋势缓存TTL(秒)" json:"trends_cache_ttl"`
|
||||||
|
TrendsTimeout int `gorm:"default:30;comment:趋势查询超时(秒)" json:"trends_timeout"`
|
||||||
|
|
||||||
|
// Direct DB Connection
|
||||||
|
DirectDB bool `gorm:"default:false;comment:启用直连数据库" json:"direct_db"`
|
||||||
|
|
||||||
|
// Other
|
||||||
|
DisableAcknowledges bool `gorm:"default:false;comment:只读用户禁用确认" json:"disable_acknowledges"`
|
||||||
|
DisableDataAlignment bool `gorm:"default:false;comment:禁用数据对齐" json:"disable_data_alignment"`
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
IsDefault bool `gorm:"default:false;index;comment:是否默认数据源" json:"is_default"`
|
||||||
|
Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"`
|
||||||
|
|
||||||
|
CreatedAt time.Time `gorm:"not null;index;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"`
|
||||||
|
DeletedAt *time.Time `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (ZabbixDataSource) TableName() string {
|
||||||
|
return "zabbix_datasources"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 请求/响应 struct
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// CreateZabbixDSReq 创建 Zabbix 数据源请求
|
||||||
|
type CreateZabbixDSReq struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
URL string `json:"url" binding:"required"`
|
||||||
|
Username string `json:"username" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
TrendsAfter int `json:"trends_after"`
|
||||||
|
TrendsRange int `json:"trends_range"`
|
||||||
|
TrendsCacheTTL int `json:"trends_cache_ttl"`
|
||||||
|
TrendsTimeout int `json:"trends_timeout"`
|
||||||
|
DirectDB bool `json:"direct_db"`
|
||||||
|
DisableAcknowledges bool `json:"disable_acknowledges"`
|
||||||
|
DisableDataAlignment bool `json:"disable_data_alignment"`
|
||||||
|
IsDefault bool `json:"is_default"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateZabbixDSReq 更新 Zabbix 数据源请求
|
||||||
|
type UpdateZabbixDSReq struct {
|
||||||
|
Name *string `json:"name"`
|
||||||
|
URL *string `json:"url"`
|
||||||
|
Username *string `json:"username"`
|
||||||
|
Password *string `json:"password"`
|
||||||
|
TrendsAfter *int `json:"trends_after"`
|
||||||
|
TrendsRange *int `json:"trends_range"`
|
||||||
|
TrendsCacheTTL *int `json:"trends_cache_ttl"`
|
||||||
|
TrendsTimeout *int `json:"trends_timeout"`
|
||||||
|
DirectDB *bool `json:"direct_db"`
|
||||||
|
DisableAcknowledges *bool `json:"disable_acknowledges"`
|
||||||
|
DisableDataAlignment *bool `json:"disable_data_alignment"`
|
||||||
|
IsDefault *bool `json:"is_default"`
|
||||||
|
Status *int `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestZabbixDSReq 测试 Zabbix 连接请求
|
||||||
|
type TestZabbixDSReq struct {
|
||||||
|
URL string `json:"url" binding:"required"`
|
||||||
|
Username string `json:"username" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
}
|
||||||
@@ -131,6 +131,7 @@ func AutoMigrate(db *gorm.DB) error {
|
|||||||
&models.AlertSilence{}, // 告警静默 // Logan.Li
|
&models.AlertSilence{}, // 告警静默 // Logan.Li
|
||||||
&models.AlertEventV2{}, // 告警事件V2 // Logan.Li
|
&models.AlertEventV2{}, // 告警事件V2 // Logan.Li
|
||||||
&models.PrometheusDataSource{}, // Prometheus 数据源配置 // Logan.Li
|
&models.PrometheusDataSource{}, // Prometheus 数据源配置 // Logan.Li
|
||||||
|
&models.ZabbixDataSource{}, // Zabbix 数据源配置 // Logan.Li
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastErr error
|
var lastErr error
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import TraceManager from '@/components/monitor/trace/TraceManager';
|
|||||||
import HostMonitorDashboard from '@/components/monitor/dashboard/HostMonitorDashboard';
|
import HostMonitorDashboard from '@/components/monitor/dashboard/HostMonitorDashboard';
|
||||||
import { MonitorCenter } from '@/pages/MonitorCenter';
|
import { MonitorCenter } from '@/pages/MonitorCenter';
|
||||||
import { MetricsExplorer } from '@/pages/MetricsExplorer';
|
import { MetricsExplorer } from '@/pages/MetricsExplorer';
|
||||||
import PrometheusDatasourceManager from '@/components/monitor/datasource/PrometheusDatasourceManager';
|
import DatasourcePage from '@/components/monitor/datasource/DatasourcePage';
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -75,7 +75,7 @@ function App() {
|
|||||||
<Route path="alert-center" element={<AlertCenter />} />
|
<Route path="alert-center" element={<AlertCenter />} />
|
||||||
<Route path="trace" element={<TraceManager />} />
|
<Route path="trace" element={<TraceManager />} />
|
||||||
<Route path="dashboards" element={<HostMonitorDashboard />} />
|
<Route path="dashboards" element={<HostMonitorDashboard />} />
|
||||||
<Route path="datasources" element={<PrometheusDatasourceManager />} />
|
<Route path="datasources" element={<DatasourcePage />} />
|
||||||
<Route path="alerts" element={<AlertManager />} />
|
<Route path="alerts" element={<AlertManager />} />
|
||||||
<Route path="logs" element={<AlertManager />} />
|
<Route path="logs" element={<AlertManager />} />
|
||||||
<Route path="approval" element={<ApprovalFlowManager />} />
|
<Route path="approval" element={<ApprovalFlowManager />} />
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// 数据源管理页面 — Prometheus + Zabbix 双 Tab // Logan.Li
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Database } from 'lucide-react';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import PrometheusDatasourceManager from '@/components/monitor/datasource/PrometheusDatasourceManager';
|
||||||
|
import ZabbixDatasourceManager from '@/components/monitor/datasource/ZabbixDatasourceManager';
|
||||||
|
|
||||||
|
export default function DatasourcePage() {
|
||||||
|
const [activeTab, setActiveTab] = useState('prometheus');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Database className="h-5 w-5 text-blue-400" />
|
||||||
|
<h1 className="text-xl font-bold text-[var(--dg-text-primary)]">数据源管理</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
|
<TabsList className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||||
|
<TabsTrigger value="prometheus" className="data-[state=active]:bg-orange-500/10 data-[state=active]:text-orange-400">
|
||||||
|
<img
|
||||||
|
src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/prometheus.png"
|
||||||
|
className="w-4 h-4 rounded mr-1.5"
|
||||||
|
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
|
||||||
|
/>
|
||||||
|
Prometheus
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="zabbix" className="data-[state=active]:bg-orange-500/10 data-[state=active]:text-orange-400">
|
||||||
|
<img
|
||||||
|
src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/zabbix.png"
|
||||||
|
className="w-4 h-4 rounded mr-1.5"
|
||||||
|
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
|
||||||
|
/>
|
||||||
|
Zabbix
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="prometheus" className="mt-4">
|
||||||
|
<PrometheusDatasourceManager />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="zabbix" className="mt-4">
|
||||||
|
<ZabbixDatasourceManager />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
// Zabbix 数据源管理 — 参照 Grafana Zabbix 数据源配置界面 // Logan.Li
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Database, Plus, Trash2, Star, RefreshCw, Plug,
|
||||||
|
Shield, Globe, Settings, Key, ChevronDown, ChevronUp,
|
||||||
|
Edit, Clock, CheckCircle2, XCircle
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import {
|
||||||
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||||
|
import { zabbixDatasourceApi } from '@/services/api';
|
||||||
|
|
||||||
|
interface ZabbixDS {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
trends_after: number;
|
||||||
|
trends_range: number;
|
||||||
|
trends_cache_ttl: number;
|
||||||
|
trends_timeout: number;
|
||||||
|
direct_db: boolean;
|
||||||
|
disable_acknowledges: boolean;
|
||||||
|
disable_data_alignment: boolean;
|
||||||
|
is_default: boolean;
|
||||||
|
status: number;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultForm = {
|
||||||
|
name: '', url: '', username: '', password: '',
|
||||||
|
trends_after: 0, trends_range: 0, trends_cache_ttl: 3600, trends_timeout: 30,
|
||||||
|
direct_db: false, disable_acknowledges: false, disable_data_alignment: false,
|
||||||
|
is_default: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ZabbixDatasourceManager() {
|
||||||
|
const _confirm = useConfirm();
|
||||||
|
const [sources, setSources] = useState<ZabbixDS[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [editingId, setEditingId] = useState<number | null>(null);
|
||||||
|
const [form, setForm] = useState({ ...defaultForm });
|
||||||
|
|
||||||
|
const [showAuth, setShowAuth] = useState(false);
|
||||||
|
const [showTrends, setShowTrends] = useState(false);
|
||||||
|
const [showOther, setShowOther] = useState(false);
|
||||||
|
|
||||||
|
const [testing, setTesting] = useState(false);
|
||||||
|
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||||
|
|
||||||
|
const fetchList = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res: any = await zabbixDatasourceApi.list();
|
||||||
|
setSources(res?.data || res || []);
|
||||||
|
} catch {
|
||||||
|
toast.error('获取数据源列表失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => { fetchList(); }, [fetchList]);
|
||||||
|
|
||||||
|
const openCreate = () => {
|
||||||
|
setEditingId(null);
|
||||||
|
setForm({ ...defaultForm });
|
||||||
|
setTestResult(null);
|
||||||
|
setShowAuth(false); setShowTrends(false); setShowOther(false);
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEdit = (ds: ZabbixDS) => {
|
||||||
|
setEditingId(ds.id);
|
||||||
|
setForm({
|
||||||
|
name: ds.name, url: ds.url,
|
||||||
|
username: ds.username || '', password: ds.password || '',
|
||||||
|
trends_after: ds.trends_after || 0, trends_range: ds.trends_range || 0,
|
||||||
|
trends_cache_ttl: ds.trends_cache_ttl || 3600, trends_timeout: ds.trends_timeout || 30,
|
||||||
|
direct_db: ds.direct_db || false,
|
||||||
|
disable_acknowledges: ds.disable_acknowledges || false,
|
||||||
|
disable_data_alignment: ds.disable_data_alignment || false,
|
||||||
|
is_default: ds.is_default || false,
|
||||||
|
});
|
||||||
|
setTestResult(null);
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateForm = (field: string, value: any) => {
|
||||||
|
setForm(prev => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!form.name.trim()) { toast.error('请输入数据源名称'); return; }
|
||||||
|
if (!form.url.trim()) { toast.error('请输入 Zabbix API URL'); return; }
|
||||||
|
if (!form.username.trim()) { toast.error('请输入用户名'); return; }
|
||||||
|
if (!form.password.trim()) { toast.error('请输入密码'); return; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (editingId) {
|
||||||
|
await zabbixDatasourceApi.update(editingId, form);
|
||||||
|
toast.success('数据源已更新');
|
||||||
|
} else {
|
||||||
|
await zabbixDatasourceApi.create(form);
|
||||||
|
toast.success('数据源已创建');
|
||||||
|
}
|
||||||
|
setDialogOpen(false);
|
||||||
|
fetchList();
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error(err?.message || '操作失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (ds: ZabbixDS) => {
|
||||||
|
const ok = await _confirm({
|
||||||
|
title: '删除数据源',
|
||||||
|
description: `确定删除「${ds.name}」?`,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
|
try {
|
||||||
|
await zabbixDatasourceApi.delete(ds.id);
|
||||||
|
toast.success('已删除');
|
||||||
|
fetchList();
|
||||||
|
} catch { toast.error('删除失败'); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetDefault = async (ds: ZabbixDS) => {
|
||||||
|
try {
|
||||||
|
await zabbixDatasourceApi.setDefault(ds.id);
|
||||||
|
toast.success(`已将「${ds.name}」设为默认`);
|
||||||
|
fetchList();
|
||||||
|
} catch { toast.error('设置失败'); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTest = async () => {
|
||||||
|
if (!form.url.trim() || !form.username.trim()) {
|
||||||
|
toast.error('请填写 URL 和用户名');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTesting(true);
|
||||||
|
setTestResult(null);
|
||||||
|
try {
|
||||||
|
const res: any = await zabbixDatasourceApi.testConnection({
|
||||||
|
url: form.url.trim(),
|
||||||
|
username: form.username.trim(),
|
||||||
|
password: form.password || '',
|
||||||
|
});
|
||||||
|
const data = res?.data || res;
|
||||||
|
setTestResult(data);
|
||||||
|
} catch (err: any) {
|
||||||
|
setTestResult({ success: false, message: err?.message || '测试失败' });
|
||||||
|
} finally {
|
||||||
|
setTesting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-96">
|
||||||
|
<RefreshCw className="animate-spin w-8 h-8 text-[var(--dg-text-secondary)]" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||||
|
<img
|
||||||
|
src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/zabbix.png"
|
||||||
|
className="w-5 h-5 rounded"
|
||||||
|
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
|
||||||
|
/>
|
||||||
|
Zabbix 数据源
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-[var(--dg-text-muted)]">配置 Zabbix API 连接,获取主机监控数据</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={openCreate} size="sm">
|
||||||
|
<Plus className="w-4 h-4 mr-1" />添加
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sources.length === 0 ? (
|
||||||
|
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||||
|
<CardContent className="py-12 text-center text-[var(--dg-text-muted)]">
|
||||||
|
<Database className="w-12 h-12 mx-auto mb-3 opacity-30" />
|
||||||
|
<p className="text-sm">暂无 Zabbix 数据源</p>
|
||||||
|
<Button size="sm" className="mt-4" onClick={openCreate}>
|
||||||
|
<Plus className="w-4 h-4 mr-1" />添加数据源
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-3">
|
||||||
|
{sources.map(ds => (
|
||||||
|
<Card key={ds.id} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] hover:border-orange-500/30 transition-colors">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<div className={`w-2 h-2 rounded-full ${ds.status === 1 ? 'bg-emerald-400' : 'bg-slate-500'}`} />
|
||||||
|
<span className="font-medium text-[var(--dg-text-primary)]">{ds.name}</span>
|
||||||
|
{ds.is_default && <Badge className="bg-amber-500/20 text-amber-400 text-[10px]"><Star className="w-3 h-3 mr-0.5" />默认</Badge>}
|
||||||
|
{ds.status === 1
|
||||||
|
? <Badge className="bg-emerald-500/20 text-emerald-400 text-[10px]">启用</Badge>
|
||||||
|
: <Badge className="bg-slate-500/20 text-slate-400 text-[10px]">禁用</Badge>}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-[var(--dg-text-muted)] space-x-3">
|
||||||
|
<code className="text-orange-300">{ds.url}</code>
|
||||||
|
<span>用户: {ds.username}</span>
|
||||||
|
{ds.direct_db && <Badge variant="outline" className="text-[10px]">直连DB</Badge>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{!ds.is_default && (
|
||||||
|
<Button variant="ghost" size="sm" className="h-7 text-xs text-amber-400" onClick={() => handleSetDefault(ds)}>
|
||||||
|
<Star className="w-3 h-3 mr-0.5" />默认
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => openEdit(ds)}>
|
||||||
|
<Edit className="w-3 h-3 mr-0.5" />编辑
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" className="h-7 text-xs text-red-400" onClick={() => handleDelete(ds)}>
|
||||||
|
<Trash2 className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 编辑 Dialog — 参照 Grafana Zabbix 数据源配置 */}
|
||||||
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
|
<DialogContent className="max-w-xl max-h-[85vh] overflow-y-auto bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||||
|
<Database className="h-5 w-5 text-orange-400" />
|
||||||
|
{editingId ? '编辑 Zabbix 数据源' : '新建 Zabbix 数据源'}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
配置 Zabbix API 连接,用于主机监控与告警数据获取
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-2">
|
||||||
|
{/* Name + Default */}
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-[var(--dg-text-secondary)]">名称 *</Label>
|
||||||
|
<Input className="mt-1 h-9 text-sm bg-[var(--dg-bg-primary)]" placeholder="如:生产 Zabbix"
|
||||||
|
value={form.name} onChange={e => updateForm('name', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end pb-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch checked={form.is_default} onCheckedChange={v => updateForm('is_default', v)} />
|
||||||
|
<Label className="text-xs cursor-pointer">设为默认</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* HTTP + Auth */}
|
||||||
|
<div className="border border-[var(--dg-border)] rounded-lg p-3 space-y-3">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-[var(--dg-text-primary)]">
|
||||||
|
<Globe className="w-4 h-4" /> HTTP
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-[var(--dg-text-secondary)]">URL *</Label>
|
||||||
|
<Input className="mt-1 h-9 text-sm font-mono bg-[var(--dg-bg-primary)]"
|
||||||
|
placeholder="http://zabbix.example.com/api_jsonrpc.php"
|
||||||
|
value={form.url} onChange={e => updateForm('url', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* API Details — 折叠 */}
|
||||||
|
<div className="border border-[var(--dg-border)] rounded-lg overflow-hidden">
|
||||||
|
<button className="w-full flex items-center justify-between px-3 py-2.5 text-sm hover:bg-[var(--dg-bg-tertiary)]"
|
||||||
|
onClick={() => setShowAuth(!showAuth)}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Shield className="w-4 h-4" />
|
||||||
|
<span>Zabbix API 认证</span>
|
||||||
|
</div>
|
||||||
|
{showAuth ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
{showAuth && (
|
||||||
|
<div className="px-3 pb-3 space-y-3">
|
||||||
|
<Input className="h-9 text-sm bg-[var(--dg-bg-primary)]" placeholder="用户名 *"
|
||||||
|
value={form.username} onChange={e => updateForm('username', e.target.value)} />
|
||||||
|
<Input className="h-9 text-sm bg-[var(--dg-bg-primary)]" type="password" placeholder="密码 *"
|
||||||
|
value={form.password} onChange={e => updateForm('password', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Trends — 折叠 */}
|
||||||
|
<div className="border border-[var(--dg-border)] rounded-lg overflow-hidden">
|
||||||
|
<button className="w-full flex items-center justify-between px-3 py-2.5 text-sm hover:bg-[var(--dg-bg-tertiary)]"
|
||||||
|
onClick={() => setShowTrends(!showTrends)}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Clock className="w-4 h-4" />
|
||||||
|
<span>Trends 设置</span>
|
||||||
|
</div>
|
||||||
|
{showTrends ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
{showTrends && (
|
||||||
|
<div className="px-3 pb-3 space-y-3">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-[var(--dg-text-secondary)]">After (天)</Label>
|
||||||
|
<Input className="mt-1 h-9 text-sm bg-[var(--dg-bg-primary)]" type="number" min={0}
|
||||||
|
value={form.trends_after} onChange={e => updateForm('trends_after', parseInt(e.target.value) || 0)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-[var(--dg-text-secondary)]">Range (秒)</Label>
|
||||||
|
<Input className="mt-1 h-9 text-sm bg-[var(--dg-bg-primary)]" type="number" min={0}
|
||||||
|
value={form.trends_range} onChange={e => updateForm('trends_range', parseInt(e.target.value) || 0)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-[var(--dg-text-secondary)]">Cache TTL (秒)</Label>
|
||||||
|
<Input className="mt-1 h-9 text-sm bg-[var(--dg-bg-primary)]" type="number" min={0}
|
||||||
|
value={form.trends_cache_ttl} onChange={e => updateForm('trends_cache_ttl', parseInt(e.target.value) || 3600)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-[var(--dg-text-secondary)]">Timeout (秒)</Label>
|
||||||
|
<Input className="mt-1 h-9 text-sm bg-[var(--dg-bg-primary)]" type="number" min={1}
|
||||||
|
value={form.trends_timeout} onChange={e => updateForm('trends_timeout', parseInt(e.target.value) || 30)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 其他设置 — 折叠 */}
|
||||||
|
<div className="border border-[var(--dg-border)] rounded-lg overflow-hidden">
|
||||||
|
<button className="w-full flex items-center justify-between px-3 py-2.5 text-sm hover:bg-[var(--dg-bg-tertiary)]"
|
||||||
|
onClick={() => setShowOther(!showOther)}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Settings className="w-4 h-4" />
|
||||||
|
<span>其他设置</span>
|
||||||
|
</div>
|
||||||
|
{showOther ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
{showOther && (
|
||||||
|
<div className="px-3 pb-3 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-xs">Direct DB Connection</Label>
|
||||||
|
<Switch checked={form.direct_db} onCheckedChange={v => updateForm('direct_db', v)} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-xs">只读用户禁用确认</Label>
|
||||||
|
<Switch checked={form.disable_acknowledges} onCheckedChange={v => updateForm('disable_acknowledges', v)} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-xs">禁用数据对齐</Label>
|
||||||
|
<Switch checked={form.disable_data_alignment} onCheckedChange={v => updateForm('disable_data_alignment', v)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 测试结果 */}
|
||||||
|
{testResult && (
|
||||||
|
<div className={`flex items-center gap-2 p-2.5 rounded text-sm ${
|
||||||
|
testResult.success ? 'bg-emerald-500/10 text-emerald-400' : 'bg-red-500/10 text-red-400'
|
||||||
|
}`}>
|
||||||
|
{testResult.success ? <CheckCircle2 className="h-4 w-4" /> : <XCircle className="h-4 w-4" />}
|
||||||
|
{testResult.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={handleTest} disabled={testing}>
|
||||||
|
<Plug className={`w-4 h-4 mr-1 ${testing ? 'animate-pulse' : ''}`} />
|
||||||
|
测试连接
|
||||||
|
</Button>
|
||||||
|
<div className="flex-1" />
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => setDialogOpen(false)}>取消</Button>
|
||||||
|
<Button size="sm" onClick={handleSave}>保存</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -695,6 +695,17 @@ export const promDatasourceApi = {
|
|||||||
testConnection: (data: any) => http.post('/prometheus-datasources/test', data),
|
testConnection: (data: any) => http.post('/prometheus-datasources/test', data),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ==================== Zabbix 数据源管理 API ==================== // Logan.Li
|
||||||
|
export const zabbixDatasourceApi = {
|
||||||
|
list: () => http.get('/zabbix-datasources'),
|
||||||
|
get: (id: number | string) => http.get(`/zabbix-datasources/${id}`),
|
||||||
|
create: (data: any) => http.post('/zabbix-datasources', data),
|
||||||
|
update: (id: number | string, data: any) => http.put(`/zabbix-datasources/${id}`, data),
|
||||||
|
delete: (id: number | string) => http.delete(`/zabbix-datasources/${id}`),
|
||||||
|
setDefault: (id: number | string) => http.post(`/zabbix-datasources/${id}/default`),
|
||||||
|
testConnection: (data: any) => http.post('/zabbix-datasources/test', data),
|
||||||
|
};
|
||||||
|
|
||||||
// AI 智能助手 API(后端 LLM 配置 + 对话代理,OpenAI 兼容) // Logan.Li
|
// AI 智能助手 API(后端 LLM 配置 + 对话代理,OpenAI 兼容) // Logan.Li
|
||||||
export interface LLMConfigData {
|
export interface LLMConfigData {
|
||||||
configured?: boolean;
|
configured?: boolean;
|
||||||
|
|||||||
Reference in new issue
Block a user