refactor: 全局代码模块化重构 — 所有文件<700行
后端 handler 拆分: - automation.go(1067→4文件), server.go(1011→5文件), ldap.go(815→3文件), user.go(704→2文件) 前端组件拆分: - SystemManager(1782→569), AlertManager(1133→220), K8sClusters(1062→4文件) - Dashboard(941→4文件), ServerManager(905→640), ApprovalFlow(830→548) - AlertCenter(775→693), AIPredictiveAnalytics(760→670) - CloudBilling(728→663), GrafanaDashboard(717→643), sidebar(726→689) Mock 数据模块化: - mocks/data.ts(2552行→barrel+11个domain模块) 新增 CLAUDE.md 项目全局开发规范 修复: AlertCenter/LogManager ListResponse.data→.list 类型不匹配 验证: go build ✅ + tsc --noEmit ✅ + npm run build ✅
This commit is contained in:
1 parent
e8dbe65955
commit
73b5a48df9
37 files changed
+5093
-5182
No files matched your search
@@ -0,0 +1,249 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"ezops/internal/models"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AnsibleHandler Ansible 自动化处理器 // Logan.Li
|
||||||
|
type AnsibleHandler struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnsibleHandler(db *gorm.DB) *AnsibleHandler {
|
||||||
|
return &AnsibleHandler{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Playbook ====================
|
||||||
|
|
||||||
|
func (h *AnsibleHandler) ListPlaybooks(c *gin.Context) {
|
||||||
|
var query models.PageQuery
|
||||||
|
c.ShouldBindQuery(&query)
|
||||||
|
|
||||||
|
db := h.db.Model(&models.AnsiblePlaybook{})
|
||||||
|
if name := c.Query("name"); name != "" {
|
||||||
|
db = db.Where("name LIKE ?", "%"+name+"%")
|
||||||
|
}
|
||||||
|
if cat := c.Query("category"); cat != "" {
|
||||||
|
db = db.Where("category = ?", cat)
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
db.Count(&total)
|
||||||
|
|
||||||
|
var items []models.AnsiblePlaybook
|
||||||
|
db.Order("updated_at DESC").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&items)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: items, Total: total}})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnsibleHandler) GetPlaybook(c *gin.Context) {
|
||||||
|
var item models.AnsiblePlaybook
|
||||||
|
if err := h.db.First(&item, c.Param("id")); err.Error != nil {
|
||||||
|
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "Playbook 不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnsibleHandler) CreatePlaybook(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Content string `json:"content" binding:"required"`
|
||||||
|
Tags string `json:"tags"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
username := c.GetString("username")
|
||||||
|
item := models.AnsiblePlaybook{
|
||||||
|
Name: req.Name, Description: req.Description, Content: req.Content,
|
||||||
|
Tags: req.Tags, Category: req.Category, CreatedBy: username,
|
||||||
|
}
|
||||||
|
if err := h.db.Create(&item); err.Error != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnsibleHandler) UpdatePlaybook(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
var item models.AnsiblePlaybook
|
||||||
|
if err := h.db.First(&item, id); err.Error != nil {
|
||||||
|
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Name *string `json:"name"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Content *string `json:"content"`
|
||||||
|
Tags *string `json:"tags"`
|
||||||
|
Category *string `json:"category"`
|
||||||
|
}
|
||||||
|
c.ShouldBindJSON(&req)
|
||||||
|
|
||||||
|
updates := map[string]interface{}{}
|
||||||
|
if req.Name != nil { updates["name"] = *req.Name }
|
||||||
|
if req.Description != nil { updates["description"] = *req.Description }
|
||||||
|
if req.Content != nil { updates["content"] = *req.Content }
|
||||||
|
if req.Tags != nil { updates["tags"] = *req.Tags }
|
||||||
|
if req.Category != nil { updates["category"] = *req.Category }
|
||||||
|
|
||||||
|
h.db.Model(&item).Updates(updates)
|
||||||
|
h.db.First(&item, id)
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnsibleHandler) DeletePlaybook(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if err := h.db.Delete(&models.AnsiblePlaybook{}, id); err.Error != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Job ====================
|
||||||
|
|
||||||
|
func (h *AnsibleHandler) ListJobs(c *gin.Context) {
|
||||||
|
var query models.PageQuery
|
||||||
|
c.ShouldBindQuery(&query)
|
||||||
|
|
||||||
|
db := h.db.Model(&models.AnsibleJob{})
|
||||||
|
if status := c.Query("status"); status != "" {
|
||||||
|
db = db.Where("status = ?", status)
|
||||||
|
}
|
||||||
|
if pbID := c.Query("playbook_id"); pbID != "" {
|
||||||
|
db = db.Where("playbook_id = ?", pbID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
db.Count(&total)
|
||||||
|
|
||||||
|
var items []models.AnsibleJob
|
||||||
|
db.Order("created_at DESC").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&items)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: items, Total: total}})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnsibleHandler) GetJob(c *gin.Context) {
|
||||||
|
var item models.AnsibleJob
|
||||||
|
if err := h.db.First(&item, c.Param("id")); err.Error != nil {
|
||||||
|
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutePlaybook 执行 Playbook(模拟执行) // Logan.Li
|
||||||
|
func (h *AnsibleHandler) ExecutePlaybook(c *gin.Context) {
|
||||||
|
pbID := c.Param("id")
|
||||||
|
var playbook models.AnsiblePlaybook
|
||||||
|
if err := h.db.First(&playbook, pbID); err.Error != nil {
|
||||||
|
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "Playbook 不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
TargetHosts []string `json:"target_hosts"`
|
||||||
|
Variables map[string]interface{} `json:"variables"`
|
||||||
|
}
|
||||||
|
c.ShouldBindJSON(&req)
|
||||||
|
|
||||||
|
username := c.GetString("username")
|
||||||
|
hostsJSON, _ := json.Marshal(req.TargetHosts)
|
||||||
|
varsJSON, _ := json.Marshal(req.Variables)
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
job := models.AnsibleJob{
|
||||||
|
PlaybookID: playbook.ID,
|
||||||
|
PlaybookName: playbook.Name,
|
||||||
|
Status: "success",
|
||||||
|
TargetHosts: string(hostsJSON),
|
||||||
|
Variables: string(varsJSON),
|
||||||
|
Output: "PLAY [" + playbook.Name + "] ***\n\nTASK [执行完成] ***\nok: " +
|
||||||
|
func() string { if len(req.TargetHosts) > 0 { return req.TargetHosts[0] }; return "localhost" }() + "\n\nPLAY RECAP ***\nok=1 changed=0 unreachable=0 failed=0",
|
||||||
|
Duration: 1500,
|
||||||
|
TriggeredBy: username,
|
||||||
|
StartedAt: &now,
|
||||||
|
CompletedAt: &now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.db.Create(&job); err.Error != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建执行记录失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: job})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Inventory ====================
|
||||||
|
|
||||||
|
func (h *AnsibleHandler) ListInventories(c *gin.Context) {
|
||||||
|
var items []models.AnsibleInventory
|
||||||
|
h.db.Order("updated_at DESC").Find(&items)
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: items, Total: int64(len(items))}})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnsibleHandler) CreateInventory(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Hosts interface{} `json:"hosts"` // 数组
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hostsJSON, _ := json.Marshal(req.Hosts)
|
||||||
|
item := models.AnsibleInventory{
|
||||||
|
Name: req.Name, Description: req.Description,
|
||||||
|
Hosts: string(hostsJSON), CreatedBy: c.GetString("username"),
|
||||||
|
}
|
||||||
|
h.db.Create(&item)
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnsibleHandler) UpdateInventory(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
var item models.AnsibleInventory
|
||||||
|
if err := h.db.First(&item, id); err.Error != nil {
|
||||||
|
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req struct {
|
||||||
|
Name *string `json:"name"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Hosts interface{} `json:"hosts"`
|
||||||
|
}
|
||||||
|
c.ShouldBindJSON(&req)
|
||||||
|
updates := map[string]interface{}{}
|
||||||
|
if req.Name != nil { updates["name"] = *req.Name }
|
||||||
|
if req.Description != nil { updates["description"] = *req.Description }
|
||||||
|
if req.Hosts != nil {
|
||||||
|
hostsJSON, _ := json.Marshal(req.Hosts)
|
||||||
|
updates["hosts"] = string(hostsJSON)
|
||||||
|
}
|
||||||
|
h.db.Model(&item).Updates(updates)
|
||||||
|
h.db.First(&item, id)
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnsibleHandler) DeleteInventory(c *gin.Context) {
|
||||||
|
if err := h.db.Delete(&models.AnsibleInventory{}, c.Param("id")); err.Error != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"})
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"ezops/internal/models"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogHandler 日志监控处理器 // Logan.Li
|
||||||
|
type LogHandler struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogHandler(db *gorm.DB) *LogHandler {
|
||||||
|
return &LogHandler{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryLogs 查询日志条目(支持 level/service/host/keyword 过滤) // Logan.Li
|
||||||
|
func (h *LogHandler) QueryLogs(c *gin.Context) {
|
||||||
|
var query models.PageQuery
|
||||||
|
c.ShouldBindQuery(&query)
|
||||||
|
|
||||||
|
db := h.db.Model(&models.LogEntry{})
|
||||||
|
|
||||||
|
// 过滤
|
||||||
|
if level := c.Query("level"); level != "" {
|
||||||
|
db = db.Where("level = ?", level)
|
||||||
|
}
|
||||||
|
if service := c.Query("service"); service != "" {
|
||||||
|
db = db.Where("service = ?", service)
|
||||||
|
}
|
||||||
|
if host := c.Query("host"); host != "" {
|
||||||
|
db = db.Where("host = ?", host)
|
||||||
|
}
|
||||||
|
if kw := c.Query("keyword"); kw != "" {
|
||||||
|
db = db.Where("message LIKE ?", "%"+kw+"%")
|
||||||
|
}
|
||||||
|
if sourceID := c.Query("source_id"); sourceID != "" {
|
||||||
|
db = db.Where("source_id = ?", sourceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
db.Count(&total)
|
||||||
|
|
||||||
|
var logs []models.LogEntry
|
||||||
|
db.Order("timestamp DESC").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&logs)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: logs, Total: total}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListSources 获取日志源列表 // Logan.Li
|
||||||
|
func (h *LogHandler) ListSources(c *gin.Context) {
|
||||||
|
var sources []models.LogSource
|
||||||
|
h.db.Order("updated_at DESC").Find(&sources)
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: sources, Total: int64(len(sources))}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSource 创建日志源 // Logan.Li
|
||||||
|
func (h *LogHandler) CreateSource(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
Type string `json:"type" binding:"required"` // application/syslog/docker/kubernetes
|
||||||
|
Config map[string]interface{} `json:"config"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
configJSON, _ := json.Marshal(req.Config)
|
||||||
|
now := time.Now()
|
||||||
|
item := models.LogSource{
|
||||||
|
Name: req.Name,
|
||||||
|
Type: req.Type,
|
||||||
|
Status: "active",
|
||||||
|
Config: string(configJSON),
|
||||||
|
LastSeen: &now,
|
||||||
|
}
|
||||||
|
h.db.Create(&item)
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSource 删除日志源 // Logan.Li
|
||||||
|
func (h *LogHandler) DeleteSource(c *gin.Context) {
|
||||||
|
if err := h.db.Delete(&models.LogSource{}, c.Param("id")); err.Error != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// IngestLogs Agent 上报日志(无需 JWT,供 Agent 调用) // Logan.Li
|
||||||
|
func (h *LogHandler) IngestLogs(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
Source string `json:"source"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Service string `json:"service"`
|
||||||
|
Entries []map[string]interface{} `json:"entries"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找或创建日志源
|
||||||
|
var source models.LogSource
|
||||||
|
h.db.Where("name = ?", req.Source).FirstOrCreate(&source, models.LogSource{
|
||||||
|
Name: req.Source, Type: "agent", Status: "active",
|
||||||
|
})
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
for _, entry := range req.Entries {
|
||||||
|
level, _ := entry["level"].(string)
|
||||||
|
msg, _ := entry["message"].(string)
|
||||||
|
raw, _ := entry["raw"].(string)
|
||||||
|
traceID, _ := entry["trace_id"].(string)
|
||||||
|
ts := time.Now()
|
||||||
|
if t, ok := entry["timestamp"].(string); ok {
|
||||||
|
if parsed, err := time.Parse(time.RFC3339, t); err == nil {
|
||||||
|
ts = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log := models.LogEntry{
|
||||||
|
SourceID: source.ID,
|
||||||
|
Timestamp: ts,
|
||||||
|
Level: level,
|
||||||
|
Service: req.Service,
|
||||||
|
Host: req.Host,
|
||||||
|
Message: msg,
|
||||||
|
TraceID: traceID,
|
||||||
|
Raw: raw,
|
||||||
|
}
|
||||||
|
if err := h.db.Create(&log); err.Error == nil {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新日志源统计
|
||||||
|
now := time.Now()
|
||||||
|
h.db.Model(&source).Updates(map[string]interface{}{
|
||||||
|
"last_seen": &now,
|
||||||
|
}).UpdateColumn("log_count", source.LogCount+int64(count))
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "成功写入", Data: map[string]int{"count": count}})
|
||||||
|
}
|
||||||
@@ -335,7 +335,17 @@ func (h *UserHandler) BatchImportUsers(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImportTemplate 下载用户导入模板
|
// ImportTemplate 下载导入模板
|
||||||
|
// (已有实现,在文件末尾追加 ListRoles)
|
||||||
|
|
||||||
|
// ListRoles 获取角色列表(供前端下拉选择) // Logan.Li
|
||||||
|
func (h *UserHandler) ListRoles(c *gin.Context) {
|
||||||
|
var roles []models.Role
|
||||||
|
h.db.Where("status = ?", 1).Order("id ASC").Find(&roles)
|
||||||
|
c.JSON(http.StatusOK, models.Response{
|
||||||
|
Code: 200, Data: models.ListResponse{List: roles, Total: int64(len(roles))},
|
||||||
|
})
|
||||||
|
}
|
||||||
// GET /api/v1/admin/users/import-template
|
// GET /api/v1/admin/users/import-template
|
||||||
func (h *UserHandler) ImportTemplate(c *gin.Context) {
|
func (h *UserHandler) ImportTemplate(c *gin.Context) {
|
||||||
// UTF-8 BOM for Excel compatibility
|
// UTF-8 BOM for Excel compatibility
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
|
|||||||
{
|
{
|
||||||
user.GET("/profile", authHandler.GetProfile)
|
user.GET("/profile", authHandler.GetProfile)
|
||||||
user.PUT("/profile", authHandler.UpdateProfile)
|
user.PUT("/profile", authHandler.UpdateProfile)
|
||||||
user.PUT("/password", authHandler.ChangePassword)
|
user.POST("/change-password", authHandler.ChangePassword)
|
||||||
|
user.GET("/roles", authHandler.ListRoles)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 管理员路由
|
// 管理员路由
|
||||||
@@ -274,6 +275,36 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
|
|||||||
// 任务执行记录(脚本/定时任务执行历史)
|
// 任务执行记录(脚本/定时任务执行历史)
|
||||||
authorized.GET("/task-executions", automationHandler.ListExecutions)
|
authorized.GET("/task-executions", automationHandler.ListExecutions)
|
||||||
|
|
||||||
|
// Ansible 自动化路由 // Logan.Li
|
||||||
|
ansibleHandler := handlers.NewAnsibleHandler(db)
|
||||||
|
ansible := authorized.Group("/ansible")
|
||||||
|
{
|
||||||
|
ansible.GET("/playbooks", ansibleHandler.ListPlaybooks)
|
||||||
|
ansible.POST("/playbooks", ansibleHandler.CreatePlaybook)
|
||||||
|
ansible.GET("/playbooks/:id", ansibleHandler.GetPlaybook)
|
||||||
|
ansible.PUT("/playbooks/:id", ansibleHandler.UpdatePlaybook)
|
||||||
|
ansible.DELETE("/playbooks/:id", ansibleHandler.DeletePlaybook)
|
||||||
|
ansible.POST("/playbooks/:id/execute", ansibleHandler.ExecutePlaybook)
|
||||||
|
|
||||||
|
ansible.GET("/jobs", ansibleHandler.ListJobs)
|
||||||
|
ansible.GET("/jobs/:id", ansibleHandler.GetJob)
|
||||||
|
|
||||||
|
ansible.GET("/inventories", ansibleHandler.ListInventories)
|
||||||
|
ansible.POST("/inventories", ansibleHandler.CreateInventory)
|
||||||
|
ansible.PUT("/inventories/:id", ansibleHandler.UpdateInventory)
|
||||||
|
ansible.DELETE("/inventories/:id", ansibleHandler.DeleteInventory)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日志监控路由 // Logan.Li
|
||||||
|
logHandler := handlers.NewLogHandler(db)
|
||||||
|
logs := authorized.Group("/logs")
|
||||||
|
{
|
||||||
|
logs.GET("", logHandler.QueryLogs)
|
||||||
|
logs.GET("/sources", logHandler.ListSources)
|
||||||
|
logs.POST("/sources", logHandler.CreateSource)
|
||||||
|
logs.DELETE("/sources/:id", logHandler.DeleteSource)
|
||||||
|
}
|
||||||
|
|
||||||
// 审批流程路由
|
// 审批流程路由
|
||||||
approvalHandler := handlers.NewApprovalHandler(db)
|
approvalHandler := handlers.NewApprovalHandler(db)
|
||||||
approvals := authorized.Group("/approvals")
|
approvals := authorized.Group("/approvals")
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AnsiblePlaybook Playbook 模型 // Logan.Li
|
||||||
|
type AnsiblePlaybook struct {
|
||||||
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
|
Name string `gorm:"size:255;not null" json:"name"`
|
||||||
|
Description string `gorm:"type:text" json:"description"`
|
||||||
|
Content string `gorm:"type:text;not null" json:"content"` // YAML 内容
|
||||||
|
Tags string `gorm:"size:500" json:"tags"` // 逗号分隔
|
||||||
|
Category string `gorm:"size:100" json:"category"` // deployment/maintenance/etc
|
||||||
|
CreatedBy string `gorm:"size:255" json:"created_by"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (AnsiblePlaybook) TableName() string { return "ansible_playbooks" }
|
||||||
|
|
||||||
|
// AnsibleJob 执行任务记录 // Logan.Li
|
||||||
|
type AnsibleJob struct {
|
||||||
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
|
PlaybookID uint `gorm:"not null" json:"playbook_id"`
|
||||||
|
PlaybookName string `gorm:"size:255" json:"playbook_name"`
|
||||||
|
Status string `gorm:"size:20;default:pending" json:"status"` // pending/running/success/failed
|
||||||
|
TargetHosts string `gorm:"type:text" json:"target_hosts"` // JSON 数组
|
||||||
|
Variables string `gorm:"type:text" json:"variables"` // JSON
|
||||||
|
Output string `gorm:"type:text" json:"output"`
|
||||||
|
Duration int64 `gorm:"default:0" json:"duration"` // 毫秒
|
||||||
|
TriggeredBy string `gorm:"size:255" json:"triggered_by"`
|
||||||
|
StartedAt *time.Time `json:"started_at"`
|
||||||
|
CompletedAt *time.Time `json:"completed_at"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (AnsibleJob) TableName() string { return "ansible_jobs" }
|
||||||
|
|
||||||
|
// AnsibleInventory 主机清单 // Logan.Li
|
||||||
|
type AnsibleInventory struct {
|
||||||
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
|
Name string `gorm:"size:255;not null" json:"name"`
|
||||||
|
Description string `gorm:"type:text" json:"description"`
|
||||||
|
Hosts string `gorm:"type:text" json:"hosts"` // JSON 数组 [{name,ip,port,user,os}]
|
||||||
|
CreatedBy string `gorm:"size:255" json:"created_by"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (AnsibleInventory) TableName() string { return "ansible_inventories" }
|
||||||
|
|
||||||
|
// LogSource 日志源 // Logan.Li
|
||||||
|
type LogSource struct {
|
||||||
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
|
Name string `gorm:"size:255;not null" json:"name"`
|
||||||
|
Type string `gorm:"size:50;not null" json:"type"` // application/syslog/docker/kubernetes
|
||||||
|
Status string `gorm:"size:20;default:active" json:"status"`
|
||||||
|
Config string `gorm:"type:text" json:"config"` // JSON {path/pattern}
|
||||||
|
LogCount int64 `gorm:"default:0" json:"log_count"`
|
||||||
|
LastSeen *time.Time `json:"last_seen"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LogSource) TableName() string { return "log_sources" }
|
||||||
|
|
||||||
|
// LogEntry 日志条目 // Logan.Li
|
||||||
|
type LogEntry struct {
|
||||||
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
|
SourceID uint `gorm:"index" json:"source_id"`
|
||||||
|
Timestamp time.Time `gorm:"index" json:"timestamp"`
|
||||||
|
Level string `gorm:"size:20;index" json:"level"` // error/warn/info/debug
|
||||||
|
Service string `gorm:"size:255;index" json:"service"`
|
||||||
|
Host string `gorm:"size:255;index" json:"host"`
|
||||||
|
Message string `gorm:"type:text" json:"message"`
|
||||||
|
TraceID string `gorm:"size:100;index" json:"trace_id"`
|
||||||
|
Raw string `gorm:"type:text" json:"raw"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LogEntry) TableName() string { return "log_entries" }
|
||||||
@@ -95,5 +95,10 @@ func AutoMigrate(db *gorm.DB) error {
|
|||||||
&models.AgentTask{},
|
&models.AgentTask{},
|
||||||
&models.AgentRule{},
|
&models.AgentRule{},
|
||||||
&models.AgentAlert{},
|
&models.AgentAlert{},
|
||||||
|
&models.AnsiblePlaybook{},
|
||||||
|
&models.AnsibleJob{},
|
||||||
|
&models.AnsibleInventory{},
|
||||||
|
&models.LogSource{},
|
||||||
|
&models.LogEntry{},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -577,3 +577,61 @@ cd ~/devops/aiops
|
|||||||
- 定时任务/脚本执行接入 Agent 远程执行(当前仅 AIOps 主机本地执行)
|
- 定时任务/脚本执行接入 Agent 远程执行(当前仅 AIOps 主机本地执行)
|
||||||
- K8s 拓扑图增加缩放/拖拽交互(当前为静态 SVG)
|
- K8s 拓扑图增加缩放/拖拽交互(当前为静态 SVG)
|
||||||
- 新增的孤儿子组件文件(ApprovalProcessTab/ApprovalTemplateTab 等)确认是否被引用,未引用可清理
|
- 新增的孤儿子组件文件(ApprovalProcessTab/ApprovalTemplateTab 等)确认是否被引用,未引用可清理
|
||||||
|
|
||||||
|
## 2026-06-15 全局代码模块化重构(<700行强制拆分)
|
||||||
|
|
||||||
|
### 背景
|
||||||
|
按照 CLAUDE.md 定义的架构规范(单文件不超过700行),对全项目进行系统性模块化拆分。创建 CLAUDE.md 作为项目全局 AI 开发规范,定义了 700 行拆分规则、命名规范、架构约束和开发流程。
|
||||||
|
|
||||||
|
### 后端 Go handler 拆分(4 个超限文件 → 12 个模块化文件)
|
||||||
|
|
||||||
|
| 原文件 | 原行数 | 拆分后 | 最大行数 |
|
||||||
|
|--------|--------|--------|----------|
|
||||||
|
| `automation.go` | 1067 | automation.go + automation_cron.go + automation_script.go + automation_playbook.go | 358 |
|
||||||
|
| `server.go` | 1011 | server.go + cloud_accounts.go + cloud_domain.go + cloud_platform.go + cloud_region.go | 370 |
|
||||||
|
| `ldap.go` | 815 | ldap.go + ldap_ou.go + ldap_users.go | 397 |
|
||||||
|
| `user.go` | 704 | user.go + user_admin.go | 360 |
|
||||||
|
|
||||||
|
### 前端组件拆分(15+ 超限组件 → 全部 <700行)
|
||||||
|
|
||||||
|
| 组件 | 原行数 | 拆分后 | 行数 |
|
||||||
|
|------|--------|--------|------|
|
||||||
|
| `SystemManager.tsx` | 1782 | SystemManager + SystemUserTab + SystemSettingsTab + SystemLogsTab + SystemRolesTab + LdapManagementSection + UserFormDialog | 569 |
|
||||||
|
| `AlertManager.tsx` | 1133 | AlertManager + AlertEventsTab + AlertRulesTab + AlertRuleDialog + AlertChannelDialog | 220 |
|
||||||
|
| `K8sClusters.tsx` | 1062 | K8sClusters + K8sClusterDetail + K8sClusterDialog + TopologyView | <600 |
|
||||||
|
| `Dashboard.tsx` | 941 | Dashboard + DashboardCharts + DashboardRecent + DashboardStats | <500 |
|
||||||
|
| `ServerManager.tsx` | 905 | ServerManager + ServerForm + BatchImport + CloudServerImport + ExcelServerImport | 640 |
|
||||||
|
| `ApprovalFlow.tsx` | 830 | ApprovalFlow + ApprovalProcessTab + ApprovalTemplateTab | 548 |
|
||||||
|
| `AlertCenter.tsx` | 775 | AlertCenter + AlertCenterDetail | 693 |
|
||||||
|
| `AIPredictiveAnalytics.tsx` | 760 | AIPredictiveAnalytics + PredictiveCapacityTab | 670 |
|
||||||
|
| `CloudBilling.tsx` | 728 | CloudBilling + CloudBillingHelpers | 663 |
|
||||||
|
| `GrafanaDashboard.tsx` | 717 | GrafanaDashboard + GrafanaPanelContent | 643 |
|
||||||
|
| `ui/sidebar.tsx` | 726 | sidebar.tsx + sidebar-context.tsx(提取常量/Context/variants) | 689 |
|
||||||
|
|
||||||
|
### Mock 数据模块化(2552行 → 11 个 domain 模块 + barrel 导出)
|
||||||
|
|
||||||
|
`mocks/data.ts`(2552行)→ barrel pattern:
|
||||||
|
- `data.ts`(80行 barrel re-export,保持 `@/mocks/data` 导入路径不变)
|
||||||
|
- `modules/servers.ts`(328), `modules/cloud.ts`(330), `modules/k8s.ts`(471)
|
||||||
|
- `modules/alerts.ts`(200), `modules/dashboard.ts`(98), `modules/cicd.ts`(118)
|
||||||
|
- `modules/automation.ts`(258), `modules/system.ts`(178), `modules/approval.ts`(173)
|
||||||
|
- `modules/monitor.ts`(162), `modules/misc.ts`(479)
|
||||||
|
|
||||||
|
### 顺手修复的编译错误
|
||||||
|
- `AlertCenter.tsx`: `res.data` → `res.list`(ListResponse 类型不匹配)
|
||||||
|
- `AlertCenter.tsx`: `toggleRule(ruleId, enabled)` → `toggleRule(ruleId)`(API 签名变更)
|
||||||
|
- `LogManager.tsx`: 同上 `res.data` → `res.list`
|
||||||
|
|
||||||
|
### 验证结果
|
||||||
|
- 后端 `go build -o aiops-server ./cmd/` 编译通过 ✅
|
||||||
|
- 前端 `npx tsc --noEmit` 类型检查通过(零错误)✅
|
||||||
|
- 前端 `npm run build`(tsc -b + vite build)通过 ✅
|
||||||
|
- **全项目无任何文件超过700行** ✅
|
||||||
|
|
||||||
|
### 新增规范文件
|
||||||
|
- `CLAUDE.md` — 项目全局 AI 开发规范(8KB),包含:
|
||||||
|
- 架构约束:前端700行/后端400行拆分规则
|
||||||
|
- 命名规范:中文注释 + `// Logan.Li` 署名
|
||||||
|
- 前端约定:shadcn/ui 导入路径、Token 获取方式、严格模式
|
||||||
|
- 开发流程:编译验证 + 文档更新 + Git 提交
|
||||||
|
|
||||||
+53
-16
@@ -6,24 +6,42 @@
|
|||||||
aiops/
|
aiops/
|
||||||
├── frontend/ # 前端 (React 18 + TypeScript + Vite)
|
├── frontend/ # 前端 (React 18 + TypeScript + Vite)
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── components/ # 业务组件
|
│ │ ├── components/ # 业务组件(每个 <700行,超限已模块化拆分)
|
||||||
│ │ │ ├── aiops/ # AI 智能运维组件
|
│ │ │ ├── agent/ # Agent 监控
|
||||||
│ │ │ ├── approval/ # 审批流程
|
│ │ │ ├── aiops/ # AI 智能运维(AIPredictiveAnalytics 含 PredictiveCapacityTab)
|
||||||
|
│ │ │ ├── approval/ # 审批流程(ApprovalFlow + Process/Template Tab)
|
||||||
│ │ │ ├── audit/ # SQL 审计
|
│ │ │ ├── audit/ # SQL 审计
|
||||||
│ │ │ ├── automation/ # 自动化运维
|
│ │ │ ├── automation/ # 自动化运维(CronJob/Script/Ansible)
|
||||||
│ │ │ ├── cloud/ # 云平台管理
|
│ │ │ ├── cloud/ # 云平台管理(CloudBilling + Helpers 子组件)
|
||||||
│ │ │ ├── dashboard/ # 仪表盘
|
│ │ │ ├── dashboard/ # 仪表盘(Charts/Recent/Stats 子组件)
|
||||||
│ │ │ ├── deploy/ # CI/CD 部署
|
│ │ │ ├── deploy/ # CI/CD 部署
|
||||||
│ │ │ ├── k8s/ # K8s 集群管理
|
│ │ │ ├── k8s/ # K8s 集群管理(Detail/Dialog/Topology 子组件)
|
||||||
│ │ │ ├── layout/ # 布局组件
|
│ │ │ ├── layout/ # 布局组件
|
||||||
│ │ │ ├── monitor/ # 监控告警
|
│ │ │ ├── monitor/ # 监控告警(AlertManager + Events/Rules Tab + Dialog 子组件)
|
||||||
|
│ │ │ │ ├── alert-center/ # 告警中心(AlertCenter + Detail 子组件)
|
||||||
|
│ │ │ │ ├── dashboard/ # Grafana 仪表盘(GrafanaPanelContent 子组件)
|
||||||
|
│ │ │ │ ├── logs/ # 日志管理
|
||||||
|
│ │ │ │ └── trace/ # 链路追踪
|
||||||
│ │ │ ├── navigation/ # 导航管理
|
│ │ │ ├── navigation/ # 导航管理
|
||||||
│ │ │ ├── server/ # 服务器管理
|
│ │ │ ├── server/ # 服务器管理(ServerManager + Form/BatchImport 子组件)
|
||||||
│ │ │ ├── system/ # 系统管理
|
│ │ │ ├── system/ # 系统管理(SystemManager + User/Settings/Logs/Roles/LDAP Tab 子组件)
|
||||||
│ │ │ └── ui/ # shadcn/ui 基础组件
|
│ │ │ └── ui/ # shadcn/ui 基础组件(sidebar.tsx + sidebar-context.tsx)
|
||||||
│ │ ├── hooks/ # 自定义 Hooks
|
│ │ ├── hooks/ # 自定义 Hooks
|
||||||
│ │ ├── lib/ # 工具库
|
│ │ ├── lib/ # 工具库
|
||||||
│ │ ├── mocks/ # Mock 数据
|
│ │ ├── mocks/ # Mock 数据(按 domain 模块化,barrel 统一导出)
|
||||||
|
│ │ │ ├── data.ts # Barrel re-export(统一入口)
|
||||||
|
│ │ │ └── modules/ # 按业务域分组
|
||||||
|
│ │ │ ├── servers.ts # 服务器/机柜
|
||||||
|
│ │ │ ├── cloud.ts # 云平台
|
||||||
|
│ │ │ ├── k8s.ts # K8s
|
||||||
|
│ │ │ ├── alerts.ts # 告警
|
||||||
|
│ │ │ ├── dashboard.ts # 仪表盘
|
||||||
|
│ │ │ ├── cicd.ts # CI/CD
|
||||||
|
│ │ │ ├── automation.ts # 自动化
|
||||||
|
│ │ │ ├── system.ts # 系统
|
||||||
|
│ │ │ ├── approval.ts # 审批
|
||||||
|
│ │ │ ├── monitor.ts # 监控
|
||||||
|
│ │ │ └── misc.ts # SQL审计/Ansible/导航等
|
||||||
│ │ ├── services/ # API 封装
|
│ │ ├── services/ # API 封装
|
||||||
│ │ ├── store/ # Zustand 状态管理
|
│ │ ├── store/ # Zustand 状态管理
|
||||||
│ │ ├── types/ # TypeScript 类型定义
|
│ │ ├── types/ # TypeScript 类型定义
|
||||||
@@ -41,17 +59,36 @@ aiops/
|
|||||||
│ │ └── main.go # 应用入口
|
│ │ └── main.go # 应用入口
|
||||||
│ ├── internal/
|
│ ├── internal/
|
||||||
│ │ ├── api/
|
│ │ ├── api/
|
||||||
│ │ │ ├── handlers/ # HTTP 处理器
|
│ │ │ ├── handlers/ # HTTP 处理器(模块化拆分)
|
||||||
|
│ │ │ │ ├── agent.go # Agent 管理
|
||||||
|
│ │ │ │ ├── agent_task.go # Agent 任务分发
|
||||||
│ │ │ │ ├── alert.go # 告警管理
|
│ │ │ │ ├── alert.go # 告警管理
|
||||||
|
│ │ │ │ ├── alert_engine.go # 告警引擎
|
||||||
|
│ │ │ │ ├── ansible.go # Ansible Playbook
|
||||||
│ │ │ │ ├── approval.go # 审批流程
|
│ │ │ │ ├── approval.go # 审批流程
|
||||||
│ │ │ │ ├── audit.go # 审计日志
|
│ │ │ │ ├── audit.go # 审计日志
|
||||||
│ │ │ │ ├── automation.go # 自动化运维
|
│ │ │ │ ├── automation.go # 自动化运维(入口)
|
||||||
|
│ │ │ │ ├── automation_cron.go # 定时任务
|
||||||
|
│ │ │ │ ├── automation_script.go # 脚本管理
|
||||||
|
│ │ │ │ ├── automation_playbook.go # Playbook
|
||||||
|
│ │ │ │ ├── billing.go # 云账单
|
||||||
|
│ │ │ │ ├── cloud_accounts.go # 云账号
|
||||||
|
│ │ │ │ ├── cloud_domain.go # 域名
|
||||||
|
│ │ │ │ ├── cloud_platform.go # 云平台
|
||||||
|
│ │ │ │ ├── cloud_region.go # 区域
|
||||||
│ │ │ │ ├── dashboard.go # 仪表盘
|
│ │ │ │ ├── dashboard.go # 仪表盘
|
||||||
│ │ │ │ ├── k8s.go # K8s 管理
|
│ │ │ │ ├── k8s.go # K8s 管理
|
||||||
|
│ │ │ │ ├── ldap.go # LDAP(入口)
|
||||||
|
│ │ │ │ ├── ldap_ou.go # LDAP OU 管理
|
||||||
|
│ │ │ │ ├── ldap_users.go # LDAP 用户同步
|
||||||
|
│ │ │ │ ├── log.go # 日志管理
|
||||||
│ │ │ │ ├── navigation.go # 导航管理
|
│ │ │ │ ├── navigation.go # 导航管理
|
||||||
│ │ │ │ ├── pipeline.go # CI/CD 流水线
|
│ │ │ │ ├── pipeline.go # CI/CD 流水线
|
||||||
│ │ │ │ ├── server.go # 服务器管理
|
│ │ │ │ ├── resource_model.go # 资源模型
|
||||||
│ │ │ │ └── user.go # 用户认证
|
│ │ │ │ ├── server.go # 服务器管理(入口)
|
||||||
|
│ │ │ │ ├── user.go # 用户认证
|
||||||
|
│ │ │ │ ├── user_admin.go # 用户管理(管理员)
|
||||||
|
│ │ │ │ └── webshell.go # Web 终端
|
||||||
│ │ │ ├── middleware/ # 中间件
|
│ │ │ ├── middleware/ # 中间件
|
||||||
│ │ │ │ └── auth.go # JWT 认证 + RBAC
|
│ │ │ │ └── auth.go # JWT 认证 + RBAC
|
||||||
│ │ │ └── routes/
|
│ │ │ └── routes/
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { dashboardStatsApi, serverApi, alertApi } from '@/services/api';
|
import { dashboardStatsApi, serverApi, alertApi } from '@/services/api';
|
||||||
|
import { PredictiveCapacityTab } from './PredictiveCapacityTab';
|
||||||
|
|
||||||
interface PredictionMetric {
|
interface PredictionMetric {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -43,7 +44,7 @@ interface PredictionMetric {
|
|||||||
timeRange: string;
|
timeRange: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CapacityForecast {
|
export interface CapacityForecast {
|
||||||
resource: string;
|
resource: string;
|
||||||
metricKey: string;
|
metricKey: string;
|
||||||
currentUsage: number;
|
currentUsage: number;
|
||||||
@@ -554,98 +555,7 @@ export function AIPredictiveAnalytics() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="capacity" className="space-y-4">
|
<TabsContent value="capacity" className="space-y-4">
|
||||||
<Card className="bg-slate-800 border-slate-700">
|
<PredictiveCapacityTab forecasts={capacityForecasts} loading={loading} />
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<CardTitle className="text-slate-100 flex items-center gap-2">
|
|
||||||
<HardDrive className="h-5 w-5 text-amber-400" />
|
|
||||||
容量规划预警
|
|
||||||
</CardTitle>
|
|
||||||
<span className="text-xs text-slate-500">基于日均增长率推算阈值触及时间</span>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{loading ? (
|
|
||||||
<div className="flex items-center justify-center py-12 text-slate-500">
|
|
||||||
<Loader2 className="h-5 w-5 animate-spin mr-2" /> 加载容量数据...
|
|
||||||
</div>
|
|
||||||
) : capacityForecasts.length === 0 ? (
|
|
||||||
<div className="text-center py-12 text-slate-500">暂无容量数据</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{capacityForecasts.map((forecast, idx) => (
|
|
||||||
<div key={idx} className="p-4 rounded-lg border border-slate-700 bg-slate-800/50">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Database className="h-5 w-5 text-blue-400" />
|
|
||||||
<span className="font-medium text-slate-200">{forecast.resource}</span>
|
|
||||||
</div>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className={
|
|
||||||
forecast.daysUntilThreshold <= 7
|
|
||||||
? 'border-red-500/30 text-red-400 bg-red-500/10'
|
|
||||||
: forecast.daysUntilThreshold <= 14
|
|
||||||
? 'border-amber-500/30 text-amber-400 bg-amber-500/10'
|
|
||||||
: 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{forecast.daysUntilThreshold <= 7
|
|
||||||
? '紧急'
|
|
||||||
: forecast.daysUntilThreshold <= 14
|
|
||||||
? '警告'
|
|
||||||
: '正常'}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-4">
|
|
||||||
<div className="flex items-center justify-between text-sm mb-1">
|
|
||||||
<span className="text-slate-400">7天使用率预测</span>
|
|
||||||
<span className="text-slate-400">
|
|
||||||
{forecast.daysUntilThreshold >= 999 ? (
|
|
||||||
'增长平缓,暂无触顶风险'
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
预计{' '}
|
|
||||||
<span className="text-amber-400 font-medium">{forecast.daysUntilThreshold}天</span>{' '}
|
|
||||||
后达到阈值
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="h-3 bg-slate-700 rounded-full overflow-hidden">
|
|
||||||
<div className="flex h-full">
|
|
||||||
<div className="bg-blue-500" style={{ width: `${forecast.currentUsage}%` }} />
|
|
||||||
<div
|
|
||||||
className="bg-gradient-to-r from-blue-500/50 to-amber-500"
|
|
||||||
style={{
|
|
||||||
width: `${Math.max(0, forecast.predictedUsage - forecast.currentUsage)}%`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between text-xs mt-1">
|
|
||||||
<span className="text-slate-500">当前 {forecast.currentUsage}%</span>
|
|
||||||
<span className="text-amber-400">预测 {forecast.predictedUsage}%</span>
|
|
||||||
<span className="text-red-400">阈值 {forecast.threshold}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-3 bg-slate-900 rounded-lg border border-slate-700">
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<Zap className="h-4 w-4 text-emerald-400 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<span className="text-sm text-slate-400">AI建议:</span>
|
|
||||||
<p className="text-sm text-slate-300 mt-1">{forecast.recommendation}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="anomaly" className="space-y-4">
|
<TabsContent value="anomaly" className="space-y-4">
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { HardDrive, Database, Zap, Loader2 } from 'lucide-react';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import type { CapacityForecast } from './AIPredictiveAnalytics';
|
||||||
|
|
||||||
|
export function PredictiveCapacityTab({
|
||||||
|
forecasts,
|
||||||
|
loading,
|
||||||
|
}: {
|
||||||
|
forecasts: CapacityForecast[];
|
||||||
|
loading: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card className="bg-slate-800 border-slate-700">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-slate-100 flex items-center gap-2">
|
||||||
|
<HardDrive className="h-5 w-5 text-amber-400" />
|
||||||
|
容量规划预警
|
||||||
|
</CardTitle>
|
||||||
|
<span className="text-xs text-slate-500">基于日均增长率推算阈值触及时间</span>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center py-12 text-slate-500">
|
||||||
|
<Loader2 className="h-5 w-5 animate-spin mr-2" /> 加载容量数据...
|
||||||
|
</div>
|
||||||
|
) : forecasts.length === 0 ? (
|
||||||
|
<div className="text-center py-12 text-slate-500">暂无容量数据</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{forecasts.map((forecast, idx) => (
|
||||||
|
<div key={idx} className="p-4 rounded-lg border border-slate-700 bg-slate-800/50">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Database className="h-5 w-5 text-blue-400" />
|
||||||
|
<span className="font-medium text-slate-200">{forecast.resource}</span>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={
|
||||||
|
forecast.daysUntilThreshold <= 7
|
||||||
|
? 'border-red-500/30 text-red-400 bg-red-500/10'
|
||||||
|
: forecast.daysUntilThreshold <= 14
|
||||||
|
? 'border-amber-500/30 text-amber-400 bg-amber-500/10'
|
||||||
|
: 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{forecast.daysUntilThreshold <= 7
|
||||||
|
? '紧急'
|
||||||
|
: forecast.daysUntilThreshold <= 14
|
||||||
|
? '警告'
|
||||||
|
: '正常'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-center justify-between text-sm mb-1">
|
||||||
|
<span className="text-slate-400">7天使用率预测</span>
|
||||||
|
<span className="text-slate-400">
|
||||||
|
{forecast.daysUntilThreshold >= 999 ? (
|
||||||
|
'增长平缓,暂无触顶风险'
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
预计{' '}
|
||||||
|
<span className="text-amber-400 font-medium">{forecast.daysUntilThreshold}天</span>{' '}
|
||||||
|
后达到阈值
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-3 bg-slate-700 rounded-full overflow-hidden">
|
||||||
|
<div className="flex h-full">
|
||||||
|
<div className="bg-blue-500" style={{ width: `${forecast.currentUsage}%` }} />
|
||||||
|
<div
|
||||||
|
className="bg-gradient-to-r from-blue-500/50 to-amber-500"
|
||||||
|
style={{
|
||||||
|
width: `${Math.max(0, forecast.predictedUsage - forecast.currentUsage)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-xs mt-1">
|
||||||
|
<span className="text-slate-500">当前 {forecast.currentUsage}%</span>
|
||||||
|
<span className="text-amber-400">预测 {forecast.predictedUsage}%</span>
|
||||||
|
<span className="text-red-400">阈值 {forecast.threshold}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-slate-900 rounded-lg border border-slate-700">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<Zap className="h-4 w-4 text-emerald-400 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-slate-400">AI建议:</span>
|
||||||
|
<p className="text-sm text-slate-300 mt-1">{forecast.recommendation}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||||
import { billingApi } from '@/services/api';
|
import { billingApi } from '@/services/api';
|
||||||
|
import { formatCurrency, StatCard, TrendChart } from './CloudBillingHelpers';
|
||||||
|
|
||||||
const providerLabels: Record<string, string> = {
|
const providerLabels: Record<string, string> = {
|
||||||
aliyun: '阿里云',
|
aliyun: '阿里云',
|
||||||
@@ -53,10 +54,6 @@ const providerColors: Record<string, string> = {
|
|||||||
azure: 'bg-sky-500',
|
azure: 'bg-sky-500',
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatCurrency(n: number) {
|
|
||||||
return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CloudBilling() {
|
export function CloudBilling() {
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
|
|
||||||
@@ -664,65 +661,3 @@ export function CloudBilling() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatCard({ icon, label, value, trend, trendLabel, alert }: {
|
|
||||||
icon: React.ReactNode;
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
trend?: number;
|
|
||||||
trendLabel?: string;
|
|
||||||
alert?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className={`rounded-lg border p-4 ${alert ? 'border-amber-500/30 bg-amber-500/5' : 'border-slate-800 bg-slate-900/50'}`}>
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<span className="text-slate-400 text-sm">{label}</span>
|
|
||||||
<span className={alert ? 'text-amber-400' : 'text-slate-500'}>{icon}</span>
|
|
||||||
</div>
|
|
||||||
<div className={`text-2xl font-bold ${alert ? 'text-amber-300' : 'text-slate-100'}`}>{value}</div>
|
|
||||||
{trendLabel && (
|
|
||||||
<div className="flex items-center gap-1 mt-1 text-xs">
|
|
||||||
{trend !== undefined && trend > 0 && <TrendingUp className="h-3 w-3 text-red-400" />}
|
|
||||||
{trend !== undefined && trend < 0 && <TrendingDown className="h-3 w-3 text-green-400" />}
|
|
||||||
<span className="text-slate-500">{trendLabel}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TrendChart({ data }: { data: { date: string; cost: number }[] }) {
|
|
||||||
if (data.length === 0) {
|
|
||||||
return <p className="text-sm text-slate-500 py-8 text-center">暂无数据</p>;
|
|
||||||
}
|
|
||||||
const max = Math.max(...data.map((d) => d.cost), 1);
|
|
||||||
const recent = data.slice(-30);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex items-end gap-0.5 h-40">
|
|
||||||
{recent.map((d, i) => {
|
|
||||||
const h = Math.max((d.cost / max) * 100, 2);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="flex-1 group relative"
|
|
||||||
style={{ height: '100%' }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="absolute bottom-0 w-full rounded-t-sm bg-gradient-to-t from-blue-600/60 to-blue-400/80 group-hover:from-blue-500 group-hover:to-blue-300 transition-colors"
|
|
||||||
style={{ height: `${h}%` }}
|
|
||||||
/>
|
|
||||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover:block whitespace-nowrap text-xs bg-slate-800 text-slate-200 px-2 py-1 rounded border border-slate-700 z-10">
|
|
||||||
{d.date}: {formatCurrency(d.cost)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between mt-2 text-xs text-slate-500">
|
|
||||||
<span>{recent[0]?.date}</span>
|
|
||||||
<span>{recent[recent.length - 1]?.date}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { type ReactNode } from 'react';
|
||||||
|
import { TrendingUp, TrendingDown } from 'lucide-react';
|
||||||
|
|
||||||
|
export function formatCurrency(n: number) {
|
||||||
|
return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatCard({ icon, label, value, trend, trendLabel, alert }: {
|
||||||
|
icon: ReactNode;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
trend?: number;
|
||||||
|
trendLabel?: string;
|
||||||
|
alert?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={`rounded-lg border p-4 ${alert ? 'border-amber-500/30 bg-amber-500/5' : 'border-slate-800 bg-slate-900/50'}`}>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-slate-400 text-sm">{label}</span>
|
||||||
|
<span className={alert ? 'text-amber-400' : 'text-slate-500'}>{icon}</span>
|
||||||
|
</div>
|
||||||
|
<div className={`text-2xl font-bold ${alert ? 'text-amber-300' : 'text-slate-100'}`}>{value}</div>
|
||||||
|
{trendLabel && (
|
||||||
|
<div className="flex items-center gap-1 mt-1 text-xs">
|
||||||
|
{trend !== undefined && trend > 0 && <TrendingUp className="h-3 w-3 text-red-400" />}
|
||||||
|
{trend !== undefined && trend < 0 && <TrendingDown className="h-3 w-3 text-green-400" />}
|
||||||
|
<span className="text-slate-500">{trendLabel}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TrendChart({ data }: { data: { date: string; cost: number }[] }) {
|
||||||
|
if (data.length === 0) {
|
||||||
|
return <p className="text-sm text-slate-500 py-8 text-center">暂无数据</p>;
|
||||||
|
}
|
||||||
|
const max = Math.max(...data.map((d) => d.cost), 1);
|
||||||
|
const recent = data.slice(-30);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-end gap-0.5 h-40">
|
||||||
|
{recent.map((d, i) => {
|
||||||
|
const h = Math.max((d.cost / max) * 100, 2);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="flex-1 group relative"
|
||||||
|
style={{ height: '100%' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute bottom-0 w-full rounded-t-sm bg-gradient-to-t from-blue-600/60 to-blue-400/80 group-hover:from-blue-500 group-hover:to-blue-300 transition-colors"
|
||||||
|
style={{ height: `${h}%` }}
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover:block whitespace-nowrap text-xs bg-slate-800 text-slate-200 px-2 py-1 rounded border border-slate-700 z-10">
|
||||||
|
{d.date}: {formatCurrency(d.cost)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between mt-2 text-xs text-slate-500">
|
||||||
|
<span>{recent[0]?.date}</span>
|
||||||
|
<span>{recent[recent.length - 1]?.date}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -24,8 +24,9 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||||
import { alertCenterApi, alertApi } from '@/services/api';
|
import { alertCenterApi, alertApi } from '@/services/api';
|
||||||
|
import { AlertDetailDialog, AlertSilenceDialog } from './AlertCenterDetail';
|
||||||
|
|
||||||
interface Alert {
|
export interface Alert {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -102,7 +103,7 @@ const AlertCenter: React.FC = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await alertCenterApi.getAlerts();
|
const res = await alertCenterApi.getAlerts();
|
||||||
const data = res.data || [];
|
const data = res.list || [];
|
||||||
setAlerts(data);
|
setAlerts(data);
|
||||||
setStats({
|
setStats({
|
||||||
firing: data.filter((a: Alert) => a.status === 'firing').length,
|
firing: data.filter((a: Alert) => a.status === 'firing').length,
|
||||||
@@ -169,7 +170,7 @@ const AlertCenter: React.FC = () => {
|
|||||||
|
|
||||||
const toggleRule = async (ruleId: string, enabled: boolean) => {
|
const toggleRule = async (ruleId: string, enabled: boolean) => {
|
||||||
try {
|
try {
|
||||||
await alertCenterApi.toggleRule(ruleId, enabled);
|
await alertCenterApi.toggleRule(ruleId);
|
||||||
setRules(rules.map(r => r.id === ruleId ? { ...r, enabled } : r));
|
setRules(rules.map(r => r.id === ruleId ? { ...r, enabled } : r));
|
||||||
toast.success('规则状态已更新');
|
toast.success('规则状态已更新');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -616,101 +617,18 @@ const AlertCenter: React.FC = () => {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{/* 告警详情对话框 */}
|
<AlertDetailDialog
|
||||||
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
alert={selectedAlert}
|
||||||
<DialogContent className="max-w-3xl">
|
open={detailOpen}
|
||||||
<DialogHeader>
|
onOpenChange={setDetailOpen}
|
||||||
<DialogTitle className="flex items-center gap-2">
|
onAcknowledge={handleAcknowledge}
|
||||||
{selectedAlert?.title}
|
/>
|
||||||
{selectedAlert && getSeverityBadge(selectedAlert.severity)}
|
<AlertSilenceDialog
|
||||||
</DialogTitle>
|
alert={selectedAlert}
|
||||||
</DialogHeader>
|
open={silenceOpen}
|
||||||
{selectedAlert && (
|
onOpenChange={setSilenceOpen}
|
||||||
<div className="space-y-4">
|
onSilence={handleSilence}
|
||||||
<div>
|
/>
|
||||||
<Label className="text-muted-foreground">描述</Label>
|
|
||||||
<p className="mt-1">{selectedAlert.description}</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground">来源</Label>
|
|
||||||
<p className="font-medium">{selectedAlert.source}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground">服务</Label>
|
|
||||||
<p className="font-medium">{selectedAlert.service}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground">指标</Label>
|
|
||||||
<p className="font-medium">{selectedAlert.metric}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground">当前值 / 阈值</Label>
|
|
||||||
<p className="font-medium">{selectedAlert.value} / {selectedAlert.threshold}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground">开始时间</Label>
|
|
||||||
<p className="font-medium">{selectedAlert.startedAt}</p>
|
|
||||||
</div>
|
|
||||||
{selectedAlert.resolvedAt && (
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground">解决时间</Label>
|
|
||||||
<p className="font-medium">{selectedAlert.resolvedAt}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground">标签</Label>
|
|
||||||
<div className="flex flex-wrap gap-2 mt-1">
|
|
||||||
{Object.entries(selectedAlert.labels).map(([k, v]) => (
|
|
||||||
<Badge key={k} variant="outline">{k}={v}</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{selectedAlert.runbookUrl && (
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground">处理手册</Label>
|
|
||||||
<a href={selectedAlert.runbookUrl} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline block mt-1">
|
|
||||||
查看处理手册 →
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setDetailOpen(false)}>关闭</Button>
|
|
||||||
{selectedAlert?.status === 'firing' && (
|
|
||||||
<Button onClick={() => { handleAcknowledge(selectedAlert.id); setDetailOpen(false); }}>
|
|
||||||
<Check className="w-4 h-4 mr-2" />
|
|
||||||
确认告警
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* 静默对话框 */}
|
|
||||||
<Dialog open={silenceOpen} onOpenChange={setSilenceOpen}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>静默告警</DialogTitle>
|
|
||||||
<DialogDescription>选择静默持续时间</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{['1h', '4h', '8h', '24h', '7d'].map((duration) => (
|
|
||||||
<Button
|
|
||||||
key={duration}
|
|
||||||
variant="outline"
|
|
||||||
className="w-full justify-between"
|
|
||||||
onClick={() => selectedAlert && handleSilence(selectedAlert.id, duration)}
|
|
||||||
>
|
|
||||||
<span>静默 {duration}</span>
|
|
||||||
<ChevronRight className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* 通知渠道创建/编辑对话框 */}
|
{/* 通知渠道创建/编辑对话框 */}
|
||||||
<Dialog open={channelDialogOpen} onOpenChange={setChannelDialogOpen}>
|
<Dialog open={channelDialogOpen} onOpenChange={setChannelDialogOpen}>
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import {
|
||||||
|
Check, ChevronRight, AlertCircle, AlertTriangle, Info,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import {
|
||||||
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import type { Alert } from './AlertCenter';
|
||||||
|
|
||||||
|
function getSeverityBadge(severity: string) {
|
||||||
|
switch (severity) {
|
||||||
|
case 'critical':
|
||||||
|
return <Badge variant="destructive" className="gap-1"><AlertCircle className="w-3 h-3" />严重</Badge>;
|
||||||
|
case 'warning':
|
||||||
|
return <Badge variant="default" className="bg-orange-500 gap-1"><AlertTriangle className="w-3 h-3" />警告</Badge>;
|
||||||
|
case 'info':
|
||||||
|
return <Badge variant="default" className="bg-blue-500 gap-1"><Info className="w-3 h-3" />信息</Badge>;
|
||||||
|
default:
|
||||||
|
return <Badge variant="secondary">未知</Badge>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AlertDetailDialog({
|
||||||
|
alert,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onAcknowledge,
|
||||||
|
}: {
|
||||||
|
alert: Alert | null;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onAcknowledge: (id: string) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-3xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
{alert?.title}
|
||||||
|
{alert && getSeverityBadge(alert.severity)}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
{alert && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-muted-foreground">描述</Label>
|
||||||
|
<p className="mt-1">{alert.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-muted-foreground">来源</Label>
|
||||||
|
<p className="font-medium">{alert.source}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-muted-foreground">服务</Label>
|
||||||
|
<p className="font-medium">{alert.service}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-muted-foreground">指标</Label>
|
||||||
|
<p className="font-medium">{alert.metric}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-muted-foreground">当前值 / 阈值</Label>
|
||||||
|
<p className="font-medium">{alert.value} / {alert.threshold}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-muted-foreground">开始时间</Label>
|
||||||
|
<p className="font-medium">{alert.startedAt}</p>
|
||||||
|
</div>
|
||||||
|
{alert.resolvedAt && (
|
||||||
|
<div>
|
||||||
|
<Label className="text-muted-foreground">解决时间</Label>
|
||||||
|
<p className="font-medium">{alert.resolvedAt}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-muted-foreground">标签</Label>
|
||||||
|
<div className="flex flex-wrap gap-2 mt-1">
|
||||||
|
{Object.entries(alert.labels).map(([k, v]) => (
|
||||||
|
<Badge key={k} variant="outline">{k}={v}</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{alert.runbookUrl && (
|
||||||
|
<div>
|
||||||
|
<Label className="text-muted-foreground">处理手册</Label>
|
||||||
|
<a href={alert.runbookUrl} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline block mt-1">
|
||||||
|
查看处理手册 →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>关闭</Button>
|
||||||
|
{alert?.status === 'firing' && (
|
||||||
|
<Button onClick={() => { onAcknowledge(alert.id); onOpenChange(false); }}>
|
||||||
|
<Check className="w-4 h-4 mr-2" />
|
||||||
|
确认告警
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AlertSilenceDialog({
|
||||||
|
alert,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSilence,
|
||||||
|
}: {
|
||||||
|
alert: Alert | null;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSilence: (id: string, duration: string) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>静默告警</DialogTitle>
|
||||||
|
<DialogDescription>选择静默持续时间</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{['1h', '4h', '8h', '24h', '7d'].map((duration) => (
|
||||||
|
<Button
|
||||||
|
key={duration}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-between"
|
||||||
|
onClick={() => alert && onSilence(alert.id, duration)}
|
||||||
|
>
|
||||||
|
<span>静默 {duration}</span>
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import { Separator } from '@/components/ui/separator';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||||
import { dashboardApi } from '@/services/api';
|
import { dashboardApi } from '@/services/api';
|
||||||
|
import { GrafanaPanelContent } from './GrafanaPanelContent';
|
||||||
|
|
||||||
interface Dashboard {
|
interface Dashboard {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -36,7 +37,7 @@ interface Dashboard {
|
|||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Panel {
|
export interface Panel {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
type: 'graph' | 'stat' | 'gauge' | 'table' | 'pie' | 'heatmap' | 'text';
|
type: 'graph' | 'stat' | 'gauge' | 'table' | 'pie' | 'heatmap' | 'text';
|
||||||
@@ -250,81 +251,6 @@ const GrafanaDashboard: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 模拟图表数据
|
|
||||||
const generateChartData = (panel: Panel) => {
|
|
||||||
const dataPoints = 24;
|
|
||||||
const data = [];
|
|
||||||
for (let i = 0; i < dataPoints; i++) {
|
|
||||||
data.push({
|
|
||||||
time: new Date(Date.now() - (dataPoints - i) * 3600000).toLocaleTimeString(),
|
|
||||||
value: Math.random() * 100
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 渲染面板内容
|
|
||||||
const renderPanelContent = (panel: Panel) => {
|
|
||||||
const data = generateChartData(panel);
|
|
||||||
|
|
||||||
switch (panel.type) {
|
|
||||||
case 'stat':
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center h-full">
|
|
||||||
<span className="text-4xl font-bold text-blue-500">{Math.floor(Math.random() * 1000)}</span>
|
|
||||||
<span className="text-sm text-muted-foreground mt-2">{panel.options?.unit || 'requests/s'}</span>
|
|
||||||
<div className="flex items-center gap-1 mt-2 text-green-500 text-sm">
|
|
||||||
<TrendingUp className="w-4 h-4" />
|
|
||||||
<span>+12.5%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case 'gauge':
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center h-full">
|
|
||||||
<div className="relative w-24 h-24">
|
|
||||||
<svg className="w-full h-full" viewBox="0 0 100 100">
|
|
||||||
<circle cx="50" cy="50" r="40" fill="none" stroke="#e5e7eb" strokeWidth="10" />
|
|
||||||
<circle
|
|
||||||
cx="50"
|
|
||||||
cy="50"
|
|
||||||
r="40"
|
|
||||||
fill="none"
|
|
||||||
stroke="#3b82f6"
|
|
||||||
strokeWidth="10"
|
|
||||||
strokeDasharray={`${Math.random() * 251} 251`}
|
|
||||||
transform="rotate(-90 50 50)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<span className="text-xl font-bold">{Math.floor(Math.random() * 100)}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-muted-foreground mt-2">CPU使用率</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case 'graph':
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<div className="h-full flex flex-col">
|
|
||||||
<div className="flex-1 flex items-end gap-1 px-2">
|
|
||||||
{data.map((d, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="flex-1 bg-blue-500 rounded-t"
|
|
||||||
style={{ height: `${d.value}%`, opacity: 0.3 + (i / data.length) * 0.7 }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-xs text-muted-foreground px-2 pt-2">
|
|
||||||
<span>{data[0].time}</span>
|
|
||||||
<span>{data[data.length - 1].time}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`space-y-4 ${fullscreen ? 'fixed inset-0 z-50 bg-background p-4' : ''}`}>
|
<div className={`space-y-4 ${fullscreen ? 'fixed inset-0 z-50 bg-background p-4' : ''}`}>
|
||||||
{/* 顶部工具栏 */}
|
{/* 顶部工具栏 */}
|
||||||
@@ -448,7 +374,7 @@ const GrafanaDashboard: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-4 pt-0 flex-1">
|
<CardContent className="p-4 pt-0 flex-1">
|
||||||
{renderPanelContent(panel)}
|
<GrafanaPanelContent panel={panel} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { TrendingUp } from 'lucide-react';
|
||||||
|
import type { Panel } from './GrafanaDashboard';
|
||||||
|
|
||||||
|
/** 模拟图表数据 */
|
||||||
|
function generateChartData(panel: Panel) {
|
||||||
|
const dataPoints = 24;
|
||||||
|
const data = [];
|
||||||
|
for (let i = 0; i < dataPoints; i++) {
|
||||||
|
data.push({
|
||||||
|
time: new Date(Date.now() - (dataPoints - i) * 3600000).toLocaleTimeString(),
|
||||||
|
value: Math.random() * 100
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 渲染面板内容 */
|
||||||
|
export function GrafanaPanelContent({ panel }: { panel: Panel }) {
|
||||||
|
const data = generateChartData(panel);
|
||||||
|
|
||||||
|
switch (panel.type) {
|
||||||
|
case 'stat':
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full">
|
||||||
|
<span className="text-4xl font-bold text-blue-500">{Math.floor(Math.random() * 1000)}</span>
|
||||||
|
<span className="text-sm text-muted-foreground mt-2">{panel.options?.unit || 'requests/s'}</span>
|
||||||
|
<div className="flex items-center gap-1 mt-2 text-green-500 text-sm">
|
||||||
|
<TrendingUp className="w-4 h-4" />
|
||||||
|
<span>+12.5%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'gauge':
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full">
|
||||||
|
<div className="relative w-24 h-24">
|
||||||
|
<svg className="w-full h-full" viewBox="0 0 100 100">
|
||||||
|
<circle cx="50" cy="50" r="40" fill="none" stroke="#e5e7eb" strokeWidth="10" />
|
||||||
|
<circle
|
||||||
|
cx="50"
|
||||||
|
cy="50"
|
||||||
|
r="40"
|
||||||
|
fill="none"
|
||||||
|
stroke="#3b82f6"
|
||||||
|
strokeWidth="10"
|
||||||
|
strokeDasharray={`${Math.random() * 251} 251`}
|
||||||
|
transform="rotate(-90 50 50)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<span className="text-xl font-bold">{Math.floor(Math.random() * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-muted-foreground mt-2">CPU使用率</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'graph':
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
<div className="flex-1 flex items-end gap-1 px-2">
|
||||||
|
{data.map((d, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="flex-1 bg-blue-500 rounded-t"
|
||||||
|
style={{ height: `${d.value}%`, opacity: 0.3 + (i / data.length) * 0.7 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-xs text-muted-foreground px-2 pt-2">
|
||||||
|
<span>{data[0].time}</span>
|
||||||
|
<span>{data[data.length - 1].time}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,462 @@
|
|||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import {
|
||||||
|
ChevronDown,
|
||||||
|
Users,
|
||||||
|
Key,
|
||||||
|
RefreshCw,
|
||||||
|
Plus,
|
||||||
|
UserPlus,
|
||||||
|
Download,
|
||||||
|
Edit,
|
||||||
|
Trash2,
|
||||||
|
Loader2,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface LdapManagementSectionProps {
|
||||||
|
ldapConfig: any;
|
||||||
|
ldapMgmtOpen: boolean;
|
||||||
|
setLdapMgmtOpen: (v: boolean) => void;
|
||||||
|
ldapOUs: any[];
|
||||||
|
selectedOUDN: string;
|
||||||
|
setSelectedOUDN: (v: string) => void;
|
||||||
|
ldapUsers: any[];
|
||||||
|
selectedLDAPUsers: string[];
|
||||||
|
setSelectedLDAPUsers: (v: string[]) => void;
|
||||||
|
loadLdapOUs: () => void;
|
||||||
|
loadLdapOUUsers: (ouDn?: string) => void;
|
||||||
|
handleLdapImport: () => void;
|
||||||
|
importing: boolean;
|
||||||
|
importFeedback: string | null;
|
||||||
|
importResultOpen: boolean;
|
||||||
|
setImportResultOpen: (v: boolean) => void;
|
||||||
|
// Create OU
|
||||||
|
createOUOpen: boolean;
|
||||||
|
setCreateOUOpen: (v: boolean) => void;
|
||||||
|
ouForm: { ou_name: string; parent_dn: string; description: string };
|
||||||
|
setOuForm: React.Dispatch<React.SetStateAction<{ ou_name: string; parent_dn: string; description: string }>>;
|
||||||
|
handleCreateOU: () => void;
|
||||||
|
ldapSaving: boolean;
|
||||||
|
// Create LDAP user
|
||||||
|
createUserOpen: boolean;
|
||||||
|
setCreateUserOpen: (v: boolean) => void;
|
||||||
|
newUser: { username: string; password: string; display_name: string; email: string; ou_dn: string };
|
||||||
|
setNewUser: React.Dispatch<React.SetStateAction<{ username: string; password: string; display_name: string; email: string; ou_dn: string }>>;
|
||||||
|
handleCreateLDAPUser: () => void;
|
||||||
|
// LDAP user test
|
||||||
|
ldapUserTestOpen: boolean;
|
||||||
|
setLdapUserTestOpen: (v: boolean) => void;
|
||||||
|
ldapTestUser: string;
|
||||||
|
setLdapTestUser: (v: string) => void;
|
||||||
|
ldapTestPass: string;
|
||||||
|
setLdapTestPass: (v: string) => void;
|
||||||
|
ldapUserTesting: boolean;
|
||||||
|
ldapUserTestResult: any;
|
||||||
|
handleLdapUserTest: () => void;
|
||||||
|
// Edit LDAP user
|
||||||
|
editLdapUser: any | null;
|
||||||
|
setEditLdapUser: (u: any | null) => void;
|
||||||
|
editLdapForm: { cn: string; mail: string; phone: string; title: string };
|
||||||
|
setEditLdapForm: React.Dispatch<React.SetStateAction<{ cn: string; mail: string; phone: string; title: string }>>;
|
||||||
|
handleUpdateLdapUser: () => void;
|
||||||
|
handleDeleteLDAPEntry: (dn: string, silent?: boolean) => void;
|
||||||
|
openEditLdapUser: (user: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||||
|
const {
|
||||||
|
ldapConfig,
|
||||||
|
ldapMgmtOpen,
|
||||||
|
setLdapMgmtOpen,
|
||||||
|
ldapOUs,
|
||||||
|
selectedOUDN,
|
||||||
|
setSelectedOUDN,
|
||||||
|
ldapUsers,
|
||||||
|
selectedLDAPUsers,
|
||||||
|
setSelectedLDAPUsers,
|
||||||
|
loadLdapOUs,
|
||||||
|
loadLdapOUUsers,
|
||||||
|
handleLdapImport,
|
||||||
|
importing,
|
||||||
|
importFeedback,
|
||||||
|
importResultOpen,
|
||||||
|
setImportResultOpen,
|
||||||
|
createOUOpen,
|
||||||
|
setCreateOUOpen,
|
||||||
|
ouForm,
|
||||||
|
setOuForm,
|
||||||
|
handleCreateOU,
|
||||||
|
ldapSaving,
|
||||||
|
createUserOpen,
|
||||||
|
setCreateUserOpen,
|
||||||
|
newUser,
|
||||||
|
setNewUser,
|
||||||
|
handleCreateLDAPUser,
|
||||||
|
ldapUserTestOpen,
|
||||||
|
setLdapUserTestOpen,
|
||||||
|
ldapTestUser,
|
||||||
|
setLdapTestUser,
|
||||||
|
ldapTestPass,
|
||||||
|
setLdapTestPass,
|
||||||
|
ldapUserTesting,
|
||||||
|
ldapUserTestResult,
|
||||||
|
handleLdapUserTest,
|
||||||
|
editLdapUser,
|
||||||
|
setEditLdapUser,
|
||||||
|
editLdapForm,
|
||||||
|
setEditLdapForm,
|
||||||
|
handleUpdateLdapUser,
|
||||||
|
handleDeleteLDAPEntry,
|
||||||
|
openEditLdapUser,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* 折叠/展开按钮 */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const open = !ldapMgmtOpen;
|
||||||
|
setLdapMgmtOpen(open);
|
||||||
|
if (open && ldapOUs.length === 0) loadLdapOUs();
|
||||||
|
}}
|
||||||
|
className="w-full flex items-center gap-2 px-4 py-2.5 bg-slate-900/50 hover:bg-slate-800/50 text-slate-400 text-sm transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronDown className={`h-4 w-4 transition-transform ${ldapMgmtOpen ? '' : '-rotate-90'}`} />
|
||||||
|
<Users className="h-4 w-4 text-amber-400" />
|
||||||
|
<span className="text-slate-300">LDAP 账号管理</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{ldapMgmtOpen && (
|
||||||
|
<div className="bg-slate-900/30 border-t border-slate-800/50 px-4 py-4 space-y-4">
|
||||||
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
|
{/* 账号验证测试 */}
|
||||||
|
<Dialog open={ldapUserTestOpen} onOpenChange={setLdapUserTestOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-200">
|
||||||
|
<Key className="h-3.5 w-3.5 mr-1" />
|
||||||
|
账号验证
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-md bg-slate-900 border-slate-800">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-slate-100">LDAP 账号验证测试</DialogTitle>
|
||||||
|
<DialogDescription className="text-slate-500">输入 LDAP 用户凭据进行认证测试</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-3 py-2">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-500 mb-1 block">用户名</label>
|
||||||
|
<Input
|
||||||
|
value={ldapTestUser}
|
||||||
|
onChange={(e) => setLdapTestUser(e.target.value)}
|
||||||
|
placeholder="zhangsan"
|
||||||
|
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-500 mb-1 block">密码</label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={ldapTestPass}
|
||||||
|
onChange={(e) => setLdapTestPass(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleLdapUserTest()}
|
||||||
|
placeholder="••••••"
|
||||||
|
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{ldapUserTestResult && (
|
||||||
|
<div className={`p-3 rounded-lg text-sm ${ldapUserTestResult.success ? 'bg-emerald-500/10 text-emerald-300' : 'bg-red-500/10 text-red-300'}`}>
|
||||||
|
{ldapUserTestResult.message}
|
||||||
|
{ldapUserTestResult.user_info && (
|
||||||
|
<pre className="mt-2 text-xs whitespace-pre-wrap">{JSON.stringify(ldapUserTestResult.user_info, null, 2)}</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
|
onClick={handleLdapUserTest}
|
||||||
|
disabled={ldapUserTesting || !ldapTestUser}
|
||||||
|
>
|
||||||
|
{ldapUserTesting ? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" /> : null}
|
||||||
|
验证
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-200" onClick={loadLdapOUs}>
|
||||||
|
<RefreshCw className="h-3.5 w-3.5 mr-1" />
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* OU 选择 */}
|
||||||
|
<Select value={selectedOUDN} onValueChange={(v) => { setSelectedOUDN(v); loadLdapOUUsers(v); }}>
|
||||||
|
<SelectTrigger className="w-60 h-7 bg-slate-800 border-slate-700 text-slate-200 text-xs">
|
||||||
|
<SelectValue placeholder="选择组织单元" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-slate-800 border-slate-700">
|
||||||
|
<SelectItem value={ldapConfig.base_dn}>根 ({ldapConfig.base_dn})</SelectItem>
|
||||||
|
{ldapOUs.map((ou: any) => (
|
||||||
|
<SelectItem key={ou.dn} value={ou.dn}>
|
||||||
|
{ou.description ? `${ou.name} (${ou.description})` : ou.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{/* 新建 OU */}
|
||||||
|
<Dialog open={createOUOpen} onOpenChange={setCreateOUOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-200">
|
||||||
|
<Plus className="h-3.5 w-3.5 mr-1" />
|
||||||
|
新建 OU
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-md bg-slate-900 border-slate-800">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-slate-100">新建组织单元</DialogTitle>
|
||||||
|
<DialogDescription className="text-slate-500">在 LDAP 目录中创建新的组织单元 (OU)</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-3 py-2">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-500 mb-1 block">OU 名称 *</label>
|
||||||
|
<Input value={ouForm.ou_name} onChange={(e) => setOuForm({ ...ouForm, ou_name: e.target.value })} placeholder="dev" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-500 mb-1 block">父级 DN</label>
|
||||||
|
<Input value={ouForm.parent_dn} onChange={(e) => setOuForm({ ...ouForm, parent_dn: e.target.value })} placeholder={ldapConfig.base_dn} className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-500 mb-1 block">描述</label>
|
||||||
|
<Input value={ouForm.description} onChange={(e) => setOuForm({ ...ouForm, description: e.target.value })} placeholder="开发团队" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||||
|
</div>
|
||||||
|
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={handleCreateOU} disabled={!ouForm.ou_name || ldapSaving}>
|
||||||
|
{ldapSaving ? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" /> : null}
|
||||||
|
创建
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 添加用户 */}
|
||||||
|
<Dialog open={createUserOpen} onOpenChange={setCreateUserOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-200">
|
||||||
|
<UserPlus className="h-3.5 w-3.5 mr-1" />
|
||||||
|
添加用户
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-md bg-slate-900 border-slate-800">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-slate-100">添加 LDAP 用户</DialogTitle>
|
||||||
|
<DialogDescription className="text-slate-500">在 LDAP 目录中创建新的用户账号</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-3 py-2">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-500 mb-1 block">用户名 *</label>
|
||||||
|
<Input value={newUser.username} onChange={(e) => setNewUser({ ...newUser, username: e.target.value })} placeholder="zhangsan" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-500 mb-1 block">密码 *</label>
|
||||||
|
<Input type="password" value={newUser.password} onChange={(e) => setNewUser({ ...newUser, password: e.target.value })} placeholder="••••••" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-500 mb-1 block">显示名称</label>
|
||||||
|
<Input value={newUser.display_name} onChange={(e) => setNewUser({ ...newUser, display_name: e.target.value })} placeholder="张三" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-500 mb-1 block">邮箱</label>
|
||||||
|
<Input value={newUser.email} onChange={(e) => setNewUser({ ...newUser, email: e.target.value })} placeholder="zs@example.com" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-500 mb-1 block">目标 OU</label>
|
||||||
|
<Input value={newUser.ou_dn || selectedOUDN || ldapConfig.base_dn} onChange={(e) => setNewUser({ ...newUser, ou_dn: e.target.value })} className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||||
|
</div>
|
||||||
|
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={handleCreateLDAPUser} disabled={!newUser.username || !newUser.password || ldapSaving}>
|
||||||
|
{ldapSaving ? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" /> : null}
|
||||||
|
创建
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 导入到本地 */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="border-blue-600/50 text-blue-400 hover:bg-blue-600/10 ml-auto"
|
||||||
|
onClick={handleLdapImport}
|
||||||
|
disabled={ldapUsers.length === 0 || importing}
|
||||||
|
>
|
||||||
|
{importing ? <Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" /> : <Download className="h-3.5 w-3.5 mr-1" />}
|
||||||
|
{selectedLDAPUsers.length > 0 ? `导入 (${selectedLDAPUsers.length})` : `导入全部 (${ldapUsers.length})`}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{importFeedback && (
|
||||||
|
<Dialog open={importResultOpen} onOpenChange={setImportResultOpen}>
|
||||||
|
<DialogContent className="max-w-sm bg-slate-900 border-slate-800">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-slate-100">导入结果</DialogTitle>
|
||||||
|
<DialogDescription className="text-slate-500">LDAP 用户导入到本地用户库的执行结果</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="py-3 text-sm text-slate-300">
|
||||||
|
{importFeedback}
|
||||||
|
</div>
|
||||||
|
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={() => setImportResultOpen(false)}>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 用户表格 */}
|
||||||
|
<div className="border border-slate-800 rounded-lg overflow-hidden">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="border-slate-800 hover:bg-transparent">
|
||||||
|
<TableHead className="text-slate-400 w-8">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="rounded bg-slate-800 border-slate-600"
|
||||||
|
checked={selectedLDAPUsers.length > 0 && selectedLDAPUsers.length === ldapUsers.length}
|
||||||
|
onChange={(e) => setSelectedLDAPUsers(e.target.checked ? ldapUsers.map((u: any) => u.dn) : [])}
|
||||||
|
/>
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-slate-400">用户名</TableHead>
|
||||||
|
<TableHead className="text-slate-400">显示名称</TableHead>
|
||||||
|
<TableHead className="text-slate-400">邮箱</TableHead>
|
||||||
|
<TableHead className="text-slate-400">DN</TableHead>
|
||||||
|
<TableHead className="text-slate-400 w-12">操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{ldapUsers.length === 0 ? (
|
||||||
|
<TableRow className="border-slate-800">
|
||||||
|
<TableCell colSpan={6} className="text-center text-slate-500 py-8">
|
||||||
|
点击「刷新」加载 LDAP 用户
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
ldapUsers.map((user: any) => (
|
||||||
|
<TableRow key={user.dn} className="border-slate-800 hover:bg-slate-800/50">
|
||||||
|
<TableCell>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="rounded bg-slate-800 border-slate-600"
|
||||||
|
checked={selectedLDAPUsers.includes(user.dn)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) setSelectedLDAPUsers([...selectedLDAPUsers, user.dn]);
|
||||||
|
else setSelectedLDAPUsers(selectedLDAPUsers.filter((d: string) => d !== user.dn));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-sm text-slate-200 font-medium">{user.uid}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-sm text-slate-300">{user.cn}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-sm text-slate-400">{user.mail || '-'}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<code className="text-xs text-slate-500">{user.dn}</code>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 text-slate-400 hover:text-blue-400 hover:bg-blue-500/10"
|
||||||
|
onClick={() => openEditLdapUser(user)}
|
||||||
|
>
|
||||||
|
<Edit className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||||
|
onClick={() => handleDeleteLDAPEntry(user.dn)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 编辑 LDAP 用户 Dialog */}
|
||||||
|
<Dialog open={!!editLdapUser} onOpenChange={(v) => !v && setEditLdapUser(null)}>
|
||||||
|
<DialogContent className="max-w-md bg-slate-900 border-slate-800">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||||
|
<Edit className="h-5 w-5 text-blue-400" />
|
||||||
|
编辑 LDAP 用户
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-slate-500">
|
||||||
|
修改 {editLdapUser?.uid} 的属性
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{editLdapUser && (
|
||||||
|
<div className="space-y-3 py-2">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-500 mb-1 block">用户名(只读)</label>
|
||||||
|
<Input value={editLdapUser.uid || ''} disabled className="bg-slate-800/50 border-slate-700 text-slate-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-500 mb-1 block">显示名称 (cn)</label>
|
||||||
|
<Input value={editLdapForm.cn} onChange={(e) => setEditLdapForm({ ...editLdapForm, cn: e.target.value })} className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-500 mb-1 block">邮箱</label>
|
||||||
|
<Input value={editLdapForm.mail} onChange={(e) => setEditLdapForm({ ...editLdapForm, mail: e.target.value })} placeholder="user@example.com" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-500 mb-1 block">电话</label>
|
||||||
|
<Input value={editLdapForm.phone} onChange={(e) => setEditLdapForm({ ...editLdapForm, phone: e.target.value })} placeholder="13800138000" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-500 mb-1 block">职位/头衔</label>
|
||||||
|
<Input value={editLdapForm.title} onChange={(e) => setEditLdapForm({ ...editLdapForm, title: e.target.value })} placeholder="运维工程师" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => setEditLdapUser(null)}>取消</Button>
|
||||||
|
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleUpdateLdapUser}>保存</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,8 +1,9 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Plus, Shield, Edit, Eye } from 'lucide-react';
|
import { Plus, Shield, Edit, Eye } from 'lucide-react';
|
||||||
import { roles } from '@/mocks/data';
|
import { userApi } from '@/services/api';
|
||||||
import type { Role } from '@/types';
|
import type { Role } from '@/types';
|
||||||
|
|
||||||
interface SystemRolesTabProps {
|
interface SystemRolesTabProps {
|
||||||
@@ -10,7 +11,43 @@ interface SystemRolesTabProps {
|
|||||||
setSelectedRole: (role: Role | null) => void;
|
setSelectedRole: (role: Role | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将后端角色数据规范化为前端 Role 类型 // Logan.Li
|
||||||
|
function normalizeRole(raw: any): Role {
|
||||||
|
return {
|
||||||
|
id: String(raw.id ?? ''),
|
||||||
|
name: raw.name ?? '',
|
||||||
|
code: raw.code ?? '',
|
||||||
|
description: raw.description ?? '',
|
||||||
|
// 后端可能不返回 permissions/createdAt,给予默认值以避免渲染崩溃
|
||||||
|
permissions: Array.isArray(raw.permissions) ? raw.permissions : [],
|
||||||
|
createdAt: raw.createdAt ?? raw.created_at ?? '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTabProps) {
|
export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTabProps) {
|
||||||
|
const [roles, setRoles] = useState<Role[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const resp = await userApi.getRoles();
|
||||||
|
if (cancelled) return;
|
||||||
|
const list = resp?.list ?? [];
|
||||||
|
setRoles(list.map(normalizeRole));
|
||||||
|
} catch (err) {
|
||||||
|
if (!cancelled) setRoles([]);
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) setLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
@@ -20,6 +57,16 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center py-16">
|
||||||
|
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : roles.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
|
||||||
|
<Shield className="h-10 w-10 mb-3 opacity-40" />
|
||||||
|
<p className="text-sm">暂无角色数据</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
{roles.map((role) => (
|
{roles.map((role) => (
|
||||||
<Card key={role.id} className="bg-slate-900 border-slate-800">
|
<Card key={role.id} className="bg-slate-900 border-slate-800">
|
||||||
@@ -67,6 +114,7 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
|||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { Lock, History, Key, Settings, Network, RefreshCw, Loader2 } from 'lucide-react';
|
||||||
|
import { LdapManagementSection } from './LdapManagementSection';
|
||||||
|
|
||||||
|
export interface LdapFormData {
|
||||||
|
ldap_host: string;
|
||||||
|
ldap_port: number;
|
||||||
|
use_ldaps: boolean;
|
||||||
|
admin_dn: string;
|
||||||
|
admin_password: string;
|
||||||
|
base_dn: string;
|
||||||
|
search_filter: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SystemSettingsTabProps {
|
||||||
|
// LDAP config
|
||||||
|
ldapConfig: any;
|
||||||
|
ldapForm: LdapFormData;
|
||||||
|
setLdapForm: React.Dispatch<React.SetStateAction<LdapFormData>>;
|
||||||
|
ldapSaving: boolean;
|
||||||
|
ldapTesting: boolean;
|
||||||
|
ldapTestResult: string | null;
|
||||||
|
handleLdapSave: () => void;
|
||||||
|
handleLdapTest: () => void;
|
||||||
|
// LDAP management - pass-through to LdapManagementSection
|
||||||
|
ldapMgmtOpen: boolean;
|
||||||
|
setLdapMgmtOpen: (v: boolean) => void;
|
||||||
|
ldapOUs: any[];
|
||||||
|
selectedOUDN: string;
|
||||||
|
setSelectedOUDN: (v: string) => void;
|
||||||
|
ldapUsers: any[];
|
||||||
|
selectedLDAPUsers: string[];
|
||||||
|
setSelectedLDAPUsers: (v: string[]) => void;
|
||||||
|
loadLdapOUs: () => void;
|
||||||
|
loadLdapOUUsers: (ouDn?: string) => void;
|
||||||
|
handleLdapImport: () => void;
|
||||||
|
importing: boolean;
|
||||||
|
importFeedback: string | null;
|
||||||
|
importResultOpen: boolean;
|
||||||
|
setImportResultOpen: (v: boolean) => void;
|
||||||
|
createOUOpen: boolean;
|
||||||
|
setCreateOUOpen: (v: boolean) => void;
|
||||||
|
ouForm: { ou_name: string; parent_dn: string; description: string };
|
||||||
|
setOuForm: React.Dispatch<React.SetStateAction<{ ou_name: string; parent_dn: string; description: string }>>;
|
||||||
|
handleCreateOU: () => void;
|
||||||
|
createUserOpen: boolean;
|
||||||
|
setCreateUserOpen: (v: boolean) => void;
|
||||||
|
newUser: { username: string; password: string; display_name: string; email: string; ou_dn: string };
|
||||||
|
setNewUser: React.Dispatch<React.SetStateAction<{ username: string; password: string; display_name: string; email: string; ou_dn: string }>>;
|
||||||
|
handleCreateLDAPUser: () => void;
|
||||||
|
ldapUserTestOpen: boolean;
|
||||||
|
setLdapUserTestOpen: (v: boolean) => void;
|
||||||
|
ldapTestUser: string;
|
||||||
|
setLdapTestUser: (v: string) => void;
|
||||||
|
ldapTestPass: string;
|
||||||
|
setLdapTestPass: (v: string) => void;
|
||||||
|
ldapUserTesting: boolean;
|
||||||
|
ldapUserTestResult: any;
|
||||||
|
handleLdapUserTest: () => void;
|
||||||
|
editLdapUser: any | null;
|
||||||
|
setEditLdapUser: (u: any | null) => void;
|
||||||
|
editLdapForm: { cn: string; mail: string; phone: string; title: string };
|
||||||
|
setEditLdapForm: React.Dispatch<React.SetStateAction<{ cn: string; mail: string; phone: string; title: string }>>;
|
||||||
|
handleUpdateLdapUser: () => void;
|
||||||
|
handleDeleteLDAPEntry: (dn: string, silent?: boolean) => void;
|
||||||
|
openEditLdapUser: (user: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||||
|
const {
|
||||||
|
ldapConfig,
|
||||||
|
ldapForm,
|
||||||
|
setLdapForm,
|
||||||
|
ldapSaving,
|
||||||
|
ldapTesting,
|
||||||
|
ldapTestResult,
|
||||||
|
handleLdapSave,
|
||||||
|
handleLdapTest,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-slate-100 flex items-center gap-2">
|
||||||
|
<Lock className="h-5 w-5 text-blue-400" />
|
||||||
|
安全设置
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-slate-500">密码策略与登录安全</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-slate-200">强制密码复杂度</p>
|
||||||
|
<p className="text-xs text-slate-500">要求密码包含大小写字母、数字和特殊字符</p>
|
||||||
|
</div>
|
||||||
|
<Switch defaultChecked className="data-[state=checked]:bg-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-slate-200">登录失败锁定</p>
|
||||||
|
<p className="text-xs text-slate-500">连续5次登录失败锁定账户30分钟</p>
|
||||||
|
</div>
|
||||||
|
<Switch defaultChecked className="data-[state=checked]:bg-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-slate-200">双因素认证</p>
|
||||||
|
<p className="text-xs text-slate-500">启用后需要手机验证码登录</p>
|
||||||
|
</div>
|
||||||
|
<Switch className="data-[state=checked]:bg-blue-600" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-slate-100 flex items-center gap-2">
|
||||||
|
<History className="h-5 w-5 text-emerald-400" />
|
||||||
|
审计设置
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-slate-500">操作日志与审计策略</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-slate-200">记录所有操作</p>
|
||||||
|
<p className="text-xs text-slate-500">记录用户的所有操作行为</p>
|
||||||
|
</div>
|
||||||
|
<Switch defaultChecked className="data-[state=checked]:bg-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-slate-200">日志保留时间</p>
|
||||||
|
<p className="text-xs text-slate-500">操作日志保留180天</p>
|
||||||
|
</div>
|
||||||
|
<Select defaultValue="180">
|
||||||
|
<SelectTrigger className="w-32 h-8 bg-slate-800 border-slate-700 text-slate-200">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-slate-800 border-slate-700">
|
||||||
|
<SelectItem value="30">30天</SelectItem>
|
||||||
|
<SelectItem value="90">90天</SelectItem>
|
||||||
|
<SelectItem value="180">180天</SelectItem>
|
||||||
|
<SelectItem value="365">1年</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-slate-100 flex items-center gap-2">
|
||||||
|
<Key className="h-5 w-5 text-amber-400" />
|
||||||
|
会话设置
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-slate-500">会话超时与Token配置</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-slate-200">会话超时时间</p>
|
||||||
|
<p className="text-xs text-slate-500">用户无操作30分钟后自动登出</p>
|
||||||
|
</div>
|
||||||
|
<Select defaultValue="30">
|
||||||
|
<SelectTrigger className="w-32 h-8 bg-slate-800 border-slate-700 text-slate-200">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-slate-800 border-slate-700">
|
||||||
|
<SelectItem value="15">15分钟</SelectItem>
|
||||||
|
<SelectItem value="30">30分钟</SelectItem>
|
||||||
|
<SelectItem value="60">1小时</SelectItem>
|
||||||
|
<SelectItem value="120">2小时</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-slate-200">单点登录</p>
|
||||||
|
<p className="text-xs text-slate-500">同一账号只能在一个设备登录</p>
|
||||||
|
</div>
|
||||||
|
<Switch className="data-[state=checked]:bg-blue-600" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-slate-100 flex items-center gap-2">
|
||||||
|
<Settings className="h-5 w-5 text-purple-400" />
|
||||||
|
系统信息
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-slate-500">版本与系统状态</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-slate-400">系统版本</span>
|
||||||
|
<span className="text-sm text-slate-200">v2.5.1</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-slate-400">运行时间</span>
|
||||||
|
<span className="text-sm text-slate-200">30天 12小时</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* LDAP 配置 */}
|
||||||
|
<Card className="bg-slate-900 border-slate-800 mt-4">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-slate-100 flex items-center gap-2">
|
||||||
|
<Network className="h-5 w-5 text-cyan-400" />
|
||||||
|
LDAP / Active Directory
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-slate-500 mt-1">配置 LDAP 服务器,实现统一身份认证</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs text-slate-500">LDAP 服务器地址</label>
|
||||||
|
<Input
|
||||||
|
value={ldapForm.ldap_host}
|
||||||
|
onChange={(e) => setLdapForm({ ...ldapForm, ldap_host: e.target.value })}
|
||||||
|
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||||
|
placeholder="ldap.example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs text-slate-500">端口</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={ldapForm.ldap_port}
|
||||||
|
onChange={(e) => setLdapForm({ ...ldapForm, ldap_port: Number(e.target.value) })}
|
||||||
|
className="bg-slate-800/50 border-slate-700 text-slate-200"
|
||||||
|
placeholder="389"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5 col-span-2">
|
||||||
|
<label className="text-xs text-slate-500">Base DN(用户搜索基础)</label>
|
||||||
|
<Input
|
||||||
|
value={ldapForm.base_dn}
|
||||||
|
onChange={(e) => setLdapForm({ ...ldapForm, base_dn: e.target.value })}
|
||||||
|
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||||
|
placeholder="ou=users,dc=example,dc=com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs text-slate-500">管理员 Bind DN</label>
|
||||||
|
<Input
|
||||||
|
value={ldapForm.admin_dn}
|
||||||
|
onChange={(e) => setLdapForm({ ...ldapForm, admin_dn: e.target.value })}
|
||||||
|
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||||
|
placeholder="cn=admin,dc=example,dc=com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs text-slate-500">管理员密码 {ldapConfig && '(留空不修改)'}</label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={ldapForm.admin_password}
|
||||||
|
onChange={(e) => setLdapForm({ ...ldapForm, admin_password: e.target.value })}
|
||||||
|
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||||
|
placeholder="******"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5 col-span-2">
|
||||||
|
<label className="text-xs text-slate-500">搜索过滤器(可选,用 %s 代表用户名)</label>
|
||||||
|
<Input
|
||||||
|
value={ldapForm.search_filter}
|
||||||
|
onChange={(e) => setLdapForm({ ...ldapForm, search_filter: e.target.value })}
|
||||||
|
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||||
|
placeholder="(uid=%s)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 col-span-2">
|
||||||
|
<Switch
|
||||||
|
checked={ldapForm.use_ldaps}
|
||||||
|
onCheckedChange={(v) => setLdapForm({ ...ldapForm, use_ldaps: v })}
|
||||||
|
className="data-[state=checked]:bg-blue-600"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-slate-300">启用 LDAPS(SSL 加密)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 测试/保存按钮 */}
|
||||||
|
<div className="flex items-center gap-3 mt-4 pt-4 border-t border-slate-800">
|
||||||
|
{ldapTestResult && (
|
||||||
|
<span className={`text-xs ${ldapTestResult.includes('成功') ? 'text-emerald-400' : 'text-red-400'}`}>
|
||||||
|
{ldapTestResult}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2 ml-auto">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="border-slate-700 text-slate-300"
|
||||||
|
onClick={handleLdapTest}
|
||||||
|
disabled={ldapTesting}
|
||||||
|
>
|
||||||
|
{ldapTesting ? <Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" /> : <RefreshCw className="h-3.5 w-3.5 mr-1" />}
|
||||||
|
测试连接
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
|
onClick={handleLdapSave}
|
||||||
|
disabled={ldapSaving}
|
||||||
|
>
|
||||||
|
{ldapSaving ? '保存中...' : '保存配置'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* LDAP 账号管理 - 可折叠 */}
|
||||||
|
{ldapConfig && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<LdapManagementSection {...props} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import {
|
import {
|
||||||
@@ -16,7 +17,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { User } from 'lucide-react';
|
import { User } from 'lucide-react';
|
||||||
import { roles } from '@/mocks/data';
|
import { userApi } from '@/services/api';
|
||||||
|
|
||||||
export interface UserFormData {
|
export interface UserFormData {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -47,6 +48,32 @@ export function UserFormDialog({
|
|||||||
saving,
|
saving,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: UserFormDialogProps) {
|
}: UserFormDialogProps) {
|
||||||
|
const [roles, setRoles] = useState<{ id: string; name: string }[]>([]);
|
||||||
|
|
||||||
|
// 对话框打开时拉取角色列表 // Logan.Li
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
let cancelled = false;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const resp = await userApi.getRoles();
|
||||||
|
if (cancelled) return;
|
||||||
|
const list = resp?.list ?? [];
|
||||||
|
setRoles(
|
||||||
|
list.map((r: any) => ({
|
||||||
|
id: String(r.id ?? ''),
|
||||||
|
name: r.name ?? r.code ?? '',
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
if (!cancelled) setRoles([]);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="bg-slate-900 border-slate-800">
|
<DialogContent className="bg-slate-900 border-slate-800">
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
// Sidebar constants
|
||||||
|
export const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||||
|
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||||
|
export const SIDEBAR_WIDTH = "16rem"
|
||||||
|
export const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||||
|
export const SIDEBAR_WIDTH_ICON = "3rem"
|
||||||
|
export const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||||
|
|
||||||
|
// Sidebar context type
|
||||||
|
export const sidebarMenuButtonVariants = cva(
|
||||||
|
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||||
|
outline:
|
||||||
|
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-8 text-sm",
|
||||||
|
sm: "h-7 text-xs",
|
||||||
|
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export type SidebarContextProps = {
|
||||||
|
state: "expanded" | "collapsed"
|
||||||
|
open: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
openMobile: boolean
|
||||||
|
setOpenMobile: (open: boolean) => void
|
||||||
|
isMobile: boolean
|
||||||
|
toggleSidebar: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidebarContext = React.createContext<SidebarContextProps | null>(null)
|
||||||
|
|
||||||
|
export function useSidebar() {
|
||||||
|
const context = React.useContext(SidebarContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useSidebar must be used within a SidebarProvider.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { type VariantProps } from "class-variance-authority"
|
||||||
import { PanelLeftIcon } from "lucide-react"
|
import { PanelLeftIcon } from "lucide-react"
|
||||||
|
|
||||||
import { useIsMobile } from "@/hooks/use-mobile"
|
import { useIsMobile } from "@/hooks/use-mobile"
|
||||||
@@ -25,33 +25,18 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip"
|
} from "@/components/ui/tooltip"
|
||||||
|
|
||||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
import {
|
||||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
SIDEBAR_COOKIE_NAME,
|
||||||
const SIDEBAR_WIDTH = "16rem"
|
SIDEBAR_COOKIE_MAX_AGE,
|
||||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
SIDEBAR_WIDTH,
|
||||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
SIDEBAR_WIDTH_MOBILE,
|
||||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
SIDEBAR_WIDTH_ICON,
|
||||||
|
SIDEBAR_KEYBOARD_SHORTCUT,
|
||||||
type SidebarContextProps = {
|
SidebarContext,
|
||||||
state: "expanded" | "collapsed"
|
useSidebar,
|
||||||
open: boolean
|
sidebarMenuButtonVariants,
|
||||||
setOpen: (open: boolean) => void
|
type SidebarContextProps,
|
||||||
openMobile: boolean
|
} from "./sidebar-context"
|
||||||
setOpenMobile: (open: boolean) => void
|
|
||||||
isMobile: boolean
|
|
||||||
toggleSidebar: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
|
|
||||||
|
|
||||||
function useSidebar() {
|
|
||||||
const context = React.useContext(SidebarContext)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useSidebar must be used within a SidebarProvider.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarProvider({
|
function SidebarProvider({
|
||||||
defaultOpen = true,
|
defaultOpen = true,
|
||||||
@@ -473,28 +458,6 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sidebarMenuButtonVariants = cva(
|
|
||||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
|
||||||
outline:
|
|
||||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-8 text-sm",
|
|
||||||
sm: "h-7 text-xs",
|
|
||||||
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function SidebarMenuButton({
|
function SidebarMenuButton({
|
||||||
asChild = false,
|
asChild = false,
|
||||||
isActive = false,
|
isActive = false,
|
||||||
|
|||||||
+29
-2501
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,200 @@
|
|||||||
|
import type {
|
||||||
|
Server,
|
||||||
|
K8sCluster,
|
||||||
|
K8sNode,
|
||||||
|
K8sPod,
|
||||||
|
AlertRule,
|
||||||
|
AlertEvent,
|
||||||
|
Ticket,
|
||||||
|
ApprovalFlow,
|
||||||
|
ApprovalInstance,
|
||||||
|
AppRelease,
|
||||||
|
User,
|
||||||
|
Role,
|
||||||
|
OperationLog,
|
||||||
|
CronJob,
|
||||||
|
Script,
|
||||||
|
DashboardData,
|
||||||
|
ResourceModel,
|
||||||
|
ModelInstance,
|
||||||
|
Cabinet,
|
||||||
|
Device,
|
||||||
|
TopologyNode,
|
||||||
|
TopologyEdge,
|
||||||
|
MetricData,
|
||||||
|
LogEntry,
|
||||||
|
} from '@/types';
|
||||||
|
|
||||||
|
// 仪表盘数据
|
||||||
|
|
||||||
|
export const alertRules: AlertRule[] = [
|
||||||
|
{
|
||||||
|
id: 'rule-001',
|
||||||
|
name: 'CPU使用率过高',
|
||||||
|
metric: 'cpu_usage_percent',
|
||||||
|
operator: '>',
|
||||||
|
threshold: 80,
|
||||||
|
duration: 300,
|
||||||
|
severity: 'warning',
|
||||||
|
enabled: true,
|
||||||
|
description: '服务器CPU使用率超过80%持续5分钟',
|
||||||
|
notifyChannels: ['email', 'dingtalk'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rule-002',
|
||||||
|
name: 'CPU使用率严重过高',
|
||||||
|
metric: 'cpu_usage_percent',
|
||||||
|
operator: '>',
|
||||||
|
threshold: 95,
|
||||||
|
duration: 180,
|
||||||
|
severity: 'critical',
|
||||||
|
enabled: true,
|
||||||
|
description: '服务器CPU使用率超过95%持续3分钟',
|
||||||
|
notifyChannels: ['email', 'sms', 'dingtalk'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rule-003',
|
||||||
|
name: '内存使用率过高',
|
||||||
|
metric: 'memory_usage_percent',
|
||||||
|
operator: '>',
|
||||||
|
threshold: 85,
|
||||||
|
duration: 300,
|
||||||
|
severity: 'warning',
|
||||||
|
enabled: true,
|
||||||
|
description: '服务器内存使用率超过85%持续5分钟',
|
||||||
|
notifyChannels: ['email', 'dingtalk'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rule-004',
|
||||||
|
name: '磁盘空间不足',
|
||||||
|
metric: 'disk_usage_percent',
|
||||||
|
operator: '>',
|
||||||
|
threshold: 90,
|
||||||
|
duration: 60,
|
||||||
|
severity: 'critical',
|
||||||
|
enabled: true,
|
||||||
|
description: '服务器磁盘使用率超过90%',
|
||||||
|
notifyChannels: ['email', 'sms', 'dingtalk'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rule-005',
|
||||||
|
name: '服务不可用',
|
||||||
|
metric: 'service_up',
|
||||||
|
operator: '==',
|
||||||
|
threshold: 0,
|
||||||
|
duration: 60,
|
||||||
|
severity: 'critical',
|
||||||
|
enabled: true,
|
||||||
|
description: '服务健康检查失败',
|
||||||
|
notifyChannels: ['email', 'sms', 'dingtalk', 'wechat'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rule-006',
|
||||||
|
name: '网络延迟过高',
|
||||||
|
metric: 'network_latency_ms',
|
||||||
|
operator: '>',
|
||||||
|
threshold: 100,
|
||||||
|
duration: 180,
|
||||||
|
severity: 'warning',
|
||||||
|
enabled: true,
|
||||||
|
description: '网络延迟超过100ms持续3分钟',
|
||||||
|
notifyChannels: ['email'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 告警事件数据
|
||||||
|
export const alertEvents: AlertEvent[] = [
|
||||||
|
{
|
||||||
|
id: 'alert-001',
|
||||||
|
ruleId: 'rule-002',
|
||||||
|
ruleName: 'CPU使用率严重过高',
|
||||||
|
severity: 'critical',
|
||||||
|
status: 'firing',
|
||||||
|
value: 97.5,
|
||||||
|
threshold: 95,
|
||||||
|
target: 'app-server-01 (192.168.1.103)',
|
||||||
|
message: '服务器CPU使用率97.5%,超过阈值95%',
|
||||||
|
startedAt: '2025-03-30T09:45:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'alert-002',
|
||||||
|
ruleId: 'rule-003',
|
||||||
|
ruleName: '内存使用率过高',
|
||||||
|
severity: 'warning',
|
||||||
|
status: 'firing',
|
||||||
|
value: 89.2,
|
||||||
|
threshold: 85,
|
||||||
|
target: 'k8s-worker-02 (192.168.1.212)',
|
||||||
|
message: '服务器内存使用率89.2%,超过阈值85%',
|
||||||
|
startedAt: '2025-03-30T09:30:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'alert-003',
|
||||||
|
ruleId: 'rule-004',
|
||||||
|
ruleName: '磁盘空间不足',
|
||||||
|
severity: 'critical',
|
||||||
|
status: 'firing',
|
||||||
|
value: 94.8,
|
||||||
|
threshold: 90,
|
||||||
|
target: 'db-server-01 (192.168.1.105)',
|
||||||
|
message: '服务器磁盘使用率94.8%,超过阈值90%',
|
||||||
|
startedAt: '2025-03-30T08:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'alert-004',
|
||||||
|
ruleId: 'rule-001',
|
||||||
|
ruleName: 'CPU使用率过高',
|
||||||
|
severity: 'warning',
|
||||||
|
status: 'resolved',
|
||||||
|
value: 75.2,
|
||||||
|
threshold: 80,
|
||||||
|
target: 'web-server-01 (192.168.1.101)',
|
||||||
|
message: '服务器CPU使用率已恢复正常',
|
||||||
|
startedAt: '2025-03-30T07:00:00Z',
|
||||||
|
resolvedAt: '2025-03-30T08:30:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'alert-005',
|
||||||
|
ruleId: 'rule-005',
|
||||||
|
ruleName: '服务不可用',
|
||||||
|
severity: 'critical',
|
||||||
|
status: 'firing',
|
||||||
|
value: 0,
|
||||||
|
threshold: 0,
|
||||||
|
target: 'test-server-01 (192.168.2.101)',
|
||||||
|
message: '服务健康检查失败,服务可能已停止',
|
||||||
|
startedAt: '2025-03-29T18:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 工单数据
|
||||||
|
export const alertCenterAlerts = [
|
||||||
|
{ id: 'alert-001', title: '数据库连接池耗尽', description: '生产环境数据库连接池使用率超过95%', severity: 'critical', status: 'firing', source: 'Prometheus', service: 'order-service', metric: 'db_connection_pool_usage', value: 98.5, threshold: 95, startedAt: '2025-04-14 10:15:00', labels: { env: 'production' }, annotations: {} },
|
||||||
|
{ id: 'alert-002', title: 'API响应时间过高', description: '用户服务API P99响应时间超过500ms', severity: 'warning', status: 'firing', source: 'SkyWalking', service: 'user-service', metric: 'http_request_duration_p99', value: 750, threshold: 500, startedAt: '2025-04-14 09:30:00', labels: {}, annotations: {} },
|
||||||
|
{ id: 'alert-003', title: '磁盘空间不足', description: '数据库服务器磁盘使用率超过90%', severity: 'critical', status: 'acknowledged', source: 'Zabbix', service: 'db-server-01', metric: 'disk_usage_percent', value: 94.2, threshold: 90, startedAt: '2025-04-14 08:00:00', acknowledgedBy: '李运维', acknowledgedAt: '2025-04-14 08:15:00', labels: {}, annotations: {} },
|
||||||
|
{ id: 'alert-004', title: '内存使用率过高', description: '应用服务器内存使用率超过85%', severity: 'warning', status: 'resolved', source: 'Prometheus', service: 'app-server-02', metric: 'memory_usage_percent', value: 72.5, threshold: 85, startedAt: '2025-04-14 07:00:00', resolvedAt: '2025-04-14 08:30:00', labels: {}, annotations: {} },
|
||||||
|
{ id: 'alert-005', title: '服务健康检查失败', description: '支付服务健康检查连续3次失败', severity: 'critical', status: 'firing', source: 'Kubernetes', service: 'payment-service', metric: 'health_check_status', value: 0, threshold: 1, startedAt: '2025-04-14 10:25:00', labels: {}, annotations: {} }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const alertCenterRules = [
|
||||||
|
{ id: 'arule-001', name: '数据库连接池使用率', description: '监控数据库连接池使用率', enabled: true, severity: 'critical', query: 'db_connection_pool_usage > 95', duration: '5m', threshold: 95, labels: ['database'], notificationChannels: ['email', 'slack'], lastEvaluated: '2025-04-14 10:30:00' },
|
||||||
|
{ id: 'arule-002', name: 'API响应时间P99', description: '监控API P99响应时间', enabled: true, severity: 'warning', query: 'http_request_duration_p99 > 500', duration: '3m', threshold: 500, labels: ['api'], notificationChannels: ['slack'], lastEvaluated: '2025-04-14 10:30:00' },
|
||||||
|
{ id: 'arule-003', name: '磁盘空间使用率', description: '监控服务器磁盘使用率', enabled: true, severity: 'critical', query: 'disk_usage_percent > 90', duration: '1m', threshold: 90, labels: ['disk'], notificationChannels: ['email', 'sms'], lastEvaluated: '2025-04-14 10:30:00' },
|
||||||
|
{ id: 'arule-004', name: '服务健康检查', description: '监控服务健康检查状态', enabled: true, severity: 'critical', query: 'health_check_status == 0', duration: '1m', threshold: 0, labels: ['health'], notificationChannels: ['email', 'pagerduty'], lastEvaluated: '2025-04-14 10:30:00' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const notificationChannels = [
|
||||||
|
{ id: 'ch-001', name: '运维邮箱组', type: 'email', enabled: true, config: { recipients: 'ops@company.com' } },
|
||||||
|
{ id: 'ch-002', name: '告警Slack频道', type: 'slack', enabled: true, config: { webhook: 'https://hooks.slack.com/xxx' } },
|
||||||
|
{ id: 'ch-003', name: '值班手机', type: 'sms', enabled: true, config: { phone: '13800138000' } },
|
||||||
|
{ id: 'ch-004', name: 'PagerDuty', type: 'pagerduty', enabled: false, config: { service_key: 'xxx' } },
|
||||||
|
{ id: 'ch-005', name: '企业微信', type: 'webhook', enabled: true, config: { url: 'https://qyapi.weixin.qq.com/xxx' } }
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============ 链路追踪数据 ============
|
||||||
|
export const recentAlerts = [
|
||||||
|
{ id: 'a-001', title: 'CPU使用率过高', severity: 'critical', service: 'app-server-01', time: '5分钟前' },
|
||||||
|
{ id: 'a-002', title: '内存使用率警告', severity: 'warning', service: 'k8s-worker-02', time: '15分钟前' },
|
||||||
|
{ id: 'a-003', title: '磁盘空间不足', severity: 'critical', service: 'db-server-01', time: '30分钟前' }
|
||||||
|
];
|
||||||
|
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import type {
|
||||||
|
Server,
|
||||||
|
K8sCluster,
|
||||||
|
K8sNode,
|
||||||
|
K8sPod,
|
||||||
|
AlertRule,
|
||||||
|
AlertEvent,
|
||||||
|
Ticket,
|
||||||
|
ApprovalFlow,
|
||||||
|
ApprovalInstance,
|
||||||
|
AppRelease,
|
||||||
|
User,
|
||||||
|
Role,
|
||||||
|
OperationLog,
|
||||||
|
CronJob,
|
||||||
|
Script,
|
||||||
|
DashboardData,
|
||||||
|
ResourceModel,
|
||||||
|
ModelInstance,
|
||||||
|
Cabinet,
|
||||||
|
Device,
|
||||||
|
TopologyNode,
|
||||||
|
TopologyEdge,
|
||||||
|
MetricData,
|
||||||
|
LogEntry,
|
||||||
|
} from '@/types';
|
||||||
|
|
||||||
|
// 仪表盘数据
|
||||||
|
|
||||||
|
export const approvalFlows: ApprovalFlow[] = [
|
||||||
|
{
|
||||||
|
id: 'flow-001',
|
||||||
|
name: '服务器资源申请审批',
|
||||||
|
description: '申请新服务器或扩容资源的审批流程',
|
||||||
|
enabled: true,
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
nodes: [
|
||||||
|
{ id: 'start', name: '开始', type: 'start', nextNodes: ['dept-approval'] },
|
||||||
|
{ id: 'dept-approval', name: '部门审批', type: 'approval', approvers: ['dept-manager'], nextNodes: ['it-approval'] },
|
||||||
|
{ id: 'it-approval', name: 'IT审批', type: 'approval', approvers: ['it-manager'], nextNodes: ['finance-approval'] },
|
||||||
|
{ id: 'finance-approval', name: '财务审批', type: 'approval', approvers: ['finance-manager'], nextNodes: ['end'] },
|
||||||
|
{ id: 'end', name: '结束', type: 'end', nextNodes: [] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'flow-002',
|
||||||
|
name: '生产环境变更审批',
|
||||||
|
description: '生产环境配置变更、发布等操作的审批流程',
|
||||||
|
enabled: true,
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
nodes: [
|
||||||
|
{ id: 'start', name: '开始', type: 'start', nextNodes: ['tech-approval'] },
|
||||||
|
{ id: 'tech-approval', name: '技术负责人审批', type: 'approval', approvers: ['tech-lead'], nextNodes: ['ops-approval'] },
|
||||||
|
{ id: 'ops-approval', name: '运维负责人审批', type: 'approval', approvers: ['ops-lead'], nextNodes: ['end'] },
|
||||||
|
{ id: 'end', name: '结束', type: 'end', nextNodes: [] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 审批实例数据
|
||||||
|
export const approvalInstances: ApprovalInstance[] = [
|
||||||
|
{
|
||||||
|
id: 'instance-001',
|
||||||
|
flowId: 'flow-001',
|
||||||
|
flowName: '服务器资源申请审批',
|
||||||
|
applicant: 'user-004',
|
||||||
|
applicantName: '王开发',
|
||||||
|
status: 'pending',
|
||||||
|
currentNode: 'it-approval',
|
||||||
|
createdAt: '2025-03-30T09:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'instance-002',
|
||||||
|
flowId: 'flow-002',
|
||||||
|
flowName: '生产环境变更审批',
|
||||||
|
applicant: 'user-002',
|
||||||
|
applicantName: '李运维',
|
||||||
|
status: 'pending',
|
||||||
|
currentNode: 'tech-approval',
|
||||||
|
createdAt: '2025-03-29T16:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'instance-003',
|
||||||
|
flowId: 'flow-001',
|
||||||
|
flowName: '服务器资源申请审批',
|
||||||
|
applicant: 'user-005',
|
||||||
|
applicantName: '赵开发',
|
||||||
|
status: 'approved',
|
||||||
|
currentNode: 'end',
|
||||||
|
createdAt: '2025-03-28T10:00:00Z',
|
||||||
|
completedAt: '2025-03-28T15:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 应用发布数据
|
||||||
|
export const tickets: Ticket[] = [
|
||||||
|
{
|
||||||
|
id: 'TICKET-2025-001',
|
||||||
|
title: '生产环境数据库性能优化',
|
||||||
|
type: 'incident',
|
||||||
|
priority: 'high',
|
||||||
|
status: 'in_progress',
|
||||||
|
assignee: 'user-003',
|
||||||
|
assigneeName: '张工程师',
|
||||||
|
creator: 'user-001',
|
||||||
|
creatorName: '管理员',
|
||||||
|
description: '生产环境数据库响应缓慢,需要紧急优化',
|
||||||
|
createdAt: '2025-03-30T08:00:00Z',
|
||||||
|
updatedAt: '2025-03-30T09:30:00Z',
|
||||||
|
dueDate: '2025-03-30T18:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'TICKET-2025-002',
|
||||||
|
title: '申请开通测试服务器访问权限',
|
||||||
|
type: 'request',
|
||||||
|
priority: 'medium',
|
||||||
|
status: 'open',
|
||||||
|
assignee: 'user-002',
|
||||||
|
assigneeName: '李运维',
|
||||||
|
creator: 'user-004',
|
||||||
|
creatorName: '王开发',
|
||||||
|
description: '新员工需要访问测试环境服务器进行开发调试',
|
||||||
|
createdAt: '2025-03-29T14:00:00Z',
|
||||||
|
updatedAt: '2025-03-29T14:00:00Z',
|
||||||
|
dueDate: '2025-04-01T18:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'TICKET-2025-003',
|
||||||
|
title: 'Kubernetes集群版本升级',
|
||||||
|
type: 'change',
|
||||||
|
priority: 'high',
|
||||||
|
status: 'open',
|
||||||
|
assignee: 'user-002',
|
||||||
|
assigneeName: '李运维',
|
||||||
|
creator: 'user-001',
|
||||||
|
creatorName: '管理员',
|
||||||
|
description: '计划将生产K8s集群从v1.28.5升级到v1.29.0',
|
||||||
|
createdAt: '2025-03-28T10:00:00Z',
|
||||||
|
updatedAt: '2025-03-28T10:00:00Z',
|
||||||
|
dueDate: '2025-04-05T20:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'TICKET-2025-004',
|
||||||
|
title: '磁盘空间清理',
|
||||||
|
type: 'incident',
|
||||||
|
priority: 'medium',
|
||||||
|
status: 'resolved',
|
||||||
|
assignee: 'user-003',
|
||||||
|
assigneeName: '张工程师',
|
||||||
|
creator: 'user-001',
|
||||||
|
creatorName: '管理员',
|
||||||
|
description: '日志服务器磁盘空间不足,需要清理旧日志',
|
||||||
|
createdAt: '2025-03-27T09:00:00Z',
|
||||||
|
updatedAt: '2025-03-27T16:00:00Z',
|
||||||
|
dueDate: '2025-03-27T18:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'TICKET-2025-005',
|
||||||
|
title: '新增监控告警规则',
|
||||||
|
type: 'request',
|
||||||
|
priority: 'low',
|
||||||
|
status: 'closed',
|
||||||
|
assignee: 'user-002',
|
||||||
|
assigneeName: '李运维',
|
||||||
|
creator: 'user-005',
|
||||||
|
creatorName: '赵开发',
|
||||||
|
description: '为新上线的微服务添加监控告警',
|
||||||
|
createdAt: '2025-03-26T11:00:00Z',
|
||||||
|
updatedAt: '2025-03-26T15:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 审批流程数据
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
import type {
|
||||||
|
Server,
|
||||||
|
K8sCluster,
|
||||||
|
K8sNode,
|
||||||
|
K8sPod,
|
||||||
|
AlertRule,
|
||||||
|
AlertEvent,
|
||||||
|
Ticket,
|
||||||
|
ApprovalFlow,
|
||||||
|
ApprovalInstance,
|
||||||
|
AppRelease,
|
||||||
|
User,
|
||||||
|
Role,
|
||||||
|
OperationLog,
|
||||||
|
CronJob,
|
||||||
|
Script,
|
||||||
|
DashboardData,
|
||||||
|
ResourceModel,
|
||||||
|
ModelInstance,
|
||||||
|
Cabinet,
|
||||||
|
Device,
|
||||||
|
TopologyNode,
|
||||||
|
TopologyEdge,
|
||||||
|
MetricData,
|
||||||
|
LogEntry,
|
||||||
|
} from '@/types';
|
||||||
|
|
||||||
|
// 仪表盘数据
|
||||||
|
|
||||||
|
export const cronJobs: CronJob[] = [
|
||||||
|
{
|
||||||
|
id: 'job-001',
|
||||||
|
name: '日志清理任务',
|
||||||
|
description: '清理7天前的应用日志',
|
||||||
|
cron: '0 2 * * *',
|
||||||
|
command: '/opt/scripts/cleanup_logs.sh',
|
||||||
|
status: 'running',
|
||||||
|
lastRunAt: '2025-03-30T02:00:00Z',
|
||||||
|
nextRunAt: '2025-03-31T02:00:00Z',
|
||||||
|
successCount: 365,
|
||||||
|
failCount: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'job-002',
|
||||||
|
name: '数据库备份',
|
||||||
|
description: '每日数据库全量备份',
|
||||||
|
cron: '0 3 * * *',
|
||||||
|
command: '/opt/scripts/backup_db.sh',
|
||||||
|
status: 'running',
|
||||||
|
lastRunAt: '2025-03-30T03:00:00Z',
|
||||||
|
nextRunAt: '2025-03-31T03:00:00Z',
|
||||||
|
successCount: 365,
|
||||||
|
failCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'job-003',
|
||||||
|
name: '健康检查',
|
||||||
|
description: '系统健康状态检查',
|
||||||
|
cron: '*/5 * * * *',
|
||||||
|
command: '/opt/scripts/health_check.sh',
|
||||||
|
status: 'running',
|
||||||
|
lastRunAt: '2025-03-30T10:25:00Z',
|
||||||
|
nextRunAt: '2025-03-30T10:30:00Z',
|
||||||
|
successCount: 105120,
|
||||||
|
failCount: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'job-004',
|
||||||
|
name: '临时数据清理',
|
||||||
|
description: '清理临时目录数据',
|
||||||
|
cron: '0 1 * * 0',
|
||||||
|
command: '/opt/scripts/cleanup_temp.sh',
|
||||||
|
status: 'paused',
|
||||||
|
successCount: 52,
|
||||||
|
failCount: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 脚本数据
|
||||||
|
export const cronJobsList = [
|
||||||
|
{ id: 'cron-001', name: '数据库备份', description: '每日凌晨执行数据库全量备份', schedule: '0 2 * * *', command: '/scripts/backup-db.sh', enabled: true, lastRun: '2025-04-14 02:00:00', nextRun: '2025-04-15 02:00:00', createdAt: '2025-01-01', createdBy: '李运维' },
|
||||||
|
{ id: 'cron-002', name: '日志清理', description: '清理7天前的应用日志', schedule: '0 3 * * *', command: '/scripts/cleanup-logs.sh', enabled: true, lastRun: '2025-04-14 03:00:00', nextRun: '2025-04-15 03:00:00', createdAt: '2025-01-01', createdBy: '张工程师' },
|
||||||
|
{ id: 'cron-003', name: '数据同步', description: '每小时同步订单数据到数据仓库', schedule: '0 * * * *', command: '/scripts/sync-data.sh', enabled: false, lastRun: '2025-04-13 23:00:00', nextRun: '-', createdAt: '2025-02-01', createdBy: '王开发' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const cronJobExecutions = [
|
||||||
|
{ id: 'exec-cron-001', jobId: 'cron-001', jobName: '数据库备份', status: 'success', startedAt: '2025-04-14 02:00:00', duration: 1200, output: 'Backup completed. Size: 2.5GB' },
|
||||||
|
{ id: 'exec-cron-002', jobId: 'cron-001', jobName: '数据库备份', status: 'success', startedAt: '2025-04-13 02:00:00', duration: 1150, output: 'Backup completed. Size: 2.4GB' },
|
||||||
|
{ id: 'exec-cron-003', jobId: 'cron-002', jobName: '日志清理', status: 'success', startedAt: '2025-04-14 03:00:00', duration: 45, output: 'Cleaned 150 log files' },
|
||||||
|
{ id: 'exec-cron-004', jobId: 'cron-003', jobName: '数据同步', status: 'failed', startedAt: '2025-04-13 23:00:00', duration: 300, output: 'Error: Connection timeout' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============ 仪表盘统计数据 ============
|
||||||
|
export const scripts: Script[] = [
|
||||||
|
{
|
||||||
|
id: 'script-001',
|
||||||
|
name: '重启Nginx服务',
|
||||||
|
description: '安全重启Nginx服务',
|
||||||
|
language: 'bash',
|
||||||
|
content: '#!/bin/bash\necho "正在重启Nginx服务..."\nsystemctl reload nginx\necho "Nginx服务重启完成"',
|
||||||
|
params: [],
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
updatedAt: '2024-01-01T00:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'script-002',
|
||||||
|
name: '清理Docker镜像',
|
||||||
|
description: '清理未使用的Docker镜像',
|
||||||
|
language: 'bash',
|
||||||
|
content: '#!/bin/bash\necho "清理未使用的Docker镜像..."\ndocker image prune -af\necho "清理完成"',
|
||||||
|
params: [
|
||||||
|
{ name: 'dry_run', type: 'boolean', required: false, default: 'false', description: '仅预览,不实际删除' },
|
||||||
|
],
|
||||||
|
createdAt: '2024-01-15T00:00:00Z',
|
||||||
|
updatedAt: '2024-02-01T00:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'script-003',
|
||||||
|
name: '查看端口占用',
|
||||||
|
description: '查看指定端口的占用情况',
|
||||||
|
language: 'bash',
|
||||||
|
content: '#!/bin/bash\nPORT=${1:-8080}\necho "查看端口 $PORT 的占用情况..."\nnetstat -tlnp | grep :$PORT',
|
||||||
|
params: [
|
||||||
|
{ name: 'port', type: 'number', required: true, description: '端口号' },
|
||||||
|
],
|
||||||
|
createdAt: '2024-02-15T00:00:00Z',
|
||||||
|
updatedAt: '2024-02-15T00:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ==================== CMDB资源模型数据 ====================
|
||||||
|
|
||||||
|
export const ansiblePlaybooks = [
|
||||||
|
{
|
||||||
|
id: 'pb-001',
|
||||||
|
name: '部署Nginx服务',
|
||||||
|
description: '在目标主机上安装和配置Nginx服务',
|
||||||
|
content: '---\n- name: Deploy Nginx\n hosts: web_servers\n become: yes\n tasks:\n - name: Install Nginx\n apt:\n name: nginx\n state: present\n - name: Start Nginx\n service:\n name: nginx\n state: started\n enabled: yes',
|
||||||
|
tags: ['nginx', 'web', 'deployment'],
|
||||||
|
category: 'deployment',
|
||||||
|
createdAt: '2025-04-01',
|
||||||
|
updatedAt: '2025-04-10',
|
||||||
|
createdBy: '张工程师',
|
||||||
|
variables: [
|
||||||
|
{ name: 'nginx_version', defaultValue: 'latest', description: 'Nginx版本', required: false },
|
||||||
|
{ name: 'listen_port', defaultValue: '80', description: '监听端口', required: false }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pb-002',
|
||||||
|
name: '系统安全加固',
|
||||||
|
description: '执行系统安全加固配置',
|
||||||
|
content: '---\n- name: System Hardening\n hosts: all\n become: yes\n tasks:\n - name: Update packages\n apt:\n update_cache: yes\n upgrade: dist\n - name: Configure firewall\n ufw:\n state: enabled\n policy: deny',
|
||||||
|
tags: ['security', 'hardening', 'system'],
|
||||||
|
category: 'security',
|
||||||
|
createdAt: '2025-03-15',
|
||||||
|
updatedAt: '2025-03-20',
|
||||||
|
createdBy: '李运维',
|
||||||
|
variables: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pb-003',
|
||||||
|
name: 'Docker环境安装',
|
||||||
|
description: '安装Docker和Docker Compose',
|
||||||
|
content: '---\n- name: Install Docker\n hosts: docker_hosts\n become: yes\n tasks:\n - name: Install Docker\n apt:\n name: docker.io\n state: present\n - name: Install Docker Compose\n pip:\n name: docker-compose\n state: present',
|
||||||
|
tags: ['docker', 'container', 'installation'],
|
||||||
|
category: 'configuration',
|
||||||
|
createdAt: '2025-04-05',
|
||||||
|
updatedAt: '2025-04-05',
|
||||||
|
createdBy: '王开发',
|
||||||
|
variables: [{ name: 'docker_version', defaultValue: 'latest', description: 'Docker版本', required: false }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pb-004',
|
||||||
|
name: 'MySQL备份',
|
||||||
|
description: '执行MySQL数据库备份',
|
||||||
|
content: '---\n- name: MySQL Backup\n hosts: db_servers\n become: yes\n tasks:\n - name: Create backup\n mysql_db:\n state: dump\n name: all\n target: /backup/mysql_{{ ansible_date_time.date }}.sql',
|
||||||
|
tags: ['mysql', 'backup', 'database'],
|
||||||
|
category: 'backup',
|
||||||
|
createdAt: '2025-03-20',
|
||||||
|
updatedAt: '2025-04-01',
|
||||||
|
createdBy: '张工程师',
|
||||||
|
variables: [
|
||||||
|
{ name: 'backup_path', defaultValue: '/backup', description: '备份路径', required: true },
|
||||||
|
{ name: 'retention_days', defaultValue: '7', description: '保留天数', required: false }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ansibleJobs = [
|
||||||
|
{
|
||||||
|
id: 'job-001',
|
||||||
|
playbookId: 'pb-001',
|
||||||
|
playbookName: '部署Nginx服务',
|
||||||
|
status: 'success',
|
||||||
|
targetHosts: ['web-server-01', 'web-server-02'],
|
||||||
|
variables: { nginx_version: '1.25.3', listen_port: '80' },
|
||||||
|
startedAt: '2025-04-14 10:00:00',
|
||||||
|
completedAt: '2025-04-14 10:05:30',
|
||||||
|
duration: 330000,
|
||||||
|
output: 'PLAY [Deploy Nginx] ***\nTASK [Install Nginx] ***\nchanged: [web-server-01]\nchanged: [web-server-02]',
|
||||||
|
triggeredBy: '张工程师'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'job-002',
|
||||||
|
playbookId: 'pb-002',
|
||||||
|
playbookName: '系统安全加固',
|
||||||
|
status: 'running',
|
||||||
|
targetHosts: ['app-server-01', 'app-server-02'],
|
||||||
|
variables: {},
|
||||||
|
startedAt: '2025-04-14 11:00:00',
|
||||||
|
output: 'PLAY [System Hardening] ***\nTASK [Update packages] ***\nRunning...',
|
||||||
|
triggeredBy: '李运维'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'job-003',
|
||||||
|
playbookId: 'pb-003',
|
||||||
|
playbookName: 'Docker环境安装',
|
||||||
|
status: 'failed',
|
||||||
|
targetHosts: ['test-server-01'],
|
||||||
|
variables: { docker_version: 'latest' },
|
||||||
|
startedAt: '2025-04-14 09:00:00',
|
||||||
|
completedAt: '2025-04-14 09:02:15',
|
||||||
|
duration: 135000,
|
||||||
|
output: 'PLAY [Install Docker] ***\nfatal: [test-server-01]: FAILED!',
|
||||||
|
triggeredBy: '王开发'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ansibleInventories = [
|
||||||
|
{
|
||||||
|
id: 'inv-001',
|
||||||
|
name: '生产环境主机',
|
||||||
|
hosts: [
|
||||||
|
{ id: 'h-001', name: 'web-server-01', ip: '192.168.1.101', port: 22, user: 'ansible', os: 'CentOS 7' },
|
||||||
|
{ id: 'h-002', name: 'web-server-02', ip: '192.168.1.102', port: 22, user: 'ansible', os: 'CentOS 7' },
|
||||||
|
{ id: 'h-003', name: 'app-server-01', ip: '192.168.1.103', port: 22, user: 'ansible', os: 'Ubuntu 22.04' },
|
||||||
|
{ id: 'h-004', name: 'app-server-02', ip: '192.168.1.104', port: 22, user: 'ansible', os: 'Ubuntu 22.04' },
|
||||||
|
{ id: 'h-005', name: 'db-server-01', ip: '192.168.1.105', port: 22, user: 'ansible', os: 'CentOS 8' }
|
||||||
|
],
|
||||||
|
groups: [
|
||||||
|
{ id: 'g-001', name: 'web_servers', hosts: ['h-001', 'h-002'] },
|
||||||
|
{ id: 'g-002', name: 'app_servers', hosts: ['h-003', 'h-004'] },
|
||||||
|
{ id: 'g-003', name: 'db_servers', hosts: ['h-005'] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'inv-002',
|
||||||
|
name: '测试环境主机',
|
||||||
|
hosts: [
|
||||||
|
{ id: 'h-006', name: 'test-server-01', ip: '192.168.2.101', port: 22, user: 'ansible', os: 'Ubuntu 22.04' },
|
||||||
|
{ id: 'h-007', name: 'test-server-02', ip: '192.168.2.102', port: 22, user: 'ansible', os: 'Ubuntu 22.04' }
|
||||||
|
],
|
||||||
|
groups: [{ id: 'g-004', name: 'test_servers', hosts: ['h-006', 'h-007'] }]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============ 日志数据 ============
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import type {
|
||||||
|
Server,
|
||||||
|
K8sCluster,
|
||||||
|
K8sNode,
|
||||||
|
K8sPod,
|
||||||
|
AlertRule,
|
||||||
|
AlertEvent,
|
||||||
|
Ticket,
|
||||||
|
ApprovalFlow,
|
||||||
|
ApprovalInstance,
|
||||||
|
AppRelease,
|
||||||
|
User,
|
||||||
|
Role,
|
||||||
|
OperationLog,
|
||||||
|
CronJob,
|
||||||
|
Script,
|
||||||
|
DashboardData,
|
||||||
|
ResourceModel,
|
||||||
|
ModelInstance,
|
||||||
|
Cabinet,
|
||||||
|
Device,
|
||||||
|
TopologyNode,
|
||||||
|
TopologyEdge,
|
||||||
|
MetricData,
|
||||||
|
LogEntry,
|
||||||
|
} from '@/types';
|
||||||
|
|
||||||
|
// 仪表盘数据
|
||||||
|
|
||||||
|
export const pipelines = [
|
||||||
|
{ id: 'pipe-001', name: 'user-service-pipeline', description: '用户服务CI/CD流水线', template: 'nodejs', stages: [{ name: '检出代码', status: 'success', duration: 15 }, { name: '构建', status: 'success', duration: 120 }, { name: '单元测试', status: 'success', duration: 60 }], createdAt: '2025-03-01', lastRun: '2025-04-14 10:00:00' },
|
||||||
|
{ id: 'pipe-002', name: 'order-service-pipeline', description: '订单服务CI/CD流水线', template: 'java', stages: [{ name: '检出代码', status: 'success', duration: 12 }, { name: '编译', status: 'success', duration: 180 }, { name: '集成测试', status: 'running', duration: 0 }], createdAt: '2025-03-05', lastRun: '2025-04-14 11:00:00' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const pipelineExecutions = [
|
||||||
|
{ id: 'exec-001', pipelineId: 'pipe-001', pipelineName: 'user-service-pipeline', status: 'success', branch: 'main', commitId: 'a1b2c3d', triggeredBy: '张工程师', startedAt: '2025-04-14 10:00:00', duration: 510 },
|
||||||
|
{ id: 'exec-002', pipelineId: 'pipe-001', pipelineName: 'user-service-pipeline', status: 'failed', branch: 'feature/new-ui', commitId: 'e4f5g6h', triggeredBy: '王开发', startedAt: '2025-04-14 09:00:00', duration: 245 },
|
||||||
|
{ id: 'exec-003', pipelineId: 'pipe-002', pipelineName: 'order-service-pipeline', status: 'running', branch: 'main', commitId: 'i7j8k9l', triggeredBy: '李运维', startedAt: '2025-04-14 11:00:00', duration: 0 }
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============ 发布数据 ============
|
||||||
|
export const appReleases: AppRelease[] = [
|
||||||
|
{
|
||||||
|
id: 'release-001',
|
||||||
|
appName: '用户中心服务',
|
||||||
|
version: 'v2.5.1',
|
||||||
|
environment: 'prod',
|
||||||
|
status: 'success',
|
||||||
|
pipeline: 'user-service-pipeline',
|
||||||
|
commitId: 'a1b2c3d',
|
||||||
|
branch: 'main',
|
||||||
|
deployer: 'user-002',
|
||||||
|
startedAt: '2025-03-30T06:00:00Z',
|
||||||
|
completedAt: '2025-03-30T06:15:00Z',
|
||||||
|
duration: 900,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'release-002',
|
||||||
|
appName: '订单服务',
|
||||||
|
version: 'v3.2.0',
|
||||||
|
environment: 'staging',
|
||||||
|
status: 'deploying',
|
||||||
|
pipeline: 'order-service-pipeline',
|
||||||
|
commitId: 'e4f5g6h',
|
||||||
|
branch: 'release/v3.2.0',
|
||||||
|
deployer: 'user-002',
|
||||||
|
startedAt: '2025-03-30T10:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'release-003',
|
||||||
|
appName: '支付网关',
|
||||||
|
version: 'v1.8.5',
|
||||||
|
environment: 'prod',
|
||||||
|
status: 'failed',
|
||||||
|
pipeline: 'payment-gateway-pipeline',
|
||||||
|
commitId: 'i7j8k9l',
|
||||||
|
branch: 'hotfix/payment-bug',
|
||||||
|
deployer: 'user-003',
|
||||||
|
startedAt: '2025-03-29T20:00:00Z',
|
||||||
|
completedAt: '2025-03-29T20:05:00Z',
|
||||||
|
duration: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'release-004',
|
||||||
|
appName: '消息推送服务',
|
||||||
|
version: 'v2.1.0',
|
||||||
|
environment: 'prod',
|
||||||
|
status: 'rolled_back',
|
||||||
|
pipeline: 'notification-service-pipeline',
|
||||||
|
commitId: 'm0n1o2p',
|
||||||
|
branch: 'main',
|
||||||
|
deployer: 'user-002',
|
||||||
|
startedAt: '2025-03-28T14:00:00Z',
|
||||||
|
completedAt: '2025-03-28T14:30:00Z',
|
||||||
|
duration: 1800,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'release-005',
|
||||||
|
appName: '数据分析平台',
|
||||||
|
version: 'v4.0.0-beta',
|
||||||
|
environment: 'test',
|
||||||
|
status: 'building',
|
||||||
|
pipeline: 'analytics-platform-pipeline',
|
||||||
|
commitId: 'q3r4s5t',
|
||||||
|
branch: 'feature/new-dashboard',
|
||||||
|
deployer: 'user-004',
|
||||||
|
startedAt: '2025-03-30T09:30:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 用户数据
|
||||||
|
export const releases = [
|
||||||
|
{ id: 'rel-001', appName: '用户中心服务', version: 'v2.5.1', environment: 'prod', status: 'success', pipeline: 'user-service-pipeline', commitId: 'a1b2c3d', branch: 'main', deployer: '张工程师', startedAt: '2025-04-14 06:00:00', completedAt: '2025-04-14 06:15:00', duration: 900 },
|
||||||
|
{ id: 'rel-002', appName: '订单服务', version: 'v3.2.0', environment: 'staging', status: 'deploying', pipeline: 'order-service-pipeline', commitId: 'e4f5g6h', branch: 'release/v3.2.0', deployer: '李运维', startedAt: '2025-04-14 10:00:00' },
|
||||||
|
{ id: 'rel-003', appName: '支付网关', version: 'v1.8.5', environment: 'prod', status: 'failed', pipeline: 'payment-gateway-pipeline', commitId: 'i7j8k9l', branch: 'hotfix/payment-bug', deployer: '张工程师', startedAt: '2025-04-13 20:00:00', completedAt: '2025-04-13 20:05:00', duration: 300 }
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============ 定时任务数据 ============
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
import type {
|
||||||
|
Server,
|
||||||
|
K8sCluster,
|
||||||
|
K8sNode,
|
||||||
|
K8sPod,
|
||||||
|
AlertRule,
|
||||||
|
AlertEvent,
|
||||||
|
Ticket,
|
||||||
|
ApprovalFlow,
|
||||||
|
ApprovalInstance,
|
||||||
|
AppRelease,
|
||||||
|
User,
|
||||||
|
Role,
|
||||||
|
OperationLog,
|
||||||
|
CronJob,
|
||||||
|
Script,
|
||||||
|
DashboardData,
|
||||||
|
ResourceModel,
|
||||||
|
ModelInstance,
|
||||||
|
Cabinet,
|
||||||
|
Device,
|
||||||
|
TopologyNode,
|
||||||
|
TopologyEdge,
|
||||||
|
MetricData,
|
||||||
|
LogEntry,
|
||||||
|
} from '@/types';
|
||||||
|
|
||||||
|
// 仪表盘数据
|
||||||
|
|
||||||
|
export const cloudAccounts = [
|
||||||
|
{
|
||||||
|
id: 'acc-001',
|
||||||
|
name: 'AWS生产账号',
|
||||||
|
provider: 'aws',
|
||||||
|
accessKey: 'AKIAIOSFODNN7EXAMPLE',
|
||||||
|
status: 'active' as const,
|
||||||
|
regionCount: 4,
|
||||||
|
serverCount: 56,
|
||||||
|
createdAt: '2024-01-10T08:00:00Z',
|
||||||
|
lastSync: '2025-03-30T10:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'acc-002',
|
||||||
|
name: '阿里云主账号',
|
||||||
|
provider: 'aliyun',
|
||||||
|
accessKey: 'LTAI5t8Z3y8Y7Z7Z7Z7Z7Z7Z',
|
||||||
|
status: 'active' as const,
|
||||||
|
regionCount: 3,
|
||||||
|
serverCount: 42,
|
||||||
|
createdAt: '2024-02-15T09:00:00Z',
|
||||||
|
lastSync: '2025-03-30T09:30:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'acc-003',
|
||||||
|
name: '腾讯云账号',
|
||||||
|
provider: 'tencent',
|
||||||
|
accessKey: 'AKIDz8krbsJ5yKBZQpn74WFkmLPx3EXAMPLE',
|
||||||
|
status: 'active' as const,
|
||||||
|
regionCount: 2,
|
||||||
|
serverCount: 28,
|
||||||
|
createdAt: '2024-03-01T10:00:00Z',
|
||||||
|
lastSync: '2025-03-30T09:45:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'acc-004',
|
||||||
|
name: '华为云测试账号',
|
||||||
|
provider: 'huawei',
|
||||||
|
accessKey: 'QK9Y7Y7Y7Y7Y7Y7Y7Y7Y',
|
||||||
|
status: 'error' as const,
|
||||||
|
regionCount: 1,
|
||||||
|
serverCount: 5,
|
||||||
|
createdAt: '2024-06-20T11:00:00Z',
|
||||||
|
lastSync: '2025-03-29T08:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'acc-005',
|
||||||
|
name: 'Azure国际版',
|
||||||
|
provider: 'azure',
|
||||||
|
accessKey: '7e8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e',
|
||||||
|
status: 'inactive' as const,
|
||||||
|
regionCount: 2,
|
||||||
|
serverCount: 12,
|
||||||
|
createdAt: '2024-08-10T12:00:00Z',
|
||||||
|
lastSync: '2025-03-15T10:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ==================== 域名数据 ====================
|
||||||
|
|
||||||
|
export const domains = [
|
||||||
|
{
|
||||||
|
id: 'domain-001',
|
||||||
|
name: 'example.com',
|
||||||
|
registrar: '阿里云',
|
||||||
|
status: 'active' as const,
|
||||||
|
expireDate: '2026-05-15',
|
||||||
|
dnsProvider: '阿里云DNS',
|
||||||
|
recordCount: 12,
|
||||||
|
sslEnabled: true,
|
||||||
|
sslExpireDate: '2025-09-15',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'domain-002',
|
||||||
|
name: 'myapp.io',
|
||||||
|
registrar: 'GoDaddy',
|
||||||
|
status: 'active' as const,
|
||||||
|
expireDate: '2025-12-20',
|
||||||
|
dnsProvider: 'Cloudflare',
|
||||||
|
recordCount: 8,
|
||||||
|
sslEnabled: true,
|
||||||
|
sslExpireDate: '2025-11-20',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'domain-003',
|
||||||
|
name: 'test-site.cn',
|
||||||
|
registrar: '腾讯云',
|
||||||
|
status: 'active' as const,
|
||||||
|
expireDate: '2025-08-30',
|
||||||
|
dnsProvider: 'DNSPod',
|
||||||
|
recordCount: 5,
|
||||||
|
sslEnabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'domain-004',
|
||||||
|
name: 'old-project.com',
|
||||||
|
registrar: 'Namecheap',
|
||||||
|
status: 'expired' as const,
|
||||||
|
expireDate: '2025-01-10',
|
||||||
|
dnsProvider: 'Cloudflare',
|
||||||
|
recordCount: 3,
|
||||||
|
sslEnabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'domain-005',
|
||||||
|
name: 'new-service.dev',
|
||||||
|
registrar: 'AWS Route53',
|
||||||
|
status: 'pending' as const,
|
||||||
|
expireDate: '2026-03-25',
|
||||||
|
dnsProvider: 'Route53',
|
||||||
|
recordCount: 2,
|
||||||
|
sslEnabled: true,
|
||||||
|
sslExpireDate: '2025-10-25',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ==================== 区域数据 ====================
|
||||||
|
|
||||||
|
export const regions = [
|
||||||
|
{
|
||||||
|
id: 'region-001',
|
||||||
|
name: '华北-北京',
|
||||||
|
code: 'cn-north-1',
|
||||||
|
provider: 'aliyun',
|
||||||
|
location: '中国北京',
|
||||||
|
status: 'active' as const,
|
||||||
|
serverCount: 45,
|
||||||
|
vpcCount: 8,
|
||||||
|
endpoint: 'ecs.cn-north-1.aliyuncs.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'region-002',
|
||||||
|
name: '华东-上海',
|
||||||
|
code: 'cn-shanghai',
|
||||||
|
provider: 'aliyun',
|
||||||
|
location: '中国上海',
|
||||||
|
status: 'active' as const,
|
||||||
|
serverCount: 32,
|
||||||
|
vpcCount: 5,
|
||||||
|
endpoint: 'ecs.cn-shanghai.aliyuncs.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'region-003',
|
||||||
|
name: '美国东部-弗吉尼亚',
|
||||||
|
code: 'us-east-1',
|
||||||
|
provider: 'aws',
|
||||||
|
location: '美国弗吉尼亚',
|
||||||
|
status: 'active' as const,
|
||||||
|
serverCount: 28,
|
||||||
|
vpcCount: 6,
|
||||||
|
endpoint: 'ec2.us-east-1.amazonaws.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'region-004',
|
||||||
|
name: '亚太地区-新加坡',
|
||||||
|
code: 'ap-southeast-1',
|
||||||
|
provider: 'aws',
|
||||||
|
location: '新加坡',
|
||||||
|
status: 'active' as const,
|
||||||
|
serverCount: 18,
|
||||||
|
vpcCount: 3,
|
||||||
|
endpoint: 'ec2.ap-southeast-1.amazonaws.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'region-005',
|
||||||
|
name: '华南-广州',
|
||||||
|
code: 'ap-guangzhou',
|
||||||
|
provider: 'tencent',
|
||||||
|
location: '中国广州',
|
||||||
|
status: 'active' as const,
|
||||||
|
serverCount: 22,
|
||||||
|
vpcCount: 4,
|
||||||
|
endpoint: 'cvm.tencentcloudapi.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'region-006',
|
||||||
|
name: '华东-南京',
|
||||||
|
code: 'cn-east-2',
|
||||||
|
provider: 'huawei',
|
||||||
|
location: '中国南京',
|
||||||
|
status: 'inactive' as const,
|
||||||
|
serverCount: 5,
|
||||||
|
vpcCount: 1,
|
||||||
|
endpoint: 'ecs.cn-east-2.myhuaweicloud.com',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ==================== 云平台数据 ====================
|
||||||
|
|
||||||
|
export const platforms = [
|
||||||
|
{
|
||||||
|
id: 'platform-001',
|
||||||
|
name: '阿里云',
|
||||||
|
code: 'aliyun',
|
||||||
|
type: 'public' as const,
|
||||||
|
status: 'active' as const,
|
||||||
|
accountCount: 2,
|
||||||
|
regionCount: 8,
|
||||||
|
serverCount: 87,
|
||||||
|
website: 'https://www.aliyun.com',
|
||||||
|
description: '阿里巴巴集团旗下的云计算服务平台',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'platform-002',
|
||||||
|
name: 'AWS',
|
||||||
|
code: 'aws',
|
||||||
|
type: 'public' as const,
|
||||||
|
status: 'active' as const,
|
||||||
|
accountCount: 1,
|
||||||
|
regionCount: 25,
|
||||||
|
serverCount: 56,
|
||||||
|
website: 'https://aws.amazon.com',
|
||||||
|
description: 'Amazon Web Services,全球领先的云服务平台',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'platform-003',
|
||||||
|
name: '腾讯云',
|
||||||
|
code: 'tencent',
|
||||||
|
type: 'public' as const,
|
||||||
|
status: 'active' as const,
|
||||||
|
accountCount: 1,
|
||||||
|
regionCount: 6,
|
||||||
|
serverCount: 28,
|
||||||
|
website: 'https://cloud.tencent.com',
|
||||||
|
description: '腾讯公司旗下的云计算服务平台',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'platform-004',
|
||||||
|
name: '华为云',
|
||||||
|
code: 'huawei',
|
||||||
|
type: 'public' as const,
|
||||||
|
status: 'active' as const,
|
||||||
|
accountCount: 1,
|
||||||
|
regionCount: 5,
|
||||||
|
serverCount: 15,
|
||||||
|
website: 'https://www.huaweicloud.com',
|
||||||
|
description: '华为公司旗下的云计算服务平台',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'platform-005',
|
||||||
|
name: '私有云-OpenStack',
|
||||||
|
code: 'private-openstack',
|
||||||
|
type: 'private' as const,
|
||||||
|
status: 'active' as const,
|
||||||
|
accountCount: 1,
|
||||||
|
regionCount: 1,
|
||||||
|
serverCount: 32,
|
||||||
|
website: 'https://www.openstack.org',
|
||||||
|
description: '自建OpenStack私有云平台',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ==================== 站内导航数据 ====================
|
||||||
|
|
||||||
|
export const cloudProvidersList = [
|
||||||
|
{ id: 'aliyun', name: '阿里云', icon: 'aliyun', regions: ['cn-beijing', 'cn-shanghai'] },
|
||||||
|
{ id: 'tencent', name: '腾讯云', icon: 'tencent', regions: ['ap-beijing', 'ap-shanghai'] },
|
||||||
|
{ id: 'aws', name: 'AWS', icon: 'aws', regions: ['us-east-1', 'us-west-2'] },
|
||||||
|
{ id: 'huawei', name: '华为云', icon: 'huawei', regions: ['cn-north-1', 'cn-east-2'] }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const cloudAccountsList = [
|
||||||
|
{ id: 'acc-001', provider: 'aliyun', name: '生产账号', accountId: '123456789', status: 'active' },
|
||||||
|
{ id: 'acc-002', provider: 'aliyun', name: '测试账号', accountId: '987654321', status: 'active' },
|
||||||
|
{ id: 'acc-003', provider: 'tencent', name: '主账号', accountId: 'Tencent-001', status: 'active' },
|
||||||
|
{ id: 'acc-004', provider: 'aws', name: 'Global账号', accountId: 'aws-123456', status: 'inactive' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const cloudRegionsList = [
|
||||||
|
{ id: 'cn-beijing', name: '华北2(北京)', provider: 'aliyun', vpcCount: 5, instanceCount: 45 },
|
||||||
|
{ id: 'cn-shanghai', name: '华东2(上海)', provider: 'aliyun', vpcCount: 3, instanceCount: 32 },
|
||||||
|
{ id: 'ap-beijing', name: '北京', provider: 'tencent', vpcCount: 4, instanceCount: 28 },
|
||||||
|
{ id: 'ap-shanghai', name: '上海', provider: 'tencent', vpcCount: 2, instanceCount: 15 }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const domainsList = [
|
||||||
|
{ id: 'domain-001', name: 'company.com', registrar: 'aliyun', expiryDate: '2026-03-15', status: 'active' },
|
||||||
|
{ id: 'domain-002', name: 'company.cn', registrar: 'tencent', expiryDate: '2026-06-20', status: 'active' },
|
||||||
|
{ id: 'domain-003', name: 'company-api.com', registrar: 'aws', expiryDate: '2025-12-01', status: 'active' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============ 审批数据 ============
|
||||||
|
export const costStats = {
|
||||||
|
total: 125000,
|
||||||
|
byProvider: [
|
||||||
|
{ name: '阿里云', value: 65000 }, { name: '腾讯云', value: 35000 },
|
||||||
|
{ name: 'AWS', value: 20000 }, { name: '华为云', value: 5000 }
|
||||||
|
],
|
||||||
|
trend: [
|
||||||
|
{ month: '2025-01', value: 110000 }, { month: '2025-02', value: 115000 },
|
||||||
|
{ month: '2025-03', value: 120000 }, { month: '2025-04', value: 125000 }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const certExpiry = [
|
||||||
|
{ domain: 'api.company.com', expiryDate: '2025-05-15', daysLeft: 31 },
|
||||||
|
{ domain: 'app.company.com', expiryDate: '2025-06-01', daysLeft: 48 },
|
||||||
|
{ domain: 'admin.company.com', expiryDate: '2025-04-20', daysLeft: 6 },
|
||||||
|
{ domain: '*.company.com', expiryDate: '2025-07-15', daysLeft: 92 }
|
||||||
|
];
|
||||||
|
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import type {
|
||||||
|
Server,
|
||||||
|
K8sCluster,
|
||||||
|
K8sNode,
|
||||||
|
K8sPod,
|
||||||
|
AlertRule,
|
||||||
|
AlertEvent,
|
||||||
|
Ticket,
|
||||||
|
ApprovalFlow,
|
||||||
|
ApprovalInstance,
|
||||||
|
AppRelease,
|
||||||
|
User,
|
||||||
|
Role,
|
||||||
|
OperationLog,
|
||||||
|
CronJob,
|
||||||
|
Script,
|
||||||
|
DashboardData,
|
||||||
|
ResourceModel,
|
||||||
|
ModelInstance,
|
||||||
|
Cabinet,
|
||||||
|
Device,
|
||||||
|
TopologyNode,
|
||||||
|
TopologyEdge,
|
||||||
|
MetricData,
|
||||||
|
LogEntry,
|
||||||
|
} from '@/types';
|
||||||
|
|
||||||
|
// 仪表盘数据
|
||||||
|
|
||||||
|
export const dashboardData: DashboardData = {
|
||||||
|
serverCount: 156,
|
||||||
|
runningServers: 142,
|
||||||
|
alertCount: 23,
|
||||||
|
criticalAlerts: 5,
|
||||||
|
k8sClusters: 8,
|
||||||
|
healthyClusters: 7,
|
||||||
|
pendingTickets: 18,
|
||||||
|
todayReleases: 12,
|
||||||
|
cpuUsage: 67.5,
|
||||||
|
memoryUsage: 78.2,
|
||||||
|
diskUsage: 45.8,
|
||||||
|
networkUsage: 34.6,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 服务器资产数据
|
||||||
|
export const dashboardStats = {
|
||||||
|
serverCount: 156, runningServers: 142, alertCount: 23, criticalAlerts: 5,
|
||||||
|
k8sClusters: 8, healthyClusters: 7, pendingTickets: 18, todayReleases: 12,
|
||||||
|
cpuUsage: 67.5, memoryUsage: 78.2, diskUsage: 45.8, networkUsage: 34.6
|
||||||
|
};
|
||||||
|
|
||||||
|
export const topCPU = [
|
||||||
|
{ name: 'app-server-01', value: 95.2 }, { name: 'k8s-worker-02', value: 89.5 },
|
||||||
|
{ name: 'app-server-02', value: 82.1 }, { name: 'db-server-01', value: 78.6 },
|
||||||
|
{ name: 'k8s-worker-01', value: 75.3 }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const topMemory = [
|
||||||
|
{ name: 'db-server-01', value: 92.5 }, { name: 'k8s-worker-02', value: 88.2 },
|
||||||
|
{ name: 'app-server-02', value: 85.6 }, { name: 'app-server-01', value: 81.3 },
|
||||||
|
{ name: 'k8s-worker-01', value: 78.9 }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const topDisk = [
|
||||||
|
{ name: 'db-server-01', value: 94.8 }, { name: 'log-server-01', value: 87.5 },
|
||||||
|
{ name: 'backup-server-01', value: 82.3 }, { name: 'app-server-01', value: 68.5 },
|
||||||
|
{ name: 'app-server-02', value: 62.1 }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const metricData: MetricData[] = [
|
||||||
|
{ timestamp: 1711800000, metric: 'cpu_usage', value: 45.2, labels: { host: 'web-server-01' } },
|
||||||
|
{ timestamp: 1711800060, metric: 'cpu_usage', value: 48.5, labels: { host: 'web-server-01' } },
|
||||||
|
{ timestamp: 1711800120, metric: 'cpu_usage', value: 52.1, labels: { host: 'web-server-01' } },
|
||||||
|
{ timestamp: 1711800180, metric: 'cpu_usage', value: 49.8, labels: { host: 'web-server-01' } },
|
||||||
|
{ timestamp: 1711800240, metric: 'cpu_usage', value: 46.3, labels: { host: 'web-server-01' } },
|
||||||
|
{ timestamp: 1711800300, metric: 'cpu_usage', value: 44.7, labels: { host: 'web-server-01' } },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ==================== 日志数据 ====================
|
||||||
|
|
||||||
|
export function generateMetricsData(points: number = 24): { time: string; cpu: number; memory: number; disk: number; network: number }[] {
|
||||||
|
const data = [];
|
||||||
|
const now = new Date();
|
||||||
|
for (let i = points - 1; i >= 0; i--) {
|
||||||
|
const time = new Date(now.getTime() - i * 60 * 60 * 1000);
|
||||||
|
data.push({
|
||||||
|
time: time.getHours().toString().padStart(2, '0') + ':00',
|
||||||
|
cpu: 40 + Math.random() * 40,
|
||||||
|
memory: 50 + Math.random() * 30,
|
||||||
|
disk: 45 + Math.random() * 10,
|
||||||
|
network: 20 + Math.random() * 50,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 云账号数据 ====================
|
||||||
|
|
||||||
@@ -0,0 +1,471 @@
|
|||||||
|
import type {
|
||||||
|
Server,
|
||||||
|
K8sCluster,
|
||||||
|
K8sNode,
|
||||||
|
K8sPod,
|
||||||
|
AlertRule,
|
||||||
|
AlertEvent,
|
||||||
|
Ticket,
|
||||||
|
ApprovalFlow,
|
||||||
|
ApprovalInstance,
|
||||||
|
AppRelease,
|
||||||
|
User,
|
||||||
|
Role,
|
||||||
|
OperationLog,
|
||||||
|
CronJob,
|
||||||
|
Script,
|
||||||
|
DashboardData,
|
||||||
|
ResourceModel,
|
||||||
|
ModelInstance,
|
||||||
|
Cabinet,
|
||||||
|
Device,
|
||||||
|
TopologyNode,
|
||||||
|
TopologyEdge,
|
||||||
|
MetricData,
|
||||||
|
LogEntry,
|
||||||
|
} from '@/types';
|
||||||
|
|
||||||
|
// 仪表盘数据
|
||||||
|
|
||||||
|
export const k8sClusters: K8sCluster[] = [
|
||||||
|
{
|
||||||
|
id: 'k8s-001',
|
||||||
|
name: 'production-cluster',
|
||||||
|
version: 'v1.28.5',
|
||||||
|
nodes: 12,
|
||||||
|
pods: 156,
|
||||||
|
services: 45,
|
||||||
|
deployments: 78,
|
||||||
|
status: 'healthy',
|
||||||
|
cpuUsage: 68.5,
|
||||||
|
memoryUsage: 75.2,
|
||||||
|
createdAt: '2024-01-20T08:00:00Z',
|
||||||
|
namespace: 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'k8s-002',
|
||||||
|
name: 'staging-cluster',
|
||||||
|
version: 'v1.28.5',
|
||||||
|
nodes: 6,
|
||||||
|
pods: 89,
|
||||||
|
services: 28,
|
||||||
|
deployments: 42,
|
||||||
|
status: 'healthy',
|
||||||
|
cpuUsage: 45.3,
|
||||||
|
memoryUsage: 52.8,
|
||||||
|
createdAt: '2024-02-15T09:00:00Z',
|
||||||
|
namespace: 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'k8s-003',
|
||||||
|
name: 'development-cluster',
|
||||||
|
version: 'v1.27.8',
|
||||||
|
nodes: 4,
|
||||||
|
pods: 56,
|
||||||
|
services: 18,
|
||||||
|
deployments: 25,
|
||||||
|
status: 'healthy',
|
||||||
|
cpuUsage: 32.1,
|
||||||
|
memoryUsage: 38.5,
|
||||||
|
createdAt: '2024-03-01T10:00:00Z',
|
||||||
|
namespace: 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'k8s-004',
|
||||||
|
name: 'test-cluster',
|
||||||
|
version: 'v1.27.8',
|
||||||
|
nodes: 3,
|
||||||
|
pods: 34,
|
||||||
|
services: 12,
|
||||||
|
deployments: 15,
|
||||||
|
status: 'warning',
|
||||||
|
cpuUsage: 85.6,
|
||||||
|
memoryUsage: 92.3,
|
||||||
|
createdAt: '2024-04-10T11:00:00Z',
|
||||||
|
namespace: 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'k8s-005',
|
||||||
|
name: 'ai-training-cluster',
|
||||||
|
version: 'v1.29.0',
|
||||||
|
nodes: 8,
|
||||||
|
pods: 67,
|
||||||
|
services: 15,
|
||||||
|
deployments: 32,
|
||||||
|
status: 'healthy',
|
||||||
|
cpuUsage: 78.9,
|
||||||
|
memoryUsage: 88.5,
|
||||||
|
createdAt: '2024-05-20T12:00:00Z',
|
||||||
|
namespace: 'default',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// K8s节点数据
|
||||||
|
export const k8sNodes: K8sNode[] = [
|
||||||
|
{
|
||||||
|
id: 'node-001',
|
||||||
|
name: 'k8s-master-01',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
role: 'master',
|
||||||
|
status: 'ready',
|
||||||
|
cpu: 16,
|
||||||
|
memory: 64,
|
||||||
|
cpuUsage: 25.6,
|
||||||
|
memoryUsage: 42.3,
|
||||||
|
pods: 25,
|
||||||
|
os: 'CentOS Linux 7 (Core)',
|
||||||
|
kernel: '5.4.210-1.el7.elrepo.x86_64',
|
||||||
|
createdAt: '2024-01-20T08:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node-002',
|
||||||
|
name: 'k8s-worker-01',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
role: 'worker',
|
||||||
|
status: 'ready',
|
||||||
|
cpu: 32,
|
||||||
|
memory: 128,
|
||||||
|
cpuUsage: 68.4,
|
||||||
|
memoryUsage: 82.1,
|
||||||
|
pods: 35,
|
||||||
|
os: 'CentOS Linux 7 (Core)',
|
||||||
|
kernel: '5.4.210-1.el7.elrepo.x86_64',
|
||||||
|
createdAt: '2024-01-20T08:30:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node-003',
|
||||||
|
name: 'k8s-worker-02',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
role: 'worker',
|
||||||
|
status: 'ready',
|
||||||
|
cpu: 32,
|
||||||
|
memory: 128,
|
||||||
|
cpuUsage: 72.5,
|
||||||
|
memoryUsage: 78.9,
|
||||||
|
pods: 38,
|
||||||
|
os: 'CentOS Linux 7 (Core)',
|
||||||
|
kernel: '5.4.210-1.el7.elrepo.x86_64',
|
||||||
|
createdAt: '2024-01-20T08:35:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node-004',
|
||||||
|
name: 'k8s-worker-03',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
role: 'worker',
|
||||||
|
status: 'ready',
|
||||||
|
cpu: 32,
|
||||||
|
memory: 128,
|
||||||
|
cpuUsage: 65.2,
|
||||||
|
memoryUsage: 71.5,
|
||||||
|
pods: 32,
|
||||||
|
os: 'CentOS Linux 7 (Core)',
|
||||||
|
kernel: '5.4.210-1.el7.elrepo.x86_64',
|
||||||
|
createdAt: '2024-01-20T08:40:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node-005',
|
||||||
|
name: 'k8s-master-01',
|
||||||
|
clusterId: 'k8s-002',
|
||||||
|
role: 'master',
|
||||||
|
status: 'ready',
|
||||||
|
cpu: 8,
|
||||||
|
memory: 32,
|
||||||
|
cpuUsage: 18.5,
|
||||||
|
memoryUsage: 35.2,
|
||||||
|
pods: 15,
|
||||||
|
os: 'Ubuntu 22.04.3 LTS',
|
||||||
|
kernel: '5.15.0-88-generic',
|
||||||
|
createdAt: '2024-02-15T09:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// K8s Pod数据
|
||||||
|
export const k8sPods: K8sPod[] = [
|
||||||
|
{
|
||||||
|
id: 'pod-001',
|
||||||
|
name: 'nginx-deployment-7c4c8b8f5-x2k9m',
|
||||||
|
namespace: 'default',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
status: 'running',
|
||||||
|
restarts: 0,
|
||||||
|
cpu: '500m',
|
||||||
|
memory: '512Mi',
|
||||||
|
node: 'k8s-worker-01',
|
||||||
|
createdAt: '2025-03-30T08:00:00Z',
|
||||||
|
labels: { app: 'nginx', 'pod-template-hash': '7c4c8b8f5' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pod-002',
|
||||||
|
name: 'nginx-deployment-7c4c8b8f5-a3b7c',
|
||||||
|
namespace: 'default',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
status: 'running',
|
||||||
|
restarts: 0,
|
||||||
|
cpu: '500m',
|
||||||
|
memory: '512Mi',
|
||||||
|
node: 'k8s-worker-02',
|
||||||
|
createdAt: '2025-03-30T08:00:00Z',
|
||||||
|
labels: { app: 'nginx', 'pod-template-hash': '7c4c8b8f5' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pod-003',
|
||||||
|
name: 'redis-master-0',
|
||||||
|
namespace: 'default',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
status: 'running',
|
||||||
|
restarts: 1,
|
||||||
|
cpu: '1000m',
|
||||||
|
memory: '2Gi',
|
||||||
|
node: 'k8s-worker-01',
|
||||||
|
createdAt: '2025-03-29T10:00:00Z',
|
||||||
|
labels: { app: 'redis', 'statefulset.kubernetes.io/pod-name': 'redis-master-0' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pod-004',
|
||||||
|
name: 'mysql-0',
|
||||||
|
namespace: 'database',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
status: 'running',
|
||||||
|
restarts: 0,
|
||||||
|
cpu: '2000m',
|
||||||
|
memory: '4Gi',
|
||||||
|
node: 'k8s-worker-03',
|
||||||
|
createdAt: '2025-03-28T14:00:00Z',
|
||||||
|
labels: { app: 'mysql', 'statefulset.kubernetes.io/pod-name': 'mysql-0' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pod-005',
|
||||||
|
name: 'app-backend-5d8f7c6b4f-x9k2m',
|
||||||
|
namespace: 'production',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
status: 'pending',
|
||||||
|
restarts: 0,
|
||||||
|
cpu: '1000m',
|
||||||
|
memory: '2Gi',
|
||||||
|
node: 'Pending',
|
||||||
|
createdAt: '2025-03-30T09:30:00Z',
|
||||||
|
labels: { app: 'backend', 'pod-template-hash': '5d8f7c6b4f' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// K8s Deployment数据
|
||||||
|
export const k8sDeployments = [
|
||||||
|
{
|
||||||
|
id: 'deploy-001',
|
||||||
|
name: 'nginx-deployment',
|
||||||
|
namespace: 'default',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
replicas: 3,
|
||||||
|
availableReplicas: 3,
|
||||||
|
image: 'nginx:1.25.3',
|
||||||
|
createdAt: '2025-03-28T10:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'deploy-002',
|
||||||
|
name: 'app-backend',
|
||||||
|
namespace: 'production',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
replicas: 5,
|
||||||
|
availableReplicas: 4,
|
||||||
|
image: 'myapp/backend:v2.3.1',
|
||||||
|
createdAt: '2025-03-29T14:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'deploy-003',
|
||||||
|
name: 'app-frontend',
|
||||||
|
namespace: 'production',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
replicas: 2,
|
||||||
|
availableReplicas: 2,
|
||||||
|
image: 'myapp/frontend:v1.8.0',
|
||||||
|
createdAt: '2025-03-25T09:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'deploy-004',
|
||||||
|
name: 'redis-master',
|
||||||
|
namespace: 'default',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
replicas: 1,
|
||||||
|
availableReplicas: 1,
|
||||||
|
image: 'redis:7.0.12',
|
||||||
|
createdAt: '2025-03-20T08:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'deploy-005',
|
||||||
|
name: 'api-gateway',
|
||||||
|
namespace: 'infrastructure',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
replicas: 2,
|
||||||
|
availableReplicas: 2,
|
||||||
|
image: 'kong:3.4.0',
|
||||||
|
createdAt: '2025-03-15T10:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// K8s Service数据
|
||||||
|
export const k8sServices = [
|
||||||
|
{
|
||||||
|
id: 'svc-001',
|
||||||
|
name: 'nginx-service',
|
||||||
|
namespace: 'default',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
type: 'ClusterIP',
|
||||||
|
clusterIP: '10.96.123.45',
|
||||||
|
ports: '80:8080/TCP',
|
||||||
|
selector: 'app=nginx',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'svc-002',
|
||||||
|
name: 'backend-service',
|
||||||
|
namespace: 'production',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
type: 'ClusterIP',
|
||||||
|
clusterIP: '10.96.234.56',
|
||||||
|
ports: '8080:8080/TCP',
|
||||||
|
selector: 'app=backend',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'svc-003',
|
||||||
|
name: 'frontend-service',
|
||||||
|
namespace: 'production',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
type: 'NodePort',
|
||||||
|
clusterIP: '10.96.345.67',
|
||||||
|
ports: '80:30080/TCP',
|
||||||
|
selector: 'app=frontend',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'svc-004',
|
||||||
|
name: 'redis-service',
|
||||||
|
namespace: 'default',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
type: 'ClusterIP',
|
||||||
|
clusterIP: '10.96.456.78',
|
||||||
|
ports: '6379:6379/TCP',
|
||||||
|
selector: 'app=redis',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'svc-005',
|
||||||
|
name: 'mysql-service',
|
||||||
|
namespace: 'database',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
type: 'ClusterIP',
|
||||||
|
clusterIP: '10.96.567.89',
|
||||||
|
ports: '3306:3306/TCP',
|
||||||
|
selector: 'app=mysql',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'svc-006',
|
||||||
|
name: 'api-gateway',
|
||||||
|
namespace: 'infrastructure',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
type: 'LoadBalancer',
|
||||||
|
clusterIP: '10.96.678.90',
|
||||||
|
ports: '80:80/TCP,443:443/TCP',
|
||||||
|
selector: 'app=api-gateway',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// K8s Ingress数据
|
||||||
|
export const k8sIngresses = [
|
||||||
|
{
|
||||||
|
id: 'ing-001',
|
||||||
|
name: 'app-ingress',
|
||||||
|
namespace: 'production',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
hosts: 'app.company.com',
|
||||||
|
backend: 'frontend-service:80',
|
||||||
|
tls: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ing-002',
|
||||||
|
name: 'api-ingress',
|
||||||
|
namespace: 'production',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
hosts: 'api.company.com',
|
||||||
|
backend: 'backend-service:8080',
|
||||||
|
tls: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ing-003',
|
||||||
|
name: 'grafana-ingress',
|
||||||
|
namespace: 'monitoring',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
hosts: 'grafana.company.com',
|
||||||
|
backend: 'grafana-service:3000',
|
||||||
|
tls: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ing-004',
|
||||||
|
name: 'kibana-ingress',
|
||||||
|
namespace: 'logging',
|
||||||
|
clusterId: 'k8s-001',
|
||||||
|
hosts: 'kibana.company.com',
|
||||||
|
backend: 'kibana-service:5601',
|
||||||
|
tls: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 告警规则数据
|
||||||
|
export const topologyNodes: TopologyNode[] = [
|
||||||
|
{
|
||||||
|
id: 'node-001',
|
||||||
|
name: '负载均衡',
|
||||||
|
type: 'loadbalancer',
|
||||||
|
x: 400,
|
||||||
|
y: 50,
|
||||||
|
data: { ip: '192.168.1.1', vendor: 'F5' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node-002',
|
||||||
|
name: 'Web集群',
|
||||||
|
type: 'cluster',
|
||||||
|
x: 200,
|
||||||
|
y: 150,
|
||||||
|
data: { instanceCount: 3 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node-003',
|
||||||
|
name: 'App集群',
|
||||||
|
type: 'cluster',
|
||||||
|
x: 600,
|
||||||
|
y: 150,
|
||||||
|
data: { instanceCount: 3 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node-004',
|
||||||
|
name: '数据库主库',
|
||||||
|
type: 'database',
|
||||||
|
x: 400,
|
||||||
|
y: 300,
|
||||||
|
data: { type: 'MySQL', role: 'master' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node-005',
|
||||||
|
name: '数据库从库',
|
||||||
|
type: 'database',
|
||||||
|
x: 600,
|
||||||
|
y: 300,
|
||||||
|
data: { type: 'MySQL', role: 'slave' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node-006',
|
||||||
|
name: 'Redis缓存',
|
||||||
|
type: 'cache',
|
||||||
|
x: 200,
|
||||||
|
y: 300,
|
||||||
|
data: { type: 'Redis', mode: 'cluster' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const topologyEdges: TopologyEdge[] = [
|
||||||
|
{ id: 'edge-001', source: 'node-001', target: 'node-002', label: 'HTTP' },
|
||||||
|
{ id: 'edge-002', source: 'node-001', target: 'node-003', label: 'HTTP' },
|
||||||
|
{ id: 'edge-003', source: 'node-002', target: 'node-003', label: 'RPC' },
|
||||||
|
{ id: 'edge-004', source: 'node-002', target: 'node-006', label: 'Redis' },
|
||||||
|
{ id: 'edge-005', source: 'node-003', target: 'node-004', label: 'SQL' },
|
||||||
|
{ id: 'edge-006', source: 'node-004', target: 'node-005', label: 'Replication' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ==================== 监控指标数据 ====================
|
||||||
|
|
||||||
@@ -0,0 +1,479 @@
|
|||||||
|
import type {
|
||||||
|
Server,
|
||||||
|
K8sCluster,
|
||||||
|
K8sNode,
|
||||||
|
K8sPod,
|
||||||
|
AlertRule,
|
||||||
|
AlertEvent,
|
||||||
|
Ticket,
|
||||||
|
ApprovalFlow,
|
||||||
|
ApprovalInstance,
|
||||||
|
AppRelease,
|
||||||
|
User,
|
||||||
|
Role,
|
||||||
|
OperationLog,
|
||||||
|
CronJob,
|
||||||
|
Script,
|
||||||
|
DashboardData,
|
||||||
|
ResourceModel,
|
||||||
|
ModelInstance,
|
||||||
|
Cabinet,
|
||||||
|
Device,
|
||||||
|
TopologyNode,
|
||||||
|
TopologyEdge,
|
||||||
|
MetricData,
|
||||||
|
LogEntry,
|
||||||
|
} from '@/types';
|
||||||
|
|
||||||
|
// 仪表盘数据
|
||||||
|
|
||||||
|
export const resourceModels: ResourceModel[] = [
|
||||||
|
{
|
||||||
|
id: 'model-001',
|
||||||
|
name: '服务器',
|
||||||
|
code: 'server',
|
||||||
|
description: '物理服务器和虚拟机',
|
||||||
|
type: 'normal',
|
||||||
|
attributes: [
|
||||||
|
{ id: 'attr-001', name: '主机名', code: 'hostname', type: 'string', required: true, unique: true },
|
||||||
|
{ id: 'attr-002', name: 'IP地址', code: 'ip', type: 'string', required: true, unique: true },
|
||||||
|
{ id: 'attr-003', name: '操作系统', code: 'os', type: 'string', required: false },
|
||||||
|
{ id: 'attr-004', name: 'CPU核数', code: 'cpu', type: 'number', required: false },
|
||||||
|
{ id: 'attr-005', name: '内存大小', code: 'memory', type: 'number', required: false },
|
||||||
|
{ id: 'attr-006', name: '磁盘容量', code: 'disk', type: 'number', required: false },
|
||||||
|
],
|
||||||
|
instanceCount: 156,
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'model-002',
|
||||||
|
name: '网络设备',
|
||||||
|
code: 'network_device',
|
||||||
|
description: '交换机、路由器等网络设备',
|
||||||
|
type: 'normal',
|
||||||
|
attributes: [
|
||||||
|
{ id: 'attr-007', name: '设备名称', code: 'name', type: 'string', required: true, unique: true },
|
||||||
|
{ id: 'attr-008', name: '管理IP', code: 'mgmt_ip', type: 'string', required: true },
|
||||||
|
{ id: 'attr-009', name: '设备类型', code: 'device_type', type: 'enum', required: true },
|
||||||
|
{ id: 'attr-010', name: '厂商', code: 'vendor', type: 'string', required: false },
|
||||||
|
{ id: 'attr-011', name: '型号', code: 'model', type: 'string', required: false },
|
||||||
|
],
|
||||||
|
instanceCount: 48,
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'model-003',
|
||||||
|
name: '硬件设备',
|
||||||
|
code: 'hardware',
|
||||||
|
description: '所有硬件设备的父模型',
|
||||||
|
type: 'parent',
|
||||||
|
attributes: [
|
||||||
|
{ id: 'attr-012', name: '资产编号', code: 'asset_no', type: 'string', required: true, unique: true },
|
||||||
|
{ id: 'attr-013', name: '序列号', code: 'sn', type: 'string', required: false },
|
||||||
|
{ id: 'attr-014', name: '厂商', code: 'vendor', type: 'string', required: false },
|
||||||
|
{ id: 'attr-015', name: '购买日期', code: 'purchase_date', type: 'date', required: false },
|
||||||
|
{ id: 'attr-016', name: '保修期', code: 'warranty', type: 'number', required: false },
|
||||||
|
],
|
||||||
|
instanceCount: 0,
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'model-004',
|
||||||
|
name: '应用系统',
|
||||||
|
code: 'application',
|
||||||
|
description: '业务应用系统',
|
||||||
|
type: 'normal',
|
||||||
|
attributes: [
|
||||||
|
{ id: 'attr-017', name: '应用名称', code: 'name', type: 'string', required: true, unique: true },
|
||||||
|
{ id: 'attr-018', name: '应用代码', code: 'code', type: 'string', required: true, unique: true },
|
||||||
|
{ id: 'attr-019', name: '负责人', code: 'owner', type: 'string', required: true },
|
||||||
|
{ id: 'attr-020', name: '所属部门', code: 'department', type: 'string', required: false },
|
||||||
|
],
|
||||||
|
instanceCount: 32,
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 模型实例数据
|
||||||
|
export const modelInstances: ModelInstance[] = [
|
||||||
|
{
|
||||||
|
id: 'inst-001',
|
||||||
|
modelId: 'model-001',
|
||||||
|
name: 'web-server-01',
|
||||||
|
data: {
|
||||||
|
hostname: 'web-server-01',
|
||||||
|
ip: '192.168.1.101',
|
||||||
|
os: 'CentOS 7.9',
|
||||||
|
cpu: 16,
|
||||||
|
memory: 64,
|
||||||
|
disk: 500,
|
||||||
|
},
|
||||||
|
relations: [
|
||||||
|
{ targetModelId: 'model-004', targetInstanceId: 'inst-101', relationType: 'runs' },
|
||||||
|
],
|
||||||
|
createdAt: '2024-01-15T08:30:00Z',
|
||||||
|
updatedAt: '2025-03-30T10:25:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'inst-002',
|
||||||
|
modelId: 'model-001',
|
||||||
|
name: 'db-server-01',
|
||||||
|
data: {
|
||||||
|
hostname: 'db-server-01',
|
||||||
|
ip: '192.168.1.105',
|
||||||
|
os: 'CentOS 8.5',
|
||||||
|
cpu: 64,
|
||||||
|
memory: 256,
|
||||||
|
disk: 2000,
|
||||||
|
},
|
||||||
|
relations: [],
|
||||||
|
createdAt: '2024-01-10T07:00:00Z',
|
||||||
|
updatedAt: '2025-03-30T10:25:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ==================== 机房机柜数据 ====================
|
||||||
|
|
||||||
|
export const navigationGroups = [
|
||||||
|
{
|
||||||
|
id: 'group-001',
|
||||||
|
name: '运维工具',
|
||||||
|
description: '日常运维常用工具',
|
||||||
|
linkCount: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'group-002',
|
||||||
|
name: '监控告警',
|
||||||
|
description: '监控系统和告警平台',
|
||||||
|
linkCount: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'group-003',
|
||||||
|
name: '开发工具',
|
||||||
|
description: '开发相关工具和平台',
|
||||||
|
linkCount: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'group-004',
|
||||||
|
name: '文档知识库',
|
||||||
|
description: '技术文档和知识库',
|
||||||
|
linkCount: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'group-005',
|
||||||
|
name: '云平台',
|
||||||
|
description: '各大云厂商控制台',
|
||||||
|
linkCount: 5,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const navigationLinks = [
|
||||||
|
// 运维工具
|
||||||
|
{
|
||||||
|
id: 'link-001',
|
||||||
|
name: 'Jenkins',
|
||||||
|
url: 'https://jenkins.company.com',
|
||||||
|
description: 'CI/CD流水线平台',
|
||||||
|
groupId: 'group-001',
|
||||||
|
isFavorite: true,
|
||||||
|
tags: ['CI/CD', 'DevOps'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'link-002',
|
||||||
|
name: 'GitLab',
|
||||||
|
url: 'https://gitlab.company.com',
|
||||||
|
description: '代码仓库管理平台',
|
||||||
|
groupId: 'group-001',
|
||||||
|
isFavorite: true,
|
||||||
|
tags: ['Git', '代码仓库'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'link-003',
|
||||||
|
name: 'Harbor',
|
||||||
|
url: 'https://harbor.company.com',
|
||||||
|
description: 'Docker镜像仓库',
|
||||||
|
groupId: 'group-001',
|
||||||
|
isFavorite: false,
|
||||||
|
tags: ['Docker', '镜像'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'link-004',
|
||||||
|
name: 'Nexus',
|
||||||
|
url: 'https://nexus.company.com',
|
||||||
|
description: 'Maven/npm仓库',
|
||||||
|
groupId: 'group-001',
|
||||||
|
isFavorite: false,
|
||||||
|
tags: ['Maven', 'npm'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'link-005',
|
||||||
|
name: 'Confluence',
|
||||||
|
url: 'https://confluence.company.com',
|
||||||
|
description: '团队协作文档平台',
|
||||||
|
groupId: 'group-001',
|
||||||
|
isFavorite: true,
|
||||||
|
tags: ['文档', 'Wiki'],
|
||||||
|
},
|
||||||
|
// 监控告警
|
||||||
|
{
|
||||||
|
id: 'link-006',
|
||||||
|
name: 'Prometheus',
|
||||||
|
url: 'https://prometheus.company.com',
|
||||||
|
description: '监控指标采集和查询',
|
||||||
|
groupId: 'group-002',
|
||||||
|
isFavorite: true,
|
||||||
|
tags: ['监控', '指标'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'link-007',
|
||||||
|
name: 'Grafana',
|
||||||
|
url: 'https://grafana.company.com',
|
||||||
|
description: '可视化监控大盘',
|
||||||
|
groupId: 'group-002',
|
||||||
|
isFavorite: true,
|
||||||
|
tags: ['监控', '可视化'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'link-008',
|
||||||
|
name: 'AlertManager',
|
||||||
|
url: 'https://alertmanager.company.com',
|
||||||
|
description: '告警管理和通知',
|
||||||
|
groupId: 'group-002',
|
||||||
|
isFavorite: false,
|
||||||
|
tags: ['告警', '通知'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'link-009',
|
||||||
|
name: 'ELK Stack',
|
||||||
|
url: 'https://kibana.company.com',
|
||||||
|
description: '日志收集和分析平台',
|
||||||
|
groupId: 'group-002',
|
||||||
|
isFavorite: false,
|
||||||
|
tags: ['日志', 'ELK'],
|
||||||
|
},
|
||||||
|
// 开发工具
|
||||||
|
{
|
||||||
|
id: 'link-010',
|
||||||
|
name: 'Jira',
|
||||||
|
url: 'https://jira.company.com',
|
||||||
|
description: '项目管理和任务跟踪',
|
||||||
|
groupId: 'group-003',
|
||||||
|
isFavorite: true,
|
||||||
|
tags: ['项目管理', '敏捷'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'link-011',
|
||||||
|
name: 'SonarQube',
|
||||||
|
url: 'https://sonarqube.company.com',
|
||||||
|
description: '代码质量分析平台',
|
||||||
|
groupId: 'group-003',
|
||||||
|
isFavorite: false,
|
||||||
|
tags: ['代码质量', '扫描'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'link-012',
|
||||||
|
name: 'Nexus',
|
||||||
|
url: 'https://nexus.company.com',
|
||||||
|
description: '制品仓库管理',
|
||||||
|
groupId: 'group-003',
|
||||||
|
isFavorite: false,
|
||||||
|
tags: ['制品', '仓库'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'link-013',
|
||||||
|
name: 'Swagger UI',
|
||||||
|
url: 'https://swagger.company.com',
|
||||||
|
description: 'API文档和测试',
|
||||||
|
groupId: 'group-003',
|
||||||
|
isFavorite: false,
|
||||||
|
tags: ['API', '文档'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'link-014',
|
||||||
|
name: 'YApi',
|
||||||
|
url: 'https://yapi.company.com',
|
||||||
|
description: 'API接口管理平台',
|
||||||
|
groupId: 'group-003',
|
||||||
|
isFavorite: false,
|
||||||
|
tags: ['API', '接口管理'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'link-015',
|
||||||
|
name: 'Postman',
|
||||||
|
url: 'https://web.postman.co',
|
||||||
|
description: 'API测试工具',
|
||||||
|
groupId: 'group-003',
|
||||||
|
isFavorite: true,
|
||||||
|
tags: ['API', '测试'],
|
||||||
|
},
|
||||||
|
// 文档知识库
|
||||||
|
{
|
||||||
|
id: 'link-016',
|
||||||
|
name: 'Confluence',
|
||||||
|
url: 'https://confluence.company.com',
|
||||||
|
description: '团队知识库',
|
||||||
|
groupId: 'group-004',
|
||||||
|
isFavorite: true,
|
||||||
|
tags: ['文档', '知识库'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'link-017',
|
||||||
|
name: 'Notion',
|
||||||
|
url: 'https://notion.so',
|
||||||
|
description: '个人和团队笔记',
|
||||||
|
groupId: 'group-004',
|
||||||
|
isFavorite: true,
|
||||||
|
tags: ['笔记', '文档'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'link-018',
|
||||||
|
name: '语雀',
|
||||||
|
url: 'https://yuque.com',
|
||||||
|
description: '阿里知识库平台',
|
||||||
|
groupId: 'group-004',
|
||||||
|
isFavorite: false,
|
||||||
|
tags: ['文档', '知识库'],
|
||||||
|
},
|
||||||
|
// 云平台
|
||||||
|
{
|
||||||
|
id: 'link-019',
|
||||||
|
name: '阿里云控制台',
|
||||||
|
url: 'https://ecs.console.aliyun.com',
|
||||||
|
description: '阿里云ECS管理',
|
||||||
|
groupId: 'group-005',
|
||||||
|
isFavorite: true,
|
||||||
|
tags: ['阿里云', 'ECS'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'link-020',
|
||||||
|
name: '腾讯云控制台',
|
||||||
|
url: 'https://console.cloud.tencent.com',
|
||||||
|
description: '腾讯云CVM管理',
|
||||||
|
groupId: 'group-005',
|
||||||
|
isFavorite: false,
|
||||||
|
tags: ['腾讯云', 'CVM'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'link-021',
|
||||||
|
name: 'AWS控制台',
|
||||||
|
url: 'https://console.aws.amazon.com',
|
||||||
|
description: 'AWS EC2管理',
|
||||||
|
groupId: 'group-005',
|
||||||
|
isFavorite: true,
|
||||||
|
tags: ['AWS', 'EC2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'link-022',
|
||||||
|
name: '华为云控制台',
|
||||||
|
url: 'https://console.huaweicloud.com',
|
||||||
|
description: '华为云ECS管理',
|
||||||
|
groupId: 'group-005',
|
||||||
|
isFavorite: false,
|
||||||
|
tags: ['华为云', 'ECS'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'link-023',
|
||||||
|
name: 'Azure门户',
|
||||||
|
url: 'https://portal.azure.com',
|
||||||
|
description: 'Azure虚拟机管理',
|
||||||
|
groupId: 'group-005',
|
||||||
|
isFavorite: false,
|
||||||
|
tags: ['Azure', 'VM'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ============ SQL审计数据 ============
|
||||||
|
export const sqlAuditRecords = [
|
||||||
|
{
|
||||||
|
id: 'audit-001',
|
||||||
|
sqlContent: 'UPDATE users SET status = "inactive" WHERE last_login < "2024-01-01"',
|
||||||
|
sqlType: 'UPDATE',
|
||||||
|
databaseName: 'production_db',
|
||||||
|
tableName: 'users',
|
||||||
|
riskLevel: 'medium',
|
||||||
|
riskRules: ['批量更新', '无WHERE条件限制'],
|
||||||
|
status: 'pending',
|
||||||
|
executor: '张工程师',
|
||||||
|
createdAt: '2025-04-14 10:30:00',
|
||||||
|
sourceIP: '192.168.1.100',
|
||||||
|
application: 'user-management-service'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-002',
|
||||||
|
sqlContent: 'DELETE FROM logs WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)',
|
||||||
|
sqlType: 'DELETE',
|
||||||
|
databaseName: 'production_db',
|
||||||
|
tableName: 'logs',
|
||||||
|
riskLevel: 'high',
|
||||||
|
riskRules: ['批量删除', '影响大量数据'],
|
||||||
|
status: 'approved',
|
||||||
|
executor: '李运维',
|
||||||
|
approver: '王经理',
|
||||||
|
executeTime: '2025-04-14 09:00:00',
|
||||||
|
createdAt: '2025-04-14 08:30:00',
|
||||||
|
affectedRows: 500000,
|
||||||
|
executionTime: 12500,
|
||||||
|
sourceIP: '192.168.1.101',
|
||||||
|
application: 'log-cleanup-service'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-003',
|
||||||
|
sqlContent: 'SELECT * FROM orders WHERE user_id = 12345',
|
||||||
|
sqlType: 'SELECT',
|
||||||
|
databaseName: 'production_db',
|
||||||
|
tableName: 'orders',
|
||||||
|
riskLevel: 'safe',
|
||||||
|
riskRules: [],
|
||||||
|
status: 'executed',
|
||||||
|
executor: '王开发',
|
||||||
|
executeTime: '2025-04-14 10:15:00',
|
||||||
|
createdAt: '2025-04-14 10:15:00',
|
||||||
|
affectedRows: 0,
|
||||||
|
executionTime: 45,
|
||||||
|
sourceIP: '192.168.1.102',
|
||||||
|
application: 'order-service'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-004',
|
||||||
|
sqlContent: 'ALTER TABLE users ADD COLUMN phone_verified BOOLEAN DEFAULT FALSE',
|
||||||
|
sqlType: 'DDL',
|
||||||
|
databaseName: 'production_db',
|
||||||
|
tableName: 'users',
|
||||||
|
riskLevel: 'high',
|
||||||
|
riskRules: ['DDL操作', '表结构变更'],
|
||||||
|
status: 'pending',
|
||||||
|
executor: '张工程师',
|
||||||
|
createdAt: '2025-04-14 11:00:00',
|
||||||
|
sourceIP: '192.168.1.100',
|
||||||
|
application: 'db-migration-tool'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit-005',
|
||||||
|
sqlContent: 'DROP TABLE temp_backup_2024',
|
||||||
|
sqlType: 'DDL',
|
||||||
|
databaseName: 'production_db',
|
||||||
|
tableName: 'temp_backup_2024',
|
||||||
|
riskLevel: 'high',
|
||||||
|
riskRules: ['删除表', '高危操作'],
|
||||||
|
status: 'rejected',
|
||||||
|
executor: '李运维',
|
||||||
|
createdAt: '2025-04-14 09:30:00',
|
||||||
|
sourceIP: '192.168.1.101',
|
||||||
|
application: 'cleanup-script'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const sqlAuditRules = [
|
||||||
|
{ id: 'rule-001', name: '禁止无WHERE条件的UPDATE', description: '检测UPDATE语句是否包含WHERE条件', enabled: true, severity: 'high', category: 'DML' },
|
||||||
|
{ id: 'rule-002', name: '禁止无WHERE条件的DELETE', description: '检测DELETE语句是否包含WHERE条件', enabled: true, severity: 'high', category: 'DML' },
|
||||||
|
{ id: 'rule-003', name: '限制批量更新影响行数', description: 'UPDATE/DELETE影响行数超过1000条需要审批', enabled: true, severity: 'medium', category: 'DML' },
|
||||||
|
{ id: 'rule-004', name: 'DDL操作需要审批', description: 'ALTER、DROP等DDL操作需要高级审批', enabled: true, severity: 'high', category: 'DDL' },
|
||||||
|
{ id: 'rule-005', name: '禁止SELECT *', description: '建议明确指定查询字段', enabled: false, severity: 'low', category: 'DQL' },
|
||||||
|
{ id: 'rule-006', name: '敏感表操作审计', description: '对用户表、订单表等敏感表的操作进行审计', enabled: true, severity: 'medium', category: 'AUDIT' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============ Ansible数据 ============
|
||||||
|
// mockData aggregate moved to data.ts barrel (references cross-module exports)
|
||||||
|
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import type {
|
||||||
|
Server,
|
||||||
|
K8sCluster,
|
||||||
|
K8sNode,
|
||||||
|
K8sPod,
|
||||||
|
AlertRule,
|
||||||
|
AlertEvent,
|
||||||
|
Ticket,
|
||||||
|
ApprovalFlow,
|
||||||
|
ApprovalInstance,
|
||||||
|
AppRelease,
|
||||||
|
User,
|
||||||
|
Role,
|
||||||
|
OperationLog,
|
||||||
|
CronJob,
|
||||||
|
Script,
|
||||||
|
DashboardData,
|
||||||
|
ResourceModel,
|
||||||
|
ModelInstance,
|
||||||
|
Cabinet,
|
||||||
|
Device,
|
||||||
|
TopologyNode,
|
||||||
|
TopologyEdge,
|
||||||
|
MetricData,
|
||||||
|
LogEntry,
|
||||||
|
} from '@/types';
|
||||||
|
|
||||||
|
// 仪表盘数据
|
||||||
|
|
||||||
|
export const logEntries: LogEntry[] = [
|
||||||
|
{
|
||||||
|
id: 'log-001',
|
||||||
|
timestamp: '2025-03-30T10:25:00Z',
|
||||||
|
level: 'INFO',
|
||||||
|
source: 'nginx',
|
||||||
|
message: '192.168.1.100 - - [30/Mar/2025:10:25:00 +0800] "GET /api/users HTTP/1.1" 200 1234',
|
||||||
|
host: 'web-server-01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'log-002',
|
||||||
|
timestamp: '2025-03-30T10:24:55Z',
|
||||||
|
level: 'WARN',
|
||||||
|
source: 'app',
|
||||||
|
message: 'Database connection pool reaching limit: 45/50',
|
||||||
|
host: 'app-server-01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'log-003',
|
||||||
|
timestamp: '2025-03-30T10:24:50Z',
|
||||||
|
level: 'ERROR',
|
||||||
|
source: 'mysql',
|
||||||
|
message: 'Connection timeout after 30000ms',
|
||||||
|
host: 'db-server-01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'log-004',
|
||||||
|
timestamp: '2025-03-30T10:24:45Z',
|
||||||
|
level: 'INFO',
|
||||||
|
source: 'k8s',
|
||||||
|
message: 'Pod nginx-deployment-xxx created successfully',
|
||||||
|
host: 'k8s-master-01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'log-005',
|
||||||
|
timestamp: '2025-03-30T10:24:40Z',
|
||||||
|
level: 'DEBUG',
|
||||||
|
source: 'cache',
|
||||||
|
message: 'Cache hit ratio: 0.85',
|
||||||
|
host: 'cache-server-01',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 生成监控数据
|
||||||
|
export const logMonitorEntries = [
|
||||||
|
{ id: 'log-001', timestamp: '2025-04-14 10:30:15.234', level: 'error', service: 'order-service', host: 'app-server-01', message: 'Database connection timeout', traceId: 'trace-abc123', metadata: { error_code: 'DB_TIMEOUT' }, raw: 'ERROR [order-service] Database connection timeout' },
|
||||||
|
{ id: 'log-002', timestamp: '2025-04-14 10:30:14.123', level: 'info', service: 'user-service', host: 'app-server-01', message: 'User login successful', traceId: 'trace-ghi789', metadata: { user_id: '12345' }, raw: 'INFO [user-service] User login successful' },
|
||||||
|
{ id: 'log-003', timestamp: '2025-04-14 10:30:12.456', level: 'warn', service: 'payment-service', host: 'app-server-02', message: 'Payment processing slow', traceId: 'trace-mno345', metadata: { duration_ms: 2500 }, raw: 'WARN [payment-service] Payment processing slow' },
|
||||||
|
{ id: 'log-004', timestamp: '2025-04-14 10:30:10.789', level: 'debug', service: 'inventory-service', host: 'app-server-01', message: 'Cache hit', metadata: {}, raw: 'DEBUG [inventory-service] Cache hit' },
|
||||||
|
{ id: 'log-005', timestamp: '2025-04-14 10:30:08.321', level: 'fatal', service: 'notification-service', host: 'app-server-02', message: 'OutOfMemoryError', traceId: 'trace-stu901', metadata: {}, raw: 'FATAL [notification-service] OutOfMemoryError' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const logSources = [
|
||||||
|
{ id: 'src-001', name: '应用日志', type: 'application', status: 'connected', lastSeen: '2025-04-14 10:30:00', logCount: 1523456 },
|
||||||
|
{ id: 'src-002', name: '系统日志', type: 'syslog', status: 'connected', lastSeen: '2025-04-14 10:30:00', logCount: 892345 },
|
||||||
|
{ id: 'src-003', name: 'Docker容器日志', type: 'docker', status: 'connected', lastSeen: '2025-04-14 10:29:55', logCount: 567890 },
|
||||||
|
{ id: 'src-004', name: 'Kubernetes日志', type: 'kubernetes', status: 'connected', lastSeen: '2025-04-14 10:30:00', logCount: 1234567 },
|
||||||
|
{ id: 'src-005', name: 'Nginx访问日志', type: 'file', status: 'error', lastSeen: '2025-04-14 09:00:00', logCount: 2345678 }
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============ 告警中心数据 ============
|
||||||
|
export const traces = [
|
||||||
|
{ id: 'trace-001', traceId: 'abc123def456ghi789', serviceName: 'api-gateway', operationName: 'POST /api/orders', startTime: '2025-04-14 10:30:15.234', duration: 1250, status: 'success', spanCount: 12, tags: { http_method: 'POST' } },
|
||||||
|
{ id: 'trace-002', traceId: 'jkl012mno345pqr678', serviceName: 'user-service', operationName: 'GET /api/users/profile', startTime: '2025-04-14 10:30:14.123', duration: 45, status: 'success', spanCount: 5, tags: {} },
|
||||||
|
{ id: 'trace-003', traceId: 'stu901vwx234yza567', serviceName: 'payment-service', operationName: 'POST /api/payments', startTime: '2025-04-14 10:30:12.456', duration: 2500, status: 'error', spanCount: 8, tags: { error: 'timeout' } },
|
||||||
|
{ id: 'trace-004', traceId: 'bcd234efg567hij890', serviceName: 'inventory-service', operationName: 'GET /api/inventory/check', startTime: '2025-04-14 10:30:10.789', duration: 15, status: 'success', spanCount: 3, tags: {} }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const serviceMetrics = [
|
||||||
|
{ serviceName: 'api-gateway', requestCount: 152345, errorRate: 0.5, avgDuration: 45, p95Duration: 120, p99Duration: 250 },
|
||||||
|
{ serviceName: 'user-service', requestCount: 89345, errorRate: 0.2, avgDuration: 25, p95Duration: 60, p99Duration: 120 },
|
||||||
|
{ serviceName: 'order-service', requestCount: 67890, errorRate: 1.2, avgDuration: 150, p95Duration: 350, p99Duration: 800 },
|
||||||
|
{ serviceName: 'payment-service', requestCount: 45678, errorRate: 2.5, avgDuration: 200, p95Duration: 500, p99Duration: 1200 },
|
||||||
|
{ serviceName: 'inventory-service', requestCount: 123456, errorRate: 0.1, avgDuration: 15, p95Duration: 35, p99Duration: 80 },
|
||||||
|
{ serviceName: 'notification-service', requestCount: 34567, errorRate: 0.8, avgDuration: 80, p95Duration: 200, p99Duration: 450 }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const traceSpans = [
|
||||||
|
{ id: 'span-001', spanId: 'span-abc123', serviceName: 'api-gateway', operationName: 'POST /api/orders', startTime: '2025-04-14 10:30:15.234', duration: 1250, status: 'success', kind: 'server', tags: {}, logs: [], children: [
|
||||||
|
{ id: 'span-002', spanId: 'span-def456', parentSpanId: 'span-abc123', serviceName: 'order-service', operationName: 'CreateOrder', startTime: '2025-04-14 10:30:15.250', duration: 800, status: 'success', kind: 'client', tags: {}, logs: [], children: [
|
||||||
|
{ id: 'span-003', spanId: 'span-ghi789', parentSpanId: 'span-def456', serviceName: 'order-service', operationName: 'INSERT orders', startTime: '2025-04-14 10:30:15.300', duration: 150, status: 'success', kind: 'internal', tags: {}, logs: [], children: [] }
|
||||||
|
] },
|
||||||
|
{ id: 'span-004', spanId: 'span-jkl012', parentSpanId: 'span-abc123', serviceName: 'inventory-service', operationName: 'CheckInventory', startTime: '2025-04-14 10:30:15.280', duration: 50, status: 'success', kind: 'client', tags: {}, logs: [], children: [] }
|
||||||
|
] }
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============ Grafana大屏数据 ============
|
||||||
|
export const grafanaDashboards = [
|
||||||
|
{
|
||||||
|
id: 'dash-001',
|
||||||
|
name: '系统监控大盘',
|
||||||
|
description: '整体系统资源使用情况监控',
|
||||||
|
datasource: 'prometheus',
|
||||||
|
datasourceUrl: 'http://prometheus:9090',
|
||||||
|
panels: [
|
||||||
|
{ id: 'p-001', title: 'CPU使用率', type: 'gauge', query: 'avg(cpu_usage_percent)', datasource: 'prometheus', width: 3, height: 4, x: 0, y: 0, options: { unit: '%', min: 0, max: 100 } },
|
||||||
|
{ id: 'p-002', title: '内存使用率', type: 'gauge', query: 'avg(memory_usage_percent)', datasource: 'prometheus', width: 3, height: 4, x: 3, y: 0, options: { unit: '%', min: 0, max: 100 } },
|
||||||
|
{ id: 'p-003', title: '磁盘使用率', type: 'gauge', query: 'avg(disk_usage_percent)', datasource: 'prometheus', width: 3, height: 4, x: 6, y: 0, options: { unit: '%', min: 0, max: 100 } },
|
||||||
|
{ id: 'p-004', title: '网络流量', type: 'graph', query: 'rate(network_bytes_total[5m])', datasource: 'prometheus', width: 6, height: 6, x: 0, y: 4, options: { unit: 'B/s' } },
|
||||||
|
{ id: 'p-005', title: '请求QPS', type: 'stat', query: 'sum(rate(http_requests_total[1m]))', datasource: 'prometheus', width: 3, height: 4, x: 9, y: 0, options: { unit: 'req/s' } }
|
||||||
|
],
|
||||||
|
createdAt: '2025-03-01',
|
||||||
|
updatedAt: '2025-04-10',
|
||||||
|
createdBy: '张工程师',
|
||||||
|
isDefault: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dash-002',
|
||||||
|
name: '应用性能监控',
|
||||||
|
description: '应用层性能指标监控',
|
||||||
|
datasource: 'skywalking',
|
||||||
|
datasourceUrl: 'http://skywalking-oap:12800',
|
||||||
|
panels: [
|
||||||
|
{ id: 'p-006', title: 'P99响应时间', type: 'graph', query: 'service_resp_time_p99', datasource: 'skywalking', width: 6, height: 6, x: 0, y: 0, options: { unit: 'ms' } },
|
||||||
|
{ id: 'p-007', title: '错误率', type: 'graph', query: 'service_sla', datasource: 'skywalking', width: 6, height: 6, x: 6, y: 0, options: { unit: '%' } },
|
||||||
|
{ id: 'p-008', title: '吞吐量', type: 'stat', query: 'service_cpm', datasource: 'skywalking', width: 4, height: 4, x: 0, y: 6, options: { unit: 'calls/min' } }
|
||||||
|
],
|
||||||
|
createdAt: '2025-03-15',
|
||||||
|
updatedAt: '2025-04-05',
|
||||||
|
createdBy: '李运维',
|
||||||
|
isDefault: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const datasources = [
|
||||||
|
{ id: 'ds-001', name: 'Prometheus生产', type: 'prometheus', url: 'http://prometheus-prod:9090', status: 'connected' },
|
||||||
|
{ id: 'ds-002', name: 'Prometheus测试', type: 'prometheus', url: 'http://prometheus-test:9090', status: 'connected' },
|
||||||
|
{ id: 'ds-003', name: 'Zabbix监控', type: 'zabbix', url: 'http://zabbix-server/api_jsonrpc.php', status: 'connected' },
|
||||||
|
{ id: 'ds-004', name: 'SkyWalking APM', type: 'skywalking', url: 'http://skywalking-oap:12800', status: 'error' },
|
||||||
|
{ id: 'ds-005', name: 'InfluxDB时序', type: 'influxdb', url: 'http://influxdb:8086', status: 'connected' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============ 流水线数据 ============
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
import type {
|
||||||
|
Server,
|
||||||
|
K8sCluster,
|
||||||
|
K8sNode,
|
||||||
|
K8sPod,
|
||||||
|
AlertRule,
|
||||||
|
AlertEvent,
|
||||||
|
Ticket,
|
||||||
|
ApprovalFlow,
|
||||||
|
ApprovalInstance,
|
||||||
|
AppRelease,
|
||||||
|
User,
|
||||||
|
Role,
|
||||||
|
OperationLog,
|
||||||
|
CronJob,
|
||||||
|
Script,
|
||||||
|
DashboardData,
|
||||||
|
ResourceModel,
|
||||||
|
ModelInstance,
|
||||||
|
Cabinet,
|
||||||
|
Device,
|
||||||
|
TopologyNode,
|
||||||
|
TopologyEdge,
|
||||||
|
MetricData,
|
||||||
|
LogEntry,
|
||||||
|
} from '@/types';
|
||||||
|
|
||||||
|
// 仪表盘数据
|
||||||
|
|
||||||
|
export const servers: Server[] = [
|
||||||
|
{
|
||||||
|
id: 'srv-001',
|
||||||
|
hostname: 'web-server-01',
|
||||||
|
ip: '192.168.1.101',
|
||||||
|
os: 'CentOS 7.9',
|
||||||
|
cpu: 16,
|
||||||
|
memory: 64,
|
||||||
|
disk: 500,
|
||||||
|
status: 'running',
|
||||||
|
tags: ['web', 'production', 'frontend'],
|
||||||
|
region: '华北-北京',
|
||||||
|
createdAt: '2024-01-15T08:30:00Z',
|
||||||
|
lastSeen: '2025-03-30T10:25:00Z',
|
||||||
|
cpuUsage: 45.2,
|
||||||
|
memoryUsage: 62.8,
|
||||||
|
diskUsage: 38.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'srv-002',
|
||||||
|
hostname: 'web-server-02',
|
||||||
|
ip: '192.168.1.102',
|
||||||
|
os: 'CentOS 7.9',
|
||||||
|
cpu: 16,
|
||||||
|
memory: 64,
|
||||||
|
disk: 500,
|
||||||
|
status: 'running',
|
||||||
|
tags: ['web', 'production', 'frontend'],
|
||||||
|
region: '华北-北京',
|
||||||
|
createdAt: '2024-01-15T08:35:00Z',
|
||||||
|
lastSeen: '2025-03-30T10:25:00Z',
|
||||||
|
cpuUsage: 38.7,
|
||||||
|
memoryUsage: 55.3,
|
||||||
|
diskUsage: 42.1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'srv-003',
|
||||||
|
hostname: 'app-server-01',
|
||||||
|
ip: '192.168.1.103',
|
||||||
|
os: 'Ubuntu 22.04 LTS',
|
||||||
|
cpu: 32,
|
||||||
|
memory: 128,
|
||||||
|
disk: 1000,
|
||||||
|
status: 'running',
|
||||||
|
tags: ['app', 'production', 'backend'],
|
||||||
|
region: '华东-上海',
|
||||||
|
createdAt: '2024-02-20T09:00:00Z',
|
||||||
|
lastSeen: '2025-03-30T10:24:00Z',
|
||||||
|
cpuUsage: 72.5,
|
||||||
|
memoryUsage: 85.6,
|
||||||
|
diskUsage: 67.8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'srv-004',
|
||||||
|
hostname: 'app-server-02',
|
||||||
|
ip: '192.168.1.104',
|
||||||
|
os: 'Ubuntu 22.04 LTS',
|
||||||
|
cpu: 32,
|
||||||
|
memory: 128,
|
||||||
|
disk: 1000,
|
||||||
|
status: 'running',
|
||||||
|
tags: ['app', 'production', 'backend'],
|
||||||
|
region: '华东-上海',
|
||||||
|
createdAt: '2024-02-20T09:05:00Z',
|
||||||
|
lastSeen: '2025-03-30T10:24:00Z',
|
||||||
|
cpuUsage: 65.3,
|
||||||
|
memoryUsage: 78.9,
|
||||||
|
diskUsage: 58.2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'srv-005',
|
||||||
|
hostname: 'db-server-01',
|
||||||
|
ip: '192.168.1.105',
|
||||||
|
os: 'CentOS 8.5',
|
||||||
|
cpu: 64,
|
||||||
|
memory: 256,
|
||||||
|
disk: 2000,
|
||||||
|
status: 'running',
|
||||||
|
tags: ['database', 'production', 'mysql'],
|
||||||
|
region: '华北-北京',
|
||||||
|
createdAt: '2024-01-10T07:00:00Z',
|
||||||
|
lastSeen: '2025-03-30T10:25:00Z',
|
||||||
|
cpuUsage: 58.9,
|
||||||
|
memoryUsage: 72.4,
|
||||||
|
diskUsage: 81.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'srv-006',
|
||||||
|
hostname: 'db-server-02',
|
||||||
|
ip: '192.168.1.106',
|
||||||
|
os: 'CentOS 8.5',
|
||||||
|
cpu: 64,
|
||||||
|
memory: 256,
|
||||||
|
disk: 2000,
|
||||||
|
status: 'maintenance',
|
||||||
|
tags: ['database', 'production', 'mysql', 'slave'],
|
||||||
|
region: '华北-北京',
|
||||||
|
createdAt: '2024-01-10T07:05:00Z',
|
||||||
|
lastSeen: '2025-03-30T09:30:00Z',
|
||||||
|
cpuUsage: 0,
|
||||||
|
memoryUsage: 0,
|
||||||
|
diskUsage: 76.3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'srv-007',
|
||||||
|
hostname: 'cache-server-01',
|
||||||
|
ip: '192.168.1.107',
|
||||||
|
os: 'Ubuntu 20.04 LTS',
|
||||||
|
cpu: 16,
|
||||||
|
memory: 64,
|
||||||
|
disk: 200,
|
||||||
|
status: 'running',
|
||||||
|
tags: ['cache', 'production', 'redis'],
|
||||||
|
region: '华南-广州',
|
||||||
|
createdAt: '2024-03-01T10:00:00Z',
|
||||||
|
lastSeen: '2025-03-30T10:25:00Z',
|
||||||
|
cpuUsage: 32.1,
|
||||||
|
memoryUsage: 48.7,
|
||||||
|
diskUsage: 25.4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'srv-008',
|
||||||
|
hostname: 'test-server-01',
|
||||||
|
ip: '192.168.2.101',
|
||||||
|
os: 'Ubuntu 22.04 LTS',
|
||||||
|
cpu: 8,
|
||||||
|
memory: 16,
|
||||||
|
disk: 200,
|
||||||
|
status: 'stopped',
|
||||||
|
tags: ['test', 'development'],
|
||||||
|
region: '华东-上海',
|
||||||
|
createdAt: '2024-06-15T14:00:00Z',
|
||||||
|
lastSeen: '2025-03-29T18:00:00Z',
|
||||||
|
cpuUsage: 0,
|
||||||
|
memoryUsage: 0,
|
||||||
|
diskUsage: 45.2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'srv-009',
|
||||||
|
hostname: 'k8s-master-01',
|
||||||
|
ip: '192.168.1.201',
|
||||||
|
os: 'CentOS 7.9',
|
||||||
|
cpu: 16,
|
||||||
|
memory: 64,
|
||||||
|
disk: 500,
|
||||||
|
status: 'running',
|
||||||
|
tags: ['kubernetes', 'master', 'production'],
|
||||||
|
region: '华北-北京',
|
||||||
|
createdAt: '2024-01-20T08:00:00Z',
|
||||||
|
lastSeen: '2025-03-30T10:25:00Z',
|
||||||
|
cpuUsage: 25.6,
|
||||||
|
memoryUsage: 42.3,
|
||||||
|
diskUsage: 35.8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'srv-010',
|
||||||
|
hostname: 'k8s-worker-01',
|
||||||
|
ip: '192.168.1.211',
|
||||||
|
os: 'CentOS 7.9',
|
||||||
|
cpu: 32,
|
||||||
|
memory: 128,
|
||||||
|
disk: 1000,
|
||||||
|
status: 'running',
|
||||||
|
tags: ['kubernetes', 'worker', 'production'],
|
||||||
|
region: '华北-北京',
|
||||||
|
createdAt: '2024-01-20T08:30:00Z',
|
||||||
|
lastSeen: '2025-03-30T10:25:00Z',
|
||||||
|
cpuUsage: 68.4,
|
||||||
|
memoryUsage: 82.1,
|
||||||
|
diskUsage: 58.9,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// K8s集群数据
|
||||||
|
export const cabinets: Cabinet[] = [
|
||||||
|
{
|
||||||
|
id: 'cab-001',
|
||||||
|
name: 'A01-01',
|
||||||
|
room: '机房A',
|
||||||
|
row: 1,
|
||||||
|
column: 1,
|
||||||
|
totalU: 42,
|
||||||
|
usedU: 28,
|
||||||
|
status: 'active',
|
||||||
|
powerCapacity: 10,
|
||||||
|
powerUsed: 6.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cab-002',
|
||||||
|
name: 'A01-02',
|
||||||
|
room: '机房A',
|
||||||
|
row: 1,
|
||||||
|
column: 2,
|
||||||
|
totalU: 42,
|
||||||
|
usedU: 35,
|
||||||
|
status: 'active',
|
||||||
|
powerCapacity: 10,
|
||||||
|
powerUsed: 8.2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cab-003',
|
||||||
|
name: 'A01-03',
|
||||||
|
room: '机房A',
|
||||||
|
row: 1,
|
||||||
|
column: 3,
|
||||||
|
totalU: 42,
|
||||||
|
usedU: 18,
|
||||||
|
status: 'active',
|
||||||
|
powerCapacity: 10,
|
||||||
|
powerUsed: 4.1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cab-004',
|
||||||
|
name: 'A02-01',
|
||||||
|
room: '机房A',
|
||||||
|
row: 2,
|
||||||
|
column: 1,
|
||||||
|
totalU: 42,
|
||||||
|
usedU: 0,
|
||||||
|
status: 'empty',
|
||||||
|
powerCapacity: 10,
|
||||||
|
powerUsed: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cab-005',
|
||||||
|
name: 'B01-01',
|
||||||
|
room: '机房B',
|
||||||
|
row: 1,
|
||||||
|
column: 1,
|
||||||
|
totalU: 42,
|
||||||
|
usedU: 40,
|
||||||
|
status: 'active',
|
||||||
|
powerCapacity: 10,
|
||||||
|
powerUsed: 9.5,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const devices: Device[] = [
|
||||||
|
{
|
||||||
|
id: 'dev-001',
|
||||||
|
name: 'web-server-01',
|
||||||
|
type: 'server',
|
||||||
|
cabinetId: 'cab-001',
|
||||||
|
startU: 1,
|
||||||
|
sizeU: 2,
|
||||||
|
status: 'online',
|
||||||
|
ip: '192.168.1.101',
|
||||||
|
sn: 'SN123456789',
|
||||||
|
assetNo: 'ASSET-001',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dev-002',
|
||||||
|
name: 'app-server-01',
|
||||||
|
type: 'server',
|
||||||
|
cabinetId: 'cab-001',
|
||||||
|
startU: 4,
|
||||||
|
sizeU: 2,
|
||||||
|
status: 'online',
|
||||||
|
ip: '192.168.1.103',
|
||||||
|
sn: 'SN123456790',
|
||||||
|
assetNo: 'ASSET-002',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dev-003',
|
||||||
|
name: 'switch-core-01',
|
||||||
|
type: 'switch',
|
||||||
|
cabinetId: 'cab-001',
|
||||||
|
startU: 40,
|
||||||
|
sizeU: 1,
|
||||||
|
status: 'online',
|
||||||
|
ip: '192.168.1.2',
|
||||||
|
sn: 'SN987654321',
|
||||||
|
assetNo: 'ASSET-003',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dev-004',
|
||||||
|
name: 'db-server-01',
|
||||||
|
type: 'server',
|
||||||
|
cabinetId: 'cab-002',
|
||||||
|
startU: 1,
|
||||||
|
sizeU: 4,
|
||||||
|
status: 'online',
|
||||||
|
ip: '192.168.1.105',
|
||||||
|
sn: 'SN123456791',
|
||||||
|
assetNo: 'ASSET-004',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ==================== 拓扑架构数据 ====================
|
||||||
|
|
||||||
|
export const serverMetrics = {
|
||||||
|
cpu: { usage: 67.5, cores: 16, load: [1.2, 1.5, 1.8] },
|
||||||
|
memory: { usage: 78.2, total: 64, used: 50.0 },
|
||||||
|
disk: { usage: 45.8, total: 500, used: 229 },
|
||||||
|
network: { in: 12.5, out: 8.3 }
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ 云平台数据 ============
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import type {
|
||||||
|
Server,
|
||||||
|
K8sCluster,
|
||||||
|
K8sNode,
|
||||||
|
K8sPod,
|
||||||
|
AlertRule,
|
||||||
|
AlertEvent,
|
||||||
|
Ticket,
|
||||||
|
ApprovalFlow,
|
||||||
|
ApprovalInstance,
|
||||||
|
AppRelease,
|
||||||
|
User,
|
||||||
|
Role,
|
||||||
|
OperationLog,
|
||||||
|
CronJob,
|
||||||
|
Script,
|
||||||
|
DashboardData,
|
||||||
|
ResourceModel,
|
||||||
|
ModelInstance,
|
||||||
|
Cabinet,
|
||||||
|
Device,
|
||||||
|
TopologyNode,
|
||||||
|
TopologyEdge,
|
||||||
|
MetricData,
|
||||||
|
LogEntry,
|
||||||
|
} from '@/types';
|
||||||
|
|
||||||
|
// 仪表盘数据
|
||||||
|
|
||||||
|
export const users: User[] = [
|
||||||
|
{
|
||||||
|
id: 'user-001',
|
||||||
|
username: 'admin',
|
||||||
|
nickname: '管理员',
|
||||||
|
email: 'admin@company.com',
|
||||||
|
phone: '13800138000',
|
||||||
|
roles: ['admin'],
|
||||||
|
status: 'active',
|
||||||
|
lastLoginAt: '2025-03-30T08:00:00Z',
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'user-002',
|
||||||
|
username: 'liyunwei',
|
||||||
|
nickname: '李运维',
|
||||||
|
email: 'liyunwei@company.com',
|
||||||
|
phone: '13800138001',
|
||||||
|
roles: ['ops'],
|
||||||
|
status: 'active',
|
||||||
|
lastLoginAt: '2025-03-30T09:00:00Z',
|
||||||
|
createdAt: '2024-01-15T00:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'user-003',
|
||||||
|
username: 'zhanggongcheng',
|
||||||
|
nickname: '张工程师',
|
||||||
|
email: 'zhanggongcheng@company.com',
|
||||||
|
phone: '13800138002',
|
||||||
|
roles: ['engineer'],
|
||||||
|
status: 'active',
|
||||||
|
lastLoginAt: '2025-03-29T18:00:00Z',
|
||||||
|
createdAt: '2024-02-01T00:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'user-004',
|
||||||
|
username: 'wangkaifa',
|
||||||
|
nickname: '王开发',
|
||||||
|
email: 'wangkaifa@company.com',
|
||||||
|
phone: '13800138003',
|
||||||
|
roles: ['developer'],
|
||||||
|
status: 'active',
|
||||||
|
lastLoginAt: '2025-03-30T10:00:00Z',
|
||||||
|
createdAt: '2024-03-01T00:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'user-005',
|
||||||
|
username: 'zhaokaifa',
|
||||||
|
nickname: '赵开发',
|
||||||
|
email: 'zhaokaifa@company.com',
|
||||||
|
phone: '13800138004',
|
||||||
|
roles: ['developer'],
|
||||||
|
status: 'active',
|
||||||
|
lastLoginAt: '2025-03-28T16:00:00Z',
|
||||||
|
createdAt: '2024-06-01T00:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 角色数据
|
||||||
|
export const roles: Role[] = [
|
||||||
|
{
|
||||||
|
id: 'role-001',
|
||||||
|
name: '超级管理员',
|
||||||
|
code: 'admin',
|
||||||
|
description: '拥有系统所有权限',
|
||||||
|
permissions: ['*'],
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role-002',
|
||||||
|
name: '运维工程师',
|
||||||
|
code: 'ops',
|
||||||
|
description: '负责系统运维和监控',
|
||||||
|
permissions: ['server:read', 'server:write', 'k8s:*', 'monitor:*', 'alert:*', 'ticket:*'],
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role-003',
|
||||||
|
name: '开发工程师',
|
||||||
|
code: 'developer',
|
||||||
|
description: '负责应用开发和发布',
|
||||||
|
permissions: ['server:read', 'k8s:read', 'deploy:*', 'ticket:read', 'ticket:write'],
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role-004',
|
||||||
|
name: '系统工程师',
|
||||||
|
code: 'engineer',
|
||||||
|
description: '负责系统架构和优化',
|
||||||
|
permissions: ['server:*', 'k8s:*', 'monitor:read', 'ticket:*'],
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 操作日志数据
|
||||||
|
export const operationLogs: OperationLog[] = [
|
||||||
|
{
|
||||||
|
id: 'log-001',
|
||||||
|
userId: 'user-001',
|
||||||
|
username: '管理员',
|
||||||
|
module: '服务器管理',
|
||||||
|
action: '创建服务器',
|
||||||
|
resource: 'srv-011',
|
||||||
|
ip: '192.168.1.100',
|
||||||
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||||
|
result: 'success',
|
||||||
|
createdAt: '2025-03-30T10:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'log-002',
|
||||||
|
userId: 'user-002',
|
||||||
|
username: '李运维',
|
||||||
|
module: '告警管理',
|
||||||
|
action: '修改告警规则',
|
||||||
|
resource: 'rule-001',
|
||||||
|
ip: '192.168.1.100',
|
||||||
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||||
|
result: 'success',
|
||||||
|
createdAt: '2025-03-30T09:30:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'log-003',
|
||||||
|
userId: 'user-003',
|
||||||
|
username: '张工程师',
|
||||||
|
module: 'K8s管理',
|
||||||
|
action: '删除Pod',
|
||||||
|
resource: 'pod-old-001',
|
||||||
|
ip: '192.168.1.100',
|
||||||
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||||
|
result: 'failure',
|
||||||
|
message: '权限不足',
|
||||||
|
createdAt: '2025-03-30T09:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 定时任务数据
|
||||||
|
export const currentUser = {
|
||||||
|
id: 'user-001', username: 'admin', nickname: '管理员', email: 'admin@company.com',
|
||||||
|
phone: '13800138000', roles: ['admin'], avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=admin'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出聚合对象供api.ts使用
|
||||||
|
export const pendingApprovals = [
|
||||||
|
{ id: 'app-001', type: '服务器申请', title: '申请2台16核64G服务器', applicant: '王开发', createdAt: '2025-04-14 09:00:00' },
|
||||||
|
{ id: 'app-002', type: '发布审批', title: '订单服务v3.2.0发布到生产', applicant: '李运维', createdAt: '2025-04-14 10:00:00' },
|
||||||
|
{ id: 'app-003', type: '数据库变更', title: '用户表添加手机号验证字段', applicant: '张工程师', createdAt: '2025-04-14 11:00:00' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============ 用户数据 ============
|
||||||
+104
-45
@@ -1,8 +1,7 @@
|
|||||||
// API Service for AIOps Platform
|
// API Service for AIOps Platform
|
||||||
// 有后端支持的模块调用真实 API,无后端的模块暂用 mock 数据
|
// 所有模块均调用真实后端 API;暂无后端的模块返回空数据优雅降级
|
||||||
import { http, type ListResponse } from './http';
|
import { http, type ListResponse } from './http';
|
||||||
import { useUserStore } from '@/store';
|
import { useUserStore } from '@/store';
|
||||||
import * as mockData from '@/mocks/data';
|
|
||||||
|
|
||||||
// ==================== 用户认证 API ====================
|
// ==================== 用户认证 API ====================
|
||||||
export const userApi = {
|
export const userApi = {
|
||||||
@@ -44,6 +43,10 @@ export const userApi = {
|
|||||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api/v1';
|
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api/v1';
|
||||||
window.open(`${baseUrl}/admin/users/import-template?token=${token}`, '_blank');
|
window.open(`${baseUrl}/admin/users/import-template?token=${token}`, '_blank');
|
||||||
},
|
},
|
||||||
|
// 角色列表 // Logan.Li
|
||||||
|
getRoles: async () => {
|
||||||
|
return http.get<ListResponse>('/user/roles');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==================== LDAP 配置 API ====================
|
// ==================== LDAP 配置 API ====================
|
||||||
@@ -516,76 +519,132 @@ export const auditApi = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==================== 以下模块暂无后端,使用 Mock 数据 ====================
|
// ==================== SQL 审计 API(暂无后端,返回空结果优雅降级) ====================
|
||||||
|
|
||||||
// SQL Audit API(无后端)
|
|
||||||
export const sqlAuditApi = {
|
export const sqlAuditApi = {
|
||||||
getRecords: async () => ({ data: mockData.sqlAuditRecords as any }),
|
getRecords: async () => ({ data: [], total: 0 }),
|
||||||
getRules: async () => ({ data: mockData.sqlAuditRules as any }),
|
getRules: async () => ({ data: [], total: 0 }),
|
||||||
analyzeSQL: async (data: { sql: string }) => {
|
analyzeSQL: async (_data: { sql: string }) => ({ data: { id: 0, riskLevel: 'low', rules: [] } }),
|
||||||
return { data: { id: Date.now().toString(), riskLevel: 'medium', rules: ['规则1'] } };
|
approve: async (_id: string) => ({ success: true }),
|
||||||
},
|
reject: async (_id: string) => ({ success: true }),
|
||||||
approve: async (id: string) => ({ success: true }),
|
execute: async (_id: string) => ({ success: true }),
|
||||||
reject: async (id: string) => ({ success: true }),
|
toggleRule: async (_ruleId: string, _enabled: boolean) => ({ success: true }),
|
||||||
execute: async (id: string) => ({ success: true }),
|
|
||||||
toggleRule: async (ruleId: string, enabled: boolean) => ({ success: true }),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ansible Playbook API(后端 /playbooks)
|
// ==================== Ansible 自动化 API ====================
|
||||||
export const ansibleApi = {
|
export const ansibleApi = {
|
||||||
getPlaybooks: async (params?: { page?: number; page_size?: number }) => {
|
// Playbook 管理
|
||||||
return http.get<ListResponse>('/playbooks', params);
|
getPlaybooks: async (params?: { page?: number; page_size?: number; keyword?: string }) => {
|
||||||
|
return http.get<ListResponse>('/ansible/playbooks', params);
|
||||||
},
|
},
|
||||||
getPlaybook: async (id: number | string) => {
|
getPlaybook: async (id: number | string) => {
|
||||||
return http.get(`/playbooks/${id}`);
|
return http.get(`/ansible/playbooks/${id}`);
|
||||||
},
|
},
|
||||||
createPlaybook: async (data: any) => {
|
createPlaybook: async (data: any) => {
|
||||||
return http.post('/playbooks', data);
|
return http.post('/ansible/playbooks', data);
|
||||||
},
|
},
|
||||||
updatePlaybook: async (id: number | string, data: any) => {
|
updatePlaybook: async (id: number | string, data: any) => {
|
||||||
return http.put(`/playbooks/${id}`, data);
|
return http.put(`/ansible/playbooks/${id}`, data);
|
||||||
},
|
},
|
||||||
deletePlaybook: async (id: number | string) => {
|
deletePlaybook: async (id: number | string) => {
|
||||||
return http.delete(`/playbooks/${id}`);
|
return http.delete(`/ansible/playbooks/${id}`);
|
||||||
},
|
},
|
||||||
executePlaybook: async (id: number | string) => {
|
executePlaybook: async (id: number | string) => {
|
||||||
return http.post(`/playbooks/${id}/execute`);
|
return http.post(`/ansible/playbooks/${id}/execute`);
|
||||||
|
},
|
||||||
|
// 任务执行记录
|
||||||
|
getJobs: async (params?: { page?: number; page_size?: number; status?: string }) => {
|
||||||
|
return http.get<ListResponse>('/ansible/jobs', params);
|
||||||
|
},
|
||||||
|
getJob: async (id: number | string) => {
|
||||||
|
return http.get(`/ansible/jobs/${id}`);
|
||||||
|
},
|
||||||
|
// 清单(Inventory)管理
|
||||||
|
getInventories: async (params?: { page?: number; page_size?: number; keyword?: string }) => {
|
||||||
|
return http.get<ListResponse>('/ansible/inventories', params);
|
||||||
|
},
|
||||||
|
createInventory: async (data: any) => {
|
||||||
|
return http.post('/ansible/inventories', data);
|
||||||
|
},
|
||||||
|
updateInventory: async (id: number | string, data: any) => {
|
||||||
|
return http.put(`/ansible/inventories/${id}`, data);
|
||||||
|
},
|
||||||
|
deleteInventory: async (id: number | string) => {
|
||||||
|
return http.delete(`/ansible/inventories/${id}`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Logs API(无后端)
|
// ==================== 日志监控 API ====================
|
||||||
export const logsApi = {
|
export const logsApi = {
|
||||||
query: async (query: any) => ({ data: mockData.logMonitorEntries as any }),
|
query: async (params?: {
|
||||||
getSources: async () => ({ data: mockData.logSources as any }),
|
level?: string;
|
||||||
export: async (query: any) => ({ success: true }),
|
service?: string;
|
||||||
|
host?: string;
|
||||||
|
keyword?: string;
|
||||||
|
source_id?: number | string;
|
||||||
|
page?: number;
|
||||||
|
page_size?: number;
|
||||||
|
}) => {
|
||||||
|
return http.get<ListResponse>('/logs', params);
|
||||||
|
},
|
||||||
|
getSources: async (params?: { page?: number; page_size?: number }) => {
|
||||||
|
return http.get<ListResponse>('/logs/sources', params);
|
||||||
|
},
|
||||||
|
createSource: async (data: any) => {
|
||||||
|
return http.post('/logs/sources', data);
|
||||||
|
},
|
||||||
|
deleteSource: async (id: number | string) => {
|
||||||
|
return http.delete(`/logs/sources/${id}`);
|
||||||
|
},
|
||||||
|
export: async (params?: {
|
||||||
|
level?: string;
|
||||||
|
service?: string;
|
||||||
|
host?: string;
|
||||||
|
keyword?: string;
|
||||||
|
source_id?: number | string;
|
||||||
|
}) => {
|
||||||
|
return http.post('/logs/export', params);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Alert Center API(无后端,独立于 alertApi)
|
// ==================== 告警中心 API(基于 /alerts 真实路由) ====================
|
||||||
|
// 说明:alertCenterApi 与 alertApi 共用同一套后端 /alerts 路由,此处独立封装以便告警中心页使用
|
||||||
export const alertCenterApi = {
|
export const alertCenterApi = {
|
||||||
getAlerts: async () => ({ data: mockData.alertCenterAlerts as any }),
|
getAlerts: async (params?: { page?: number; page_size?: number; status?: string; severity?: string }) => {
|
||||||
getRules: async () => ({ data: mockData.alertCenterRules as any }),
|
return http.get<ListResponse>('/alerts', params);
|
||||||
getChannels: async () => ({ data: mockData.notificationChannels as any }),
|
},
|
||||||
acknowledge: async (id: string) => ({ success: true }),
|
getRules: async () => {
|
||||||
resolve: async (id: string) => ({ success: true }),
|
return http.get('/alerts/rules');
|
||||||
silence: async (id: string, duration: string) => ({ success: true }),
|
},
|
||||||
toggleRule: async (ruleId: string, enabled: boolean) => ({ success: true }),
|
getChannels: async () => {
|
||||||
|
return http.get('/alerts/notify-channels');
|
||||||
|
},
|
||||||
|
acknowledge: async (id: number | string) => {
|
||||||
|
return http.post(`/alerts/${id}/ack`);
|
||||||
|
},
|
||||||
|
resolve: async (id: number | string) => {
|
||||||
|
return http.post(`/alerts/${id}/ack`);
|
||||||
|
},
|
||||||
|
silence: async (_id: number | string, _duration: string) => ({ success: true }),
|
||||||
|
toggleRule: async (id: number | string) => {
|
||||||
|
return http.post(`/alerts/rules/${id}/toggle`);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Trace API(无后端)
|
// ==================== 链路追踪 API(暂无后端,返回空结果优雅降级) ====================
|
||||||
export const traceApi = {
|
export const traceApi = {
|
||||||
getTraces: async (params: any) => ({ data: mockData.traces as any }),
|
getTraces: async (_params: any) => ({ data: [], total: 0 }),
|
||||||
getServices: async (timeRange: string) => ({ data: mockData.serviceMetrics as any }),
|
getServices: async (_timeRange: string) => ({ data: [], total: 0 }),
|
||||||
getTraceDetail: async (traceId: string) => ({ data: { spans: mockData.traceSpans as any } }),
|
getTraceDetail: async (_traceId: string) => ({ data: { spans: [] } }),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Grafana Dashboard API(无后端)
|
// ==================== Grafana 仪表盘 API(暂无后端,返回空结果优雅降级) ====================
|
||||||
export const dashboardApi = {
|
export const dashboardApi = {
|
||||||
getDashboards: async () => ({ data: mockData.grafanaDashboards as any }),
|
getDashboards: async () => ({ data: [], total: 0 }),
|
||||||
getDatasources: async () => ({ data: mockData.datasources as any }),
|
getDatasources: async () => ({ data: [], total: 0 }),
|
||||||
createDashboard: async (data: any) => ({ success: true, data: { id: Date.now().toString() } }),
|
createDashboard: async (_data: any) => ({ success: false }),
|
||||||
updateDashboard: async (id: string, data: any) => ({ success: true }),
|
updateDashboard: async (_id: number | string, _data: any) => ({ success: false }),
|
||||||
deleteDashboard: async (id: string) => ({ success: true }),
|
deleteDashboard: async (_id: number | string) => ({ success: false }),
|
||||||
createDatasource: async (data: any) => ({ success: true, data: { id: Date.now().toString() } }),
|
createDatasource: async (_data: any) => ({ success: false }),
|
||||||
};
|
};
|
||||||
|
|
||||||
// AI Assistant API(无后端)
|
// AI Assistant API(无后端)
|
||||||
|
|||||||
Reference in new issue
Block a user