merge: 合并远程 A2 告警引擎分布式安全更新,保留本地日志中心等最新改动
This commit is contained in:
commit
23e9e3da62
137 files changed
+947
-611
No files matched your search
@@ -137,7 +137,7 @@
|
|||||||
| 前端 React Hooks exhaustive-deps 缺失 | 38 处 | P1 |
|
| 前端 React Hooks exhaustive-deps 缺失 | 38 处 | P1 |
|
||||||
| 后端 `_ =` 忽略错误 | 13 处(3 处中风险) | P2 |
|
| 后端 `_ =` 忽略错误 | 13 处(3 处中风险) | P2 |
|
||||||
| 前端硬编码内网 IP(10.10.10.75 等) | 6+ 处 | P2 |
|
| 前端硬编码内网 IP(10.10.10.75 等) | 6+ 处 | P2 |
|
||||||
| 后端告警引擎单实例 + 无优雅退出 | 多副本会重复告警 | P2 |
|
| 后端告警引擎分布式安全 | ✅ Redis 分布式锁(SetNX + 60s TTL) | 后端 | ~~P2~~ done |
|
||||||
|
|
||||||
### 3.3 🟢 低优先级
|
### 3.3 🟢 低优先级
|
||||||
|
|
||||||
@@ -202,6 +202,6 @@
|
|||||||
| 功能完整性 | **88/100** | 8 大模块齐全,K8s 管理极为完善;扣分项:监控大屏/链路追踪/告警静默未对接 |
|
| 功能完整性 | **88/100** | 8 大模块齐全,K8s 管理极为完善;扣分项:监控大屏/链路追踪/告警静默未对接 |
|
||||||
| 代码质量 | **72/100** | 后端规范好(0 panic, 1 TODO);前端 1026 处 any + 218 处未用变量是主要扣分项 |
|
| 代码质量 | **72/100** | 后端规范好(0 panic, 1 TODO);前端 1026 处 any + 218 处未用变量是主要扣分项 |
|
||||||
| 架构设计 | **82/100** | 前后端分离清晰,自研轻量 K8s 客户端;扣分项:kubernetes.go/api.ts 巨型文件 |
|
| 架构设计 | **82/100** | 前后端分离清晰,自研轻量 K8s 客户端;扣分项:kubernetes.go/api.ts 巨型文件 |
|
||||||
| 部署运维 | **85/100** | 三种部署方式 + run.sh 统一入口;扣分项:后端镜像偏大,告警引擎单实例 |
|
| 部署运维 | **87/100** | 三种部署方式 + run.sh 统一入口;扣分项:后端镜像偏大 |
|
||||||
| 用户体验 | **86/100** | 骨架屏 + 页面过渡 + 5 套主题 + 懒加载;扣分项:无 i18n,无虚拟滚动 |
|
| 用户体验 | **86/100** | 骨架屏 + 页面过渡 + 5 套主题 + 懒加载;扣分项:无 i18n,无虚拟滚动 |
|
||||||
| **综合** | **83/100** | 企业级运维平台,功能覆盖全面,工程治理是当前首要提升方向 |
|
| **综合** | **83/100** | 企业级运维平台,功能覆盖全面,工程治理是当前首要提升方向 |
|
||||||
+11
-3
@@ -162,11 +162,19 @@ services/api.ts (1324L)
|
|||||||
|
|
||||||
**方案**: 以 Kuboard code 路由为主,ID 路由保留向后兼容(标记 deprecated)
|
**方案**: 以 Kuboard code 路由为主,ID 路由保留向后兼容(标记 deprecated)
|
||||||
|
|
||||||
### A2: 告警引擎分布式安全 [pending]
|
### A2: 告警引擎分布式安全 [done]
|
||||||
|
|
||||||
**问题**: 告警引擎用 goroutine + 30s ticker 常驻,多副本会重复告警
|
**问题**: 告警引擎用 goroutine + 30s ticker 常驻,多副本会重复告警
|
||||||
|
|
||||||
**方案**: Redis 分布式锁(每轮 ticker 先抢锁),或选主机制
|
**方案**: Redis 分布式锁(SetNX 原子抢锁),每轮 ticker 先抢锁,60s TTL 防死锁。单实例/redka 模式自动 fallback 为无锁直行。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [x] Cache 接口新增 `SetNX` 方法(go-redis 原子 / redka check-then-set 单副本安全)
|
||||||
|
- [x] 多副本部署时仅 Leader 副本执行告警检查(SetNX 分布式锁 + 实例 ID 校验)
|
||||||
|
- [x] Leader 崩溃后锁自动过期(60s TTL),其他副本自动接管
|
||||||
|
- [x] Redis 故障时 fail-open(不阻断告警)
|
||||||
|
- [x] `GET /alerts/engine/status` 状态监控端点(is_leader / instance_id / lock_holder)
|
||||||
|
- [x] 本地/redka 模式(cache=nil)自动 fallback 为单实例直行
|
||||||
|
|
||||||
### A3: WebSocket 连接统一管理 [pending]
|
### A3: WebSocket 连接统一管理 [pending]
|
||||||
|
|
||||||
@@ -207,7 +215,7 @@ services/api.ts (1324L)
|
|||||||
| F2 | 审计日志导出 | P1 | **done** |
|
| F2 | 审计日志导出 | P1 | **done** |
|
||||||
| F3 | 前端 RBAC 控制 | P1 | **done** |
|
| F3 | 前端 RBAC 控制 | P1 | **done** |
|
||||||
| A1 | K8s 路由统一 | P2 | pending |
|
| A1 | K8s 路由统一 | P2 | pending |
|
||||||
| A2 | 告警引擎分布式 | P2 | pending |
|
| A2 | 告警引擎分布式 | P2 | **done** |
|
||||||
| A3 | WebSocket 管理 | P2 | pending |
|
| A3 | WebSocket 管理 | P2 | pending |
|
||||||
| U1 | 虚拟滚动 | P3 | pending |
|
| U1 | 虚拟滚动 | P3 | pending |
|
||||||
| U2 | 统一 DataTable | P3 | pending |
|
| U2 | 统一 DataTable | P3 | pending |
|
||||||
|
|||||||
+46
@@ -340,6 +340,52 @@ curl -s -X POST "http://127.0.0.1:8080/api/v1/servers" \
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## P2-A2: 告警引擎分布式安全
|
||||||
|
|
||||||
|
### 验证用例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOKEN=$(python3 -c "
|
||||||
|
import urllib.request, json
|
||||||
|
login_data = json.dumps({'username': 'admin', 'password': '123456'}).encode()
|
||||||
|
req = urllib.request.Request('http://127.0.0.1:8080/api/v1/auth/login', data=login_data, method='POST')
|
||||||
|
req.add_header('Content-Type', 'application/json')
|
||||||
|
resp = urllib.request.urlopen(req)
|
||||||
|
print(json.loads(resp.read())['data']['token'])
|
||||||
|
")
|
||||||
|
|
||||||
|
# U1: 引擎状态端点
|
||||||
|
curl -s http://127.0.0.1:8080/api/v1/alerts/engine/status \
|
||||||
|
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
|
||||||
|
# 期望: code=200, distributed=true, is_leader=true, instance_id 非空
|
||||||
|
|
||||||
|
# U2: 模拟其他副本持有锁 — 引擎应识别为非 Leader
|
||||||
|
docker exec aiops-redis redis-cli SET aiops:alert_engine:lock "ae-OTHER" EX 40
|
||||||
|
curl -s http://127.0.0.1:8080/api/v1/alerts/engine/status \
|
||||||
|
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; print(f'is_leader={d[\"is_leader\"]} lock_holder={d[\"lock_holder\"]}')"
|
||||||
|
# 期望: is_leader=False, lock_holder=ae-OTHER
|
||||||
|
|
||||||
|
# U3: 等待 35s,验证引擎跳过检查(日志输出)
|
||||||
|
sleep 35
|
||||||
|
docker logs aiops 2>&1 | grep "AlertEngine.*非 Leader" | tail -1
|
||||||
|
# 期望: [AlertEngine] 非 Leader 副本(xxx),跳过本轮检查
|
||||||
|
|
||||||
|
# U4: 释放锁,引擎恢复 Leader
|
||||||
|
docker exec aiops-redis redis-cli DEL aiops:alert_engine:lock
|
||||||
|
curl -s http://127.0.0.1:8080/api/v1/alerts/engine/status \
|
||||||
|
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; print(f'is_leader={d[\"is_leader\"]}')"
|
||||||
|
# 期望: is_leader=True
|
||||||
|
```
|
||||||
|
|
||||||
|
### 通过标准
|
||||||
|
- [x] U1: 引擎状态端点返回 distributed/is_leader/instance_id/lock_ttl_sec
|
||||||
|
- [x] U2: 其他副本持锁时正确识别为非 Leader
|
||||||
|
- [x] U3: 日志确认引擎跳过检查(非 Leader 副本不执行告警扫描)
|
||||||
|
- [x] U4: 锁释放后恢复 Leader 角色
|
||||||
|
- [x] `go build` 零错误,5/5 核心 API 回归通过
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 回归测试基线(每次改动后必跑)
|
## 回归测试基线(每次改动后必跑)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
--mode
|
||||||
|
local
|
||||||
|
--port
|
||||||
|
8080
|
||||||
@@ -1,32 +1,117 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ezops/model"
|
"ezops/model"
|
||||||
|
ezredis "ezops/pkg/redis"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertEngine 告警引擎 — 定时扫描 Agent 上报数据并触发告警 // Logan.Li
|
// AlertEngine 告警引擎 — 定时扫描 Agent 上报数据并触发告警
|
||||||
|
// 支持分布式锁:多副本部署时仅 Leader 执行检查,避免重复告警
|
||||||
type AlertEngine struct {
|
type AlertEngine struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
cache ezredis.Cache // 可选 Redis 缓存(分布式锁)
|
||||||
|
instanceID string // 当前实例唯一标识
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAlertEngine(db *gorm.DB) *AlertEngine {
|
const (
|
||||||
return &AlertEngine{db: db}
|
alertEngineLockKey = "aiops:alert_engine:lock"
|
||||||
|
alertEngineLockTTL = 60 * time.Second // 锁 TTL:2x tick 间隔,保证处理时间内不释放
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewAlertEngine 创建告警引擎实例
|
||||||
|
// cache 参数用于分布式锁(多副本部署)。nil 时为单实例模式,不竞争锁。
|
||||||
|
func NewAlertEngine(db *gorm.DB, cache ezredis.Cache) *AlertEngine {
|
||||||
|
// 生成唯一实例 ID(随机 hex,防止多副本 PID/hostname 碰撞)
|
||||||
|
idBytes := make([]byte, 8)
|
||||||
|
_, _ = rand.Read(idBytes)
|
||||||
|
instanceID := fmt.Sprintf("ae-%s", hex.EncodeToString(idBytes))
|
||||||
|
return &AlertEngine{
|
||||||
|
db: db,
|
||||||
|
cache: cache,
|
||||||
|
instanceID: instanceID,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check 每 30 秒调用一次
|
// Check 每 30 秒调用一次。多副本模式下通过分布式锁保证仅 Leader 执行。
|
||||||
|
// 无 cache(单实例/缓存初始化失败)→ 直接执行
|
||||||
|
// 有 cache(Docker/K8s 多副本)→ SetNX 原子抢锁
|
||||||
|
// Redis 故障 → fail-open(继续执行,避免告警遗漏)
|
||||||
func (e *AlertEngine) Check() {
|
func (e *AlertEngine) Check() {
|
||||||
|
if !e.tryAcquireLock() {
|
||||||
|
log.Printf("[AlertEngine] 非 Leader 副本(%s),跳过本轮检查", e.instanceID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer e.releaseLock()
|
||||||
|
|
||||||
e.checkResourceAlerts()
|
e.checkResourceAlerts()
|
||||||
e.checkOfflineAlerts()
|
e.checkOfflineAlerts()
|
||||||
e.checkPortAlerts()
|
e.checkPortAlerts()
|
||||||
e.autoResolveAlerts()
|
e.autoResolveAlerts()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tryAcquireLock 尝试获取分布式锁,返回是否获得 Leader 角色
|
||||||
|
func (e *AlertEngine) tryAcquireLock() bool {
|
||||||
|
if e.cache == nil {
|
||||||
|
return true // 单实例模式
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
acquired, err := e.cache.SetNX(ctx, alertEngineLockKey, e.instanceID, alertEngineLockTTL)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[AlertEngine] 获取分布式锁失败(%s),fail-open 继续: %v", e.instanceID, err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return acquired
|
||||||
|
}
|
||||||
|
|
||||||
|
// releaseLock 释放分布式锁(仅当当前实例持有锁时才释放,防止误删其他副本的锁)
|
||||||
|
func (e *AlertEngine) releaseLock() {
|
||||||
|
if e.cache == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
val, err := e.cache.Get(ctx, alertEngineLockKey)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if val == e.instanceID {
|
||||||
|
_ = e.cache.Del(ctx, alertEngineLockKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEngineStatus 返回告警引擎运行状态(用于运维监控)
|
||||||
|
func (e *AlertEngine) GetEngineStatus() map[string]interface{} {
|
||||||
|
isLeader := true
|
||||||
|
lockHolder := ""
|
||||||
|
if e.cache != nil {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if val, err := e.cache.Get(ctx, alertEngineLockKey); err == nil {
|
||||||
|
lockHolder = val
|
||||||
|
isLeader = lockHolder == "" || lockHolder == e.instanceID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map[string]interface{}{
|
||||||
|
"is_leader": isLeader,
|
||||||
|
"instance_id": e.instanceID,
|
||||||
|
"lock_holder": lockHolder,
|
||||||
|
"distributed": e.cache != nil,
|
||||||
|
"tick_interval": 30,
|
||||||
|
"lock_ttl_sec": int(alertEngineLockTTL / time.Second),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 检查 CPU/内存/磁盘/进程/TCP连接/端口 阈值
|
// 检查 CPU/内存/磁盘/进程/TCP连接/端口 阈值
|
||||||
func (e *AlertEngine) checkResourceAlerts() {
|
func (e *AlertEngine) checkResourceAlerts() {
|
||||||
var rules []model.AgentRule
|
var rules []model.AgentRule
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ func (r *goRedisCache) Set(ctx context.Context, key string, value string, expira
|
|||||||
return r.client.Set(ctx, key, value, expiration).Err()
|
return r.client.Set(ctx, key, value, expiration).Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetNX 原子 SET if Not eXists — go-redis 原生支持,用于分布式锁
|
||||||
|
func (r *goRedisCache) SetNX(ctx context.Context, key string, value string, expiration time.Duration) (bool, error) {
|
||||||
|
return r.client.SetNX(ctx, key, value, expiration).Result()
|
||||||
|
}
|
||||||
|
|
||||||
func (r *goRedisCache) Del(ctx context.Context, keys ...string) error {
|
func (r *goRedisCache) Del(ctx context.Context, keys ...string) error {
|
||||||
return r.client.Del(ctx, keys...).Err()
|
return r.client.Del(ctx, keys...).Err()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ type Cache interface {
|
|||||||
Capabilities() Capabilities
|
Capabilities() Capabilities
|
||||||
Get(ctx context.Context, key string) (string, error)
|
Get(ctx context.Context, key string) (string, error)
|
||||||
Set(ctx context.Context, key string, value string, expiration time.Duration) error
|
Set(ctx context.Context, key string, value string, expiration time.Duration) error
|
||||||
|
// SetNX 原子「Set if Not eXists」— 用于分布式锁(多副本 Leader 选举)
|
||||||
|
SetNX(ctx context.Context, key string, value string, expiration time.Duration) (bool, error)
|
||||||
Del(ctx context.Context, keys ...string) error
|
Del(ctx context.Context, keys ...string) error
|
||||||
Exists(ctx context.Context, keys ...string) (int64, error)
|
Exists(ctx context.Context, keys ...string) (int64, error)
|
||||||
Expire(ctx context.Context, key string, expiration time.Duration) error
|
Expire(ctx context.Context, key string, expiration time.Duration) error
|
||||||
|
|||||||
@@ -65,6 +65,27 @@ func (r *redkaCache) Set(ctx context.Context, key string, value string, expirati
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetNX check-then-set — Redka v0.2.0 无原子 SetNX,使用 Exists+Set 模拟。
|
||||||
|
// 本地/redka 模式为单副本运行,不存在并发竞争,非原子实现安全。
|
||||||
|
func (r *redkaCache) SetNX(ctx context.Context, key string, value string, expiration time.Duration) (bool, error) {
|
||||||
|
ok, err := r.db.Key().Exists(key)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err := r.db.Str().Set(key, value); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if expiration > 0 {
|
||||||
|
if _, err := r.db.Key().Expire(key, expiration); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *redkaCache) Del(ctx context.Context, keys ...string) error {
|
func (r *redkaCache) Del(ctx context.Context, keys ...string) error {
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
if _, err := r.db.Key().Delete(key); err != nil {
|
if _, err := r.db.Key().Delete(key); err != nil {
|
||||||
|
|||||||
@@ -496,6 +496,9 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
kubernetesRouter.GET("/compare", k8sHandler.GetMultiClusterComparison)
|
kubernetesRouter.GET("/compare", k8sHandler.GetMultiClusterComparison)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 告警引擎(分布式锁 — 多副本部署时仅 Leader 执行检查)
|
||||||
|
alertEngine := controller.NewAlertEngine(db, redisCache)
|
||||||
|
|
||||||
// 告警路由
|
// 告警路由
|
||||||
alertHandler := controller.NewAlertHandler(db)
|
alertHandler := controller.NewAlertHandler(db)
|
||||||
alerts := authorized.Group("/alerts")
|
alerts := authorized.Group("/alerts")
|
||||||
@@ -532,6 +535,10 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
alerts.PUT("/silences/:id", alertHandler.UpdateSilence)
|
alerts.PUT("/silences/:id", alertHandler.UpdateSilence)
|
||||||
alerts.DELETE("/silences/:id", alertHandler.DeleteSilence)
|
alerts.DELETE("/silences/:id", alertHandler.DeleteSilence)
|
||||||
alerts.POST("/silences/:id/toggle", alertHandler.ToggleSilence)
|
alerts.POST("/silences/:id/toggle", alertHandler.ToggleSilence)
|
||||||
|
// 告警引擎状态 — 查看当前 Leader 副本和分布式锁状态
|
||||||
|
alerts.GET("/engine/status", func(c *gin.Context) {
|
||||||
|
c.JSON(200, model.Response{Code: 200, Data: alertEngine.GetEngineStatus()})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流水线路由
|
// 流水线路由
|
||||||
@@ -943,8 +950,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
siteGroup.GET("/stats", siteHandler.GetSiteStats)
|
siteGroup.GET("/stats", siteHandler.GetSiteStats)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动告警引擎(每 30 秒检查一次)
|
// 启动告警引擎(每 30 秒检查一次;alertEngine 在告警路由上方创建)
|
||||||
alertEngine := controller.NewAlertEngine(db)
|
|
||||||
go func() {
|
go func() {
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
mode: "local"
|
||||||
|
port: "8080"
|
||||||
|
log_level: "info"
|
||||||
|
|
||||||
|
database:
|
||||||
|
driver: "sqlite"
|
||||||
|
host: "localhost"
|
||||||
|
port: 3306
|
||||||
|
user: ""
|
||||||
|
password: ""
|
||||||
|
dbname: "sqlite3.db"
|
||||||
|
charset: "utf8mb4"
|
||||||
|
max_conn: 100
|
||||||
|
max_idle: 10
|
||||||
|
|
||||||
|
redis:
|
||||||
|
host: "localhost"
|
||||||
|
port: 6379
|
||||||
|
driver: "redka"
|
||||||
|
password: ""
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
secret: "aiops-local-dev-secret-change-in-production"
|
||||||
|
expire_hour: 24
|
||||||
|
|
||||||
|
websocket:
|
||||||
|
read_buffer_size: 1024
|
||||||
|
write_buffer_size: 1024
|
||||||
@@ -27,18 +27,19 @@ import {
|
|||||||
severityBadge,
|
severityBadge,
|
||||||
} from './agentAlertHelper';
|
} from './agentAlertHelper';
|
||||||
import { AgentRuleDialog } from './AgentRuleDialog';
|
import { AgentRuleDialog } from './AgentRuleDialog';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export function AgentAlerts() {
|
export function AgentAlerts() {
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const [activeTab, setActiveTab] = useState('events');
|
const [activeTab, setActiveTab] = useState('events');
|
||||||
const [alerts, setAlerts] = useState<any[]>([]);
|
const [alerts, setAlerts] = useState<JsonData[]>([]);
|
||||||
const [rules, setRules] = useState<any[]>([]);
|
const [rules, setRules] = useState<JsonData[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [resolvedView, setResolvedView] = useState(false);
|
const [resolvedView, setResolvedView] = useState(false);
|
||||||
|
|
||||||
// 规则编辑弹窗
|
// 规则编辑弹窗
|
||||||
const [ruleOpen, setRuleOpen] = useState(false);
|
const [ruleOpen, setRuleOpen] = useState(false);
|
||||||
const [editingRule, setEditingRule] = useState<any>(null);
|
const [editingRule, setEditingRule] = useState<JsonData>(null);
|
||||||
|
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -80,7 +81,7 @@ export function AgentAlerts() {
|
|||||||
setRuleOpen(true);
|
setRuleOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEditRule = ( rule: any) => {
|
const openEditRule = (rule: JsonData) => {
|
||||||
setEditingRule(rule);
|
setEditingRule(rule);
|
||||||
setRuleOpen(true);
|
setRuleOpen(true);
|
||||||
};
|
};
|
||||||
@@ -97,7 +98,7 @@ export function AgentAlerts() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleRule = async ( rule: any) => {
|
const handleToggleRule = async (rule: JsonData) => {
|
||||||
try {
|
try {
|
||||||
await agentApi.updateRule(rule.id, { ...rule, enabled: !rule.enabled });
|
await agentApi.updateRule(rule.id, { ...rule, enabled: !rule.enabled });
|
||||||
loadData();
|
loadData();
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ import {
|
|||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import { METRIC_OPTIONS, OPERATOR_OPTIONS, SEVERITY_OPTIONS } from './agentAlertHelper';
|
import { METRIC_OPTIONS, OPERATOR_OPTIONS, SEVERITY_OPTIONS } from './agentAlertHelper';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (v: boolean) => void;
|
onOpenChange: (v: boolean) => void;
|
||||||
editingRule: any;
|
editingRule?: JsonData;
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,17 +29,18 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export function HostDetailDialog({
|
export function HostDetailDialog({
|
||||||
host,
|
host,
|
||||||
onClose,
|
onClose,
|
||||||
onCommand,
|
onCommand,
|
||||||
}: {
|
}: {
|
||||||
host: any;
|
host?: JsonData;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onCommand: (hostname: string) => void;
|
onCommand: (hostname: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [history, setHistory] = useState<any[]>([]);
|
const [history, setHistory] = useState<JsonData[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [hours, setHours] = useState(1);
|
const [hours, setHours] = useState(1);
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ export function HostDetailDialog({
|
|||||||
if (!host) return null;
|
if (!host) return null;
|
||||||
|
|
||||||
const online = isOnline(host.last_report || host.last_reported_at || host.updated_at);
|
const online = isOnline(host.last_report || host.last_reported_at || host.updated_at);
|
||||||
const ports: Record<string, any>[] = Array.isArray(host.ports) ? host.ports : [];
|
const ports: JsonData[] = Array.isArray(host.ports) ? host.ports : [];
|
||||||
|
|
||||||
const chartData = history.map((h) => ({
|
const chartData = history.map((h) => ({
|
||||||
time: new Date(h.timestamp || h.time || h.created_at || Date.now()).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
time: new Date(h.timestamp || h.time || h.created_at || Date.now()).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Agent 告警常量与辅助函数(从 AgentAlerts.tsx 提取) // Logan.Li
|
// Agent 告警常量与辅助函数(从 AgentAlerts.tsx 提取) // Logan.Li
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { AlertTriangle, Cpu, MemoryStick, HardDrive, WifiOff, Network } from 'lucide-react';
|
import { AlertTriangle, Cpu, MemoryStick, HardDrive, WifiOff, Network } from 'lucide-react';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
// 监控指标选项(含图标,告警事件/规则/编辑弹窗共用) // Logan.Li
|
// 监控指标选项(含图标,告警事件/规则/编辑弹窗共用) // Logan.Li
|
||||||
export const METRIC_OPTIONS = [
|
export const METRIC_OPTIONS = [
|
||||||
@@ -33,7 +34,7 @@ export function getMetricLabel(metric: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 兼容多种列表响应格式(数组 / {list} / {items} / {data}) // Logan.Li
|
// 兼容多种列表响应格式(数组 / {list} / {items} / {data}) // Logan.Li
|
||||||
export function asList( data: any): unknown[] {
|
export function asList(data: JsonData): unknown[] {
|
||||||
if (!data) return [];
|
if (!data) return [];
|
||||||
if (Array.isArray(data)) return data;
|
if (Array.isArray(data)) return data;
|
||||||
if (Array.isArray(data.list)) return data.list;
|
if (Array.isArray(data.list)) return data.list;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
} from './analyzerHelpers';
|
} from './analyzerHelpers';
|
||||||
import { AIAnalysisResult } from './AIAnalysisResult';
|
import { AIAnalysisResult } from './AIAnalysisResult';
|
||||||
import { AutoFixDialog } from './AutoFixDialog';
|
import { AutoFixDialog } from './AutoFixDialog';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
// Parse LLM JSON response (may have markdown fences) into AIAnalysis
|
// Parse LLM JSON response (may have markdown fences) into AIAnalysis
|
||||||
function parseAIResponse(raw: string, fallback: AIAnalysis): AIAnalysis {
|
function parseAIResponse(raw: string, fallback: AIAnalysis): AIAnalysis {
|
||||||
@@ -61,7 +62,7 @@ export function AIAlertAnalyzer() {
|
|||||||
const loadAlerts = async () => {
|
const loadAlerts = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res: Record<string, any> = await alertApi.getAlerts({ page: 1, page_size: 50 });
|
const res: JsonData = await alertApi.getAlerts({ page: 1, page_size: 50 });
|
||||||
// 列表响应格式:http.ts 已提取 .data,此处仍是 {list, total}
|
// 列表响应格式:http.ts 已提取 .data,此处仍是 {list, total}
|
||||||
const list = res?.list || res || [];
|
const list = res?.list || res || [];
|
||||||
setAlerts(list.map(mapAlertEvent));
|
setAlerts(list.map(mapAlertEvent));
|
||||||
@@ -102,7 +103,7 @@ export function AIAlertAnalyzer() {
|
|||||||
}`;
|
}`;
|
||||||
|
|
||||||
const res = await aiApi.chat([{ role: 'user', content: prompt }]);
|
const res = await aiApi.chat([{ role: 'user', content: prompt }]);
|
||||||
const content = (res as Record<string, any>)?.content || (res as Record<string, any>)?.message?.content || '';
|
const content = (res as JsonData)?.content || (res as JsonData)?.message?.content || '';
|
||||||
if (content) {
|
if (content) {
|
||||||
setAnalysis(parseAIResponse(content, fallback));
|
setAnalysis(parseAIResponse(content, fallback));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
Wifi,
|
Wifi,
|
||||||
Square,
|
Square,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
// LLM 提供商列表(OpenAI 兼容接口) // Logan.Li
|
// LLM 提供商列表(OpenAI 兼容接口) // Logan.Li
|
||||||
const LLM_PROVIDERS: { id: string; name: string; defaultEndpoint: string; models: string[] }[] = [
|
const LLM_PROVIDERS: { id: string; name: string; defaultEndpoint: string; models: string[] }[] = [
|
||||||
@@ -68,7 +69,7 @@ const defaultConfig: LLMConfigData = {
|
|||||||
// 拉取最近告警事件,格式化为紧凑列表供 LLM 分析
|
// 拉取最近告警事件,格式化为紧凑列表供 LLM 分析
|
||||||
async function loadAlertsContext(): Promise<string> {
|
async function loadAlertsContext(): Promise<string> {
|
||||||
const res = await alertApi.getAlerts({ page: 1, page_size: 20 });
|
const res = await alertApi.getAlerts({ page: 1, page_size: 20 });
|
||||||
const alerts: Record<string, any>[] = (res as Record<string, any>)?.list || [];
|
const alerts: JsonData[] = (res as JsonData)?.list || [];
|
||||||
if (!alerts.length) return '(当前无告警事件)';
|
if (!alerts.length) return '(当前无告警事件)';
|
||||||
const lines = alerts.slice(0, 15).map((a, i) =>
|
const lines = alerts.slice(0, 15).map((a, i) =>
|
||||||
`${i + 1}. [${a.severity || '未知'}|${a.status || '-'}] ${a.rule_name || '告警'} → 目标:${a.target || '-'} 值:${a.value ?? '-'} 时间:${(a.started_at || '').slice(0, 16)}`,
|
`${i + 1}. [${a.severity || '未知'}|${a.status || '-'}] ${a.rule_name || '告警'} → 目标:${a.target || '-'} 值:${a.value ?? '-'} 时间:${(a.started_at || '').slice(0, 16)}`,
|
||||||
@@ -80,7 +81,7 @@ async function loadAlertsContext(): Promise<string> {
|
|||||||
// 拉取 Agent 实时主机指标,识别高负载主机
|
// 拉取 Agent 实时主机指标,识别高负载主机
|
||||||
async function loadMetricsContext(): Promise<string> {
|
async function loadMetricsContext(): Promise<string> {
|
||||||
const res = await agentApi.getLatest();
|
const res = await agentApi.getLatest();
|
||||||
const hosts: Record<string, any>[] = Array.isArray(res) ? res : (res as Record<string, any>)?.list || [];
|
const hosts: JsonData[] = Array.isArray(res) ? res : (res as JsonData)?.list || [];
|
||||||
if (!hosts.length) return '(当前无 Agent 上报数据,可能未部署监控 Agent)';
|
if (!hosts.length) return '(当前无 Agent 上报数据,可能未部署监控 Agent)';
|
||||||
const lines = hosts.slice(0, 20).map((h, i) => {
|
const lines = hosts.slice(0, 20).map((h, i) => {
|
||||||
const cpu = h.cpu_usage ?? '-';
|
const cpu = h.cpu_usage ?? '-';
|
||||||
@@ -101,11 +102,11 @@ async function loadReportContext(): Promise<string> {
|
|||||||
dashboardStatsApi.getAlerts(10).catch(() => null),
|
dashboardStatsApi.getAlerts(10).catch(() => null),
|
||||||
dashboardStatsApi.getReleases(10).catch(() => null),
|
dashboardStatsApi.getReleases(10).catch(() => null),
|
||||||
]);
|
]);
|
||||||
const s: Record<string, any> = stats || {};
|
const s: JsonData = stats || {};
|
||||||
const ss = s.server_stats || {}, ks = s.kubernetes_stats || {}, as = s.alert_stats || {}, ps = s.pipeline_stats || {}, cs = s.cost_stats || {};
|
const ss = s.server_stats || {}, ks = s.kubernetes_stats || {}, as = s.alert_stats || {}, ps = s.pipeline_stats || {}, cs = s.cost_stats || {};
|
||||||
const overview = `服务器:${ss.total ?? 0}(运行${ss.running ?? 0}) | Kubernetes集群:${ks.total ?? 0} 节点:${ks.total_nodes ?? 0} Pod:${ks.total_pods ?? 0} | 告警:${as.total ?? 0}(firing ${as.firing ?? 0}) | 流水线:${ps.total ?? 0}(成功率${ps.success_rate ?? 0}%) | 本月成本:¥${cs.month_cost ?? 0}`;
|
const overview = `服务器:${ss.total ?? 0}(运行${ss.running ?? 0}) | Kubernetes集群:${ks.total ?? 0} 节点:${ks.total_nodes ?? 0} Pod:${ks.total_pods ?? 0} | 告警:${as.total ?? 0}(firing ${as.firing ?? 0}) | 流水线:${ps.total ?? 0}(成功率${ps.success_rate ?? 0}%) | 本月成本:¥${cs.month_cost ?? 0}`;
|
||||||
const alertList: Record<string, any>[] = Array.isArray(alerts) ? alerts : (alerts as Record<string, any>)?.list || [];
|
const alertList: JsonData[] = Array.isArray(alerts) ? alerts : (alerts as JsonData)?.list || [];
|
||||||
const releaseList: Record<string, any>[] = Array.isArray(releases) ? releases : (releases as Record<string, any>)?.list || [];
|
const releaseList: JsonData[] = Array.isArray(releases) ? releases : (releases as JsonData)?.list || [];
|
||||||
const alertLines = alertList.slice(0, 10).map((a, i) => `${i + 1}.[${a.severity || '?'}|${a.status || '?'}] ${a.rule_name || '-'} → ${a.target || '-'}`);
|
const alertLines = alertList.slice(0, 10).map((a, i) => `${i + 1}.[${a.severity || '?'}|${a.status || '?'}] ${a.rule_name || '-'} → ${a.target || '-'}`);
|
||||||
const releaseLines = releaseList.slice(0, 10).map((r, i) => `${i + 1}.${r.app_name || '-'} ${r.version || ''} [${r.environment || '-'}/${r.status || '-'}] by ${r.author || '-'}`);
|
const releaseLines = releaseList.slice(0, 10).map((r, i) => `${i + 1}.${r.app_name || '-'} ${r.version || ''} [${r.environment || '-'}/${r.status || '-'}] by ${r.author || '-'}`);
|
||||||
return `平台概览:${overview}\n\n最近告警:\n${alertLines.join('\n') || '(无)'}\n\n最近发布:\n${releaseLines.join('\n') || '(无)'}`;
|
return `平台概览:${overview}\n\n最近告警:\n${alertLines.join('\n') || '(无)'}\n\n最近发布:\n${releaseLines.join('\n') || '(无)'}`;
|
||||||
@@ -188,7 +189,7 @@ export function AIOpsAssistant() {
|
|||||||
const loadHistory = async () => {
|
const loadHistory = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await aiApi.getHistory(sessionId);
|
const res = await aiApi.getHistory(sessionId);
|
||||||
const items: ChatHistoryItem[] = (res as Record<string, any>)?.list || [];
|
const items: ChatHistoryItem[] = (res as JsonData)?.list || [];
|
||||||
setMessages(
|
setMessages(
|
||||||
items.map((m) => ({
|
items.map((m) => ({
|
||||||
id: String(m.id),
|
id: String(m.id),
|
||||||
@@ -227,7 +228,7 @@ export function AIOpsAssistant() {
|
|||||||
try {
|
try {
|
||||||
const saved = await aiApi.saveConfig(config);
|
const saved = await aiApi.saveConfig(config);
|
||||||
setConfig(saved);
|
setConfig(saved);
|
||||||
setConfigReady(!!(saved.enabled && (saved as Record<string, any>).configured));
|
setConfigReady(!!(saved.enabled && (saved as JsonData).configured));
|
||||||
toast.success('配置已保存');
|
toast.success('配置已保存');
|
||||||
setIsConfigOpen(false);
|
setIsConfigOpen(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
trendColorClass,
|
trendColorClass,
|
||||||
type CapacityForecast,
|
type CapacityForecast,
|
||||||
} from './predictUtils';
|
} from './predictUtils';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
// 重新导出 CapacityForecast,兼容已有 import 路径 './AIPredictiveAnalytics'
|
// 重新导出 CapacityForecast,兼容已有 import 路径 './AIPredictiveAnalytics'
|
||||||
export type { CapacityForecast };
|
export type { CapacityForecast };
|
||||||
@@ -46,10 +47,10 @@ export function AIPredictiveAnalytics() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
// 原始数据
|
// 原始数据
|
||||||
const [rawMetrics, setRawMetrics] = useState<any[]>([]);
|
const [rawMetrics, setRawMetrics] = useState<JsonData[]>([]);
|
||||||
const [serverStats, setServerStats] = useState<any>({});
|
const [serverStats, setServerStats] = useState<JsonData>({});
|
||||||
const [alertEvents, setAlertEvents] = useState<any[]>([]);
|
const [alertEvents, setAlertEvents] = useState<JsonData[]>([]);
|
||||||
const [alertRules, setAlertRules] = useState<any[]>([]);
|
const [alertRules, setAlertRules] = useState<JsonData[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadData();
|
void loadData();
|
||||||
@@ -66,9 +67,9 @@ export function AIPredictiveAnalytics() {
|
|||||||
]);
|
]);
|
||||||
setRawMetrics(Array.isArray(metricsData) ? metricsData : []);
|
setRawMetrics(Array.isArray(metricsData) ? metricsData : []);
|
||||||
setServerStats(statsResp || {});
|
setServerStats(statsResp || {});
|
||||||
const alertsRaw = (alertsResp as Record<string, any>)?.list || (alertsResp as Record<string, any>) || [];
|
const alertsRaw = (alertsResp as JsonData)?.list || (alertsResp as JsonData) || [];
|
||||||
setAlertEvents(Array.isArray(alertsRaw) ? alertsRaw : []);
|
setAlertEvents(Array.isArray(alertsRaw) ? alertsRaw : []);
|
||||||
const rulesRaw = (rulesResp as Record<string, any>)?.list || (rulesResp as Record<string, any>) || [];
|
const rulesRaw = (rulesResp as JsonData)?.list || (rulesResp as JsonData) || [];
|
||||||
setAlertRules(Array.isArray(rulesRaw) ? rulesRaw : []);
|
setAlertRules(Array.isArray(rulesRaw) ? rulesRaw : []);
|
||||||
} catch {
|
} catch {
|
||||||
// 保持空数据,组件显示空状态
|
// 保持空数据,组件显示空状态
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { JsonData } from "@/types/common";
|
||||||
// AI 告警分析纯逻辑辅助函数:类型定义 + 后端告警映射 + 动态 AI 分析生成 + 严重等级配色 // Logan.Li
|
// AI 告警分析纯逻辑辅助函数:类型定义 + 后端告警映射 + 动态 AI 分析生成 + 严重等级配色 // Logan.Li
|
||||||
|
|
||||||
// 告警条目(前端统一模型,从后端 alert event 映射而来)
|
// 告警条目(前端统一模型,从后端 alert event 映射而来)
|
||||||
@@ -145,7 +146,7 @@ export function generateAIAnalysis(alert: AlertItem): AIAnalysis {
|
|||||||
|
|
||||||
// ─── 后端告警事件 → AlertItem 映射 ───
|
// ─── 后端告警事件 → AlertItem 映射 ───
|
||||||
// 后端 alert event 字段为 snake_case(rule_name/target/started_at/value 等),此处统一映射为前端 AlertItem
|
// 后端 alert event 字段为 snake_case(rule_name/target/started_at/value 等),此处统一映射为前端 AlertItem
|
||||||
export function mapAlertEvent( event: any): AlertItem {
|
export function mapAlertEvent(event: JsonData): AlertItem {
|
||||||
const expr = event.rule?.expression || '';
|
const expr = event.rule?.expression || '';
|
||||||
// 从表达式(如 "cpu_usage > 80")中提取指标名与阈值
|
// 从表达式(如 "cpu_usage > 80")中提取指标名与阈值
|
||||||
const metricMatch = expr.match(/([\w_]+)\s*[><=!]+\s*([\d.]+)/);
|
const metricMatch = expr.match(/([\w_]+)\s*[><=!]+\s*([\d.]+)/);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { JsonData } from "@/types/common";
|
||||||
/**
|
/**
|
||||||
* AI 预测分析模块的纯计算工具 — 类型定义、常量、线性回归、预测推算
|
* AI 预测分析模块的纯计算工具 — 类型定义、常量、线性回归、预测推算
|
||||||
*
|
*
|
||||||
@@ -88,7 +89,7 @@ export function linearRegression(values: number[]): { slope: number; r2: number
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 从时序指标中提取按时间排序的数值序列 */
|
/** 从时序指标中提取按时间排序的数值序列 */
|
||||||
export function extractSeries( metrics: any[], name: string): number[] {
|
export function extractSeries( metrics: JsonData[], name: string): number[] {
|
||||||
return metrics
|
return metrics
|
||||||
.filter((m) => m.name === name)
|
.filter((m) => m.name === name)
|
||||||
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
|
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
|
||||||
@@ -140,10 +141,10 @@ export function anomalyAction(metricKey: string): string {
|
|||||||
|
|
||||||
/** computePredictions 的输入参数 */
|
/** computePredictions 的输入参数 */
|
||||||
export interface ComputePredictionsInput {
|
export interface ComputePredictionsInput {
|
||||||
rawMetrics: any[];
|
rawMetrics: JsonData[];
|
||||||
serverStats: any;
|
serverStats?: JsonData;
|
||||||
alertEvents: any[];
|
alertEvents: JsonData[];
|
||||||
alertRules: any[];
|
alertRules: JsonData[];
|
||||||
selectedTimeRange: string; // '1h' | '24h' | '7d' | '30d'
|
selectedTimeRange: string; // '1h' | '24h' | '7d' | '30d'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ import { Loader2 } from 'lucide-react';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { approvalApi } from '@/api';
|
import { approvalApi } from '@/api';
|
||||||
import { parseFields, APPROVAL_TYPE_OPTIONS } from './helpers';
|
import { parseFields, APPROVAL_TYPE_OPTIONS } from './helpers';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
interface ApprovalCreateDialogProps {
|
interface ApprovalCreateDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
templates: any[];
|
templates: JsonData[];
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,8 +39,8 @@ export function ApprovalCreateDialog({
|
|||||||
templates,
|
templates,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}: ApprovalCreateDialogProps) {
|
}: ApprovalCreateDialogProps) {
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<any>(null);
|
const [selectedTemplate, setSelectedTemplate] = useState<JsonData>(null);
|
||||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
const [formData, setFormData] = useState<JsonData>({});
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
// 提交新建申请 // Logan.Li
|
// 提交新建申请 // Logan.Li
|
||||||
@@ -143,7 +144,7 @@ export function ApprovalCreateDialog({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-4">
|
<div className="grid grid-cols-2 gap-x-4 gap-y-4">
|
||||||
{parseFields(selectedTemplate.form_schema).map(( field: any) => {
|
{parseFields(selectedTemplate.form_schema).map((field: JsonData) => {
|
||||||
const isTextarea = field.type === 'textarea';
|
const isTextarea = field.type === 'textarea';
|
||||||
return (
|
return (
|
||||||
<div key={field.key} className={cn('space-y-1.5', isTextarea && 'col-span-2')}>
|
<div key={field.key} className={cn('space-y-1.5', isTextarea && 'col-span-2')}>
|
||||||
|
|||||||
@@ -16,14 +16,15 @@ import {
|
|||||||
getApprovalTypeColor,
|
getApprovalTypeColor,
|
||||||
getInstanceType,
|
getInstanceType,
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
interface ApprovalDetailDialogProps {
|
interface ApprovalDetailDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
instance: any;
|
instance?: JsonData;
|
||||||
templates: any[];
|
templates: JsonData[];
|
||||||
onApprove: ( instance: any) => void;
|
onApprove: (instance: JsonData) => void;
|
||||||
onReject: ( instance: any) => void;
|
onReject: (instance: JsonData) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 审批详情弹窗 — 展示流程详情/表单/进度/操作记录 // Logan.Li
|
// 审批详情弹窗 — 展示流程详情/表单/进度/操作记录 // Logan.Li
|
||||||
@@ -91,7 +92,7 @@ export function ApprovalDetailDialog({
|
|||||||
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-4">
|
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-4">
|
||||||
<p className="text-sm font-medium text-[var(--dg-text-secondary)] mb-3">操作记录</p>
|
<p className="text-sm font-medium text-[var(--dg-text-secondary)] mb-3">操作记录</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{instance.actions.map(( action: any, index: number) => (
|
{instance.actions.map((action: JsonData, index: number) => (
|
||||||
<div key={index} className="flex items-center gap-3 text-sm">
|
<div key={index} className="flex items-center gap-3 text-sm">
|
||||||
{action.action === 'approve' ? (
|
{action.action === 'approve' ? (
|
||||||
<CheckCircle2 className="h-4 w-4 text-emerald-400" />
|
<CheckCircle2 className="h-4 w-4 text-emerald-400" />
|
||||||
|
|||||||
@@ -33,13 +33,14 @@ import {
|
|||||||
getApprovalTypeColor,
|
getApprovalTypeColor,
|
||||||
getInstanceType,
|
getInstanceType,
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export function ApprovalFlow() {
|
export function ApprovalFlow() {
|
||||||
const [activeTab, setActiveTab] = useState<'instances' | 'flows' | 'my-approvals'>('instances');
|
const [activeTab, setActiveTab] = useState<'instances' | 'flows' | 'my-approvals'>('instances');
|
||||||
|
|
||||||
const [processes, setProcesses] = useState<any[]>([]);
|
const [processes, setProcesses] = useState<JsonData[]>([]);
|
||||||
const [templates, setTemplates] = useState<any[]>([]);
|
const [templates, setTemplates] = useState<JsonData[]>([]);
|
||||||
const [myApprovals, setMyApprovals] = useState<any[]>([]);
|
const [myApprovals, setMyApprovals] = useState<JsonData[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
@@ -48,7 +49,7 @@ export function ApprovalFlow() {
|
|||||||
|
|
||||||
// 详情弹窗
|
// 详情弹窗
|
||||||
const [detailOpen, setDetailOpen] = useState(false);
|
const [detailOpen, setDetailOpen] = useState(false);
|
||||||
const [selectedInstance, setSelectedInstance] = useState<any>(null);
|
const [selectedInstance, setSelectedInstance] = useState<JsonData>(null);
|
||||||
|
|
||||||
// 审批操作弹窗
|
// 审批操作弹窗
|
||||||
const [actionDialogOpen, setActionDialogOpen] = useState(false);
|
const [actionDialogOpen, setActionDialogOpen] = useState(false);
|
||||||
@@ -95,13 +96,13 @@ export function ApprovalFlow() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 打开详情弹窗 // Logan.Li
|
// 打开详情弹窗 // Logan.Li
|
||||||
const handleView = ( instance: any) => {
|
const handleView = (instance: JsonData) => {
|
||||||
setSelectedInstance(instance);
|
setSelectedInstance(instance);
|
||||||
setDetailOpen(true);
|
setDetailOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打开审批操作弹窗 // Logan.Li
|
// 打开审批操作弹窗 // Logan.Li
|
||||||
const openActionDialog = (type: 'approve' | 'reject', instance: any) => {
|
const openActionDialog = (type: 'approve' | 'reject', instance: JsonData) => {
|
||||||
setActionType(type);
|
setActionType(type);
|
||||||
setSelectedInstance(instance);
|
setSelectedInstance(instance);
|
||||||
setActionComment('');
|
setActionComment('');
|
||||||
@@ -123,7 +124,7 @@ export function ApprovalFlow() {
|
|||||||
setActionDialogOpen(false);
|
setActionDialogOpen(false);
|
||||||
loadData();
|
loadData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('操作失败', { description: (error as Record<string, any>).message });
|
toast.error('操作失败', { description: (error as JsonData).message });
|
||||||
} finally {
|
} finally {
|
||||||
setActionSubmitting(false);
|
setActionSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
getApprovalTypeIcon,
|
getApprovalTypeIcon,
|
||||||
getInstanceType,
|
getInstanceType,
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
// 状态标签页定义
|
// 状态标签页定义
|
||||||
const STATUS_TABS = [
|
const STATUS_TABS = [
|
||||||
@@ -69,16 +70,16 @@ function getStatusBadge(status: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ApprovalProcessTabProps {
|
interface ApprovalProcessTabProps {
|
||||||
processes: any[];
|
processes: JsonData[];
|
||||||
filteredInstances: any[];
|
filteredInstances: JsonData[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
setSearchQuery: (v: string) => void;
|
setSearchQuery: (v: string) => void;
|
||||||
statusFilter: string;
|
statusFilter: string;
|
||||||
setStatusFilter: (v: string) => void;
|
setStatusFilter: (v: string) => void;
|
||||||
onView: ( instance: any) => void;
|
onView: (instance: JsonData) => void;
|
||||||
onApprove: ( instance: any) => void;
|
onApprove: (instance: JsonData) => void;
|
||||||
onReject: ( instance: any) => void;
|
onReject: (instance: JsonData) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ApprovalProcessTab({
|
export function ApprovalProcessTab({
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
|
|||||||
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 { Filter, Edit, GitBranch, ArrowRight } from 'lucide-react';
|
import { Filter, Edit, GitBranch, ArrowRight } from 'lucide-react';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
function parseSteps( steps: any) {
|
function parseSteps(steps: JsonData) {
|
||||||
if (Array.isArray(steps)) return steps;
|
if (Array.isArray(steps)) return steps;
|
||||||
if (typeof steps === 'string') {
|
if (typeof steps === 'string') {
|
||||||
try {
|
try {
|
||||||
@@ -31,14 +32,14 @@ const getApprovalTypeColor = (value: string): string =>
|
|||||||
APPROVAL_TYPE_OPTIONS.find((o) => o.value === value)?.color || 'var(--dg-text-secondary)';
|
APPROVAL_TYPE_OPTIONS.find((o) => o.value === value)?.color || 'var(--dg-text-secondary)';
|
||||||
|
|
||||||
// 模拟审批流程图
|
// 模拟审批流程图
|
||||||
function renderFlowChart( flow: any) {
|
function renderFlowChart(flow: JsonData) {
|
||||||
const nodes = parseSteps(flow.steps);
|
const nodes = parseSteps(flow.steps);
|
||||||
if (nodes.length === 0) {
|
if (nodes.length === 0) {
|
||||||
return <div className="text-[var(--dg-text-muted)] text-center py-4">暂无流程节点</div>;
|
return <div className="text-[var(--dg-text-muted)] text-center py-4">暂无流程节点</div>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 overflow-x-auto py-4">
|
<div className="flex items-center gap-2 overflow-x-auto py-4">
|
||||||
{nodes.map(( node: any, index: number) => (
|
{nodes.map((node: JsonData, index: number) => (
|
||||||
<div key={node.id || index} className="flex items-center">
|
<div key={node.id || index} className="flex items-center">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -66,7 +67,7 @@ function renderFlowChart( flow: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ApprovalTemplateTabProps {
|
interface ApprovalTemplateTabProps {
|
||||||
filteredTemplates: any[];
|
filteredTemplates: JsonData[];
|
||||||
templateTypeFilter: string;
|
templateTypeFilter: string;
|
||||||
setTemplateTypeFilter: (value: string) => void;
|
setTemplateTypeFilter: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import {
|
|||||||
GitCommit,
|
GitCommit,
|
||||||
FileText,
|
FileText,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
// 解析审批步骤数组(兼容 JSON 字符串) // Logan.Li
|
// 解析审批步骤数组(兼容 JSON 字符串) // Logan.Li
|
||||||
export function parseSteps( steps: any) {
|
export function parseSteps(steps: JsonData) {
|
||||||
if (Array.isArray(steps)) return steps;
|
if (Array.isArray(steps)) return steps;
|
||||||
if (typeof steps === 'string') {
|
if (typeof steps === 'string') {
|
||||||
try {
|
try {
|
||||||
@@ -24,7 +25,7 @@ export function parseSteps( steps: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 解析模板的 form_schema,返回字段定义数组 // Logan.Li
|
// 解析模板的 form_schema,返回字段定义数组 // Logan.Li
|
||||||
export function parseFields( formSchema: any): any[] {
|
export function parseFields(formSchema: JsonData): JsonData[] {
|
||||||
if (!formSchema) return [];
|
if (!formSchema) return [];
|
||||||
const schema = typeof formSchema === 'string' ? (() => { try { return JSON.parse(formSchema); } catch { return null; } })() : formSchema;
|
const schema = typeof formSchema === 'string' ? (() => { try { return JSON.parse(formSchema); } catch { return null; } })() : formSchema;
|
||||||
if (!schema) return [];
|
if (!schema) return [];
|
||||||
@@ -32,7 +33,7 @@ export function parseFields( formSchema: any): any[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 在详情弹窗中渲染表单数据为键值对展示 // Logan.Li
|
// 在详情弹窗中渲染表单数据为键值对展示 // Logan.Li
|
||||||
export function renderFormData(formDataStr: string, template?: any) {
|
export function renderFormData(formDataStr: string, template?: JsonData) {
|
||||||
if (!formDataStr) return null;
|
if (!formDataStr) return null;
|
||||||
let data: Record<string, unknown>;
|
let data: Record<string, unknown>;
|
||||||
try {
|
try {
|
||||||
@@ -78,26 +79,26 @@ export const getApprovalTypeIcon = (value: string) =>
|
|||||||
APPROVAL_TYPE_OPTIONS.find((o) => o.value === value)?.icon || FileText;
|
APPROVAL_TYPE_OPTIONS.find((o) => o.value === value)?.icon || FileText;
|
||||||
|
|
||||||
// 获取审批实例的类型(优先从关联模板取) // Logan.Li
|
// 获取审批实例的类型(优先从关联模板取) // Logan.Li
|
||||||
export const getInstanceType = ( instance: any): string =>
|
export const getInstanceType = (instance: JsonData): string =>
|
||||||
instance?.template?.type || instance?.type || '';
|
instance?.template?.type || instance?.type || '';
|
||||||
|
|
||||||
// 计算审批进度信息 // Logan.Li
|
// 计算审批进度信息 // Logan.Li
|
||||||
export function getProgressInfo( instance: any) {
|
export function getProgressInfo(instance: JsonData) {
|
||||||
const steps = parseSteps(instance.steps);
|
const steps = parseSteps(instance.steps);
|
||||||
const currentIndex = instance.current_step ?? 0;
|
const currentIndex = instance.current_step ?? 0;
|
||||||
const total = steps.length;
|
const total = steps.length;
|
||||||
const completed = steps.filter(( s: any) => s.status === 'approved' || s.status === 'completed').length;
|
const completed = steps.filter((s: JsonData) => s.status === 'approved' || s.status === 'completed').length;
|
||||||
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
|
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
|
||||||
return { steps, currentIndex, total, completed, percentage };
|
return { steps, currentIndex, total, completed, percentage };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染审批进度(用于详情弹窗) // Logan.Li
|
// 渲染审批进度(用于详情弹窗) // Logan.Li
|
||||||
export function renderStepProgress( instance: any) {
|
export function renderStepProgress(instance: JsonData) {
|
||||||
const { steps, currentIndex } = getProgressInfo(instance);
|
const { steps, currentIndex } = getProgressInfo(instance);
|
||||||
if (steps.length === 0) return null;
|
if (steps.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1 overflow-x-auto">
|
<div className="flex items-center gap-1 overflow-x-auto">
|
||||||
{steps.map(( step: any, index: number) => {
|
{steps.map((step: JsonData, index: number) => {
|
||||||
const isCompleted = step.status === 'approved' || step.status === 'completed';
|
const isCompleted = step.status === 'approved' || step.status === 'completed';
|
||||||
const isCurrent = index === currentIndex;
|
const isCurrent = index === currentIndex;
|
||||||
const isRejected = step.status === 'rejected';
|
const isRejected = step.status === 'rejected';
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { cronJobApi, scriptApi } from '@/api';
|
import { cronJobApi, scriptApi } from '@/api';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
// 定时任务类型定义 // Logan.Li
|
// 定时任务类型定义 // Logan.Li
|
||||||
const jobTypes = [
|
const jobTypes = [
|
||||||
@@ -37,7 +38,7 @@ export function CronJobFormDialog({
|
|||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (v: boolean) => void;
|
onOpenChange: (v: boolean) => void;
|
||||||
editJob: any | null;
|
editJob: JsonData | null;
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
@@ -48,15 +49,15 @@ export function CronJobFormDialog({
|
|||||||
content: '',
|
content: '',
|
||||||
script_id: null as number | null,
|
script_id: null as number | null,
|
||||||
});
|
});
|
||||||
const [scripts, setScripts] = useState<any[]>([]);
|
const [scripts, setScripts] = useState<JsonData[]>([]);
|
||||||
const [selectedScript, setSelectedScript] = useState<any>(null);
|
const [selectedScript, setSelectedScript] = useState<JsonData>(null);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
// 弹窗打开时:加载脚本库 + 初始化表单 // Logan.Li
|
// 弹窗打开时:加载脚本库 + 初始化表单 // Logan.Li
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
// 拉取脚本库
|
// 拉取脚本库
|
||||||
scriptApi.getScripts({ page_size: 200 }).then(( res: any) => {
|
scriptApi.getScripts({ page_size: 200 }).then((res: JsonData) => {
|
||||||
setScripts(res?.list || []);
|
setScripts(res?.list || []);
|
||||||
}).catch(() => setScripts([]));
|
}).catch(() => setScripts([]));
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@ export function CronJobFormDialog({
|
|||||||
});
|
});
|
||||||
// 如果关联了脚本,拉取脚本详情
|
// 如果关联了脚本,拉取脚本详情
|
||||||
if (editJob.script_id) {
|
if (editJob.script_id) {
|
||||||
scriptApi.getScript(editJob.script_id).then(( res: any) => {
|
scriptApi.getScript(editJob.script_id).then((res: JsonData) => {
|
||||||
setSelectedScript(res);
|
setSelectedScript(res);
|
||||||
}).catch(() => setSelectedScript(null));
|
}).catch(() => setSelectedScript(null));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
import { History, Loader2, Terminal } from 'lucide-react';
|
import { History, Loader2, Terminal } from 'lucide-react';
|
||||||
import { taskExecutionApi } from '@/api';
|
import { taskExecutionApi } from '@/api';
|
||||||
import { getStatusBadge } from './cronJobHelpers';
|
import { getStatusBadge } from './cronJobHelpers';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
// 定时任务执行历史弹窗(打开时自动加载,点击行展开输出) // Logan.Li
|
// 定时任务执行历史弹窗(打开时自动加载,点击行展开输出) // Logan.Li
|
||||||
export function ExecutionHistoryDialog({
|
export function ExecutionHistoryDialog({
|
||||||
@@ -26,7 +27,7 @@ export function ExecutionHistoryDialog({
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (v: boolean) => void;
|
onOpenChange: (v: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const [executions, setExecutions] = useState<any[]>([]);
|
const [executions, setExecutions] = useState<JsonData[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [expandedExec, setExpandedExec] = useState<number | null>(null);
|
const [expandedExec, setExpandedExec] = useState<number | null>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import ScriptDetailDialog from './ScriptDetailDialog';
|
|||||||
import ScriptLogsDialog from './ScriptLogsDialog';
|
import ScriptLogsDialog from './ScriptLogsDialog';
|
||||||
import ScriptCard from './ScriptCard';
|
import ScriptCard from './ScriptCard';
|
||||||
import ScriptExecutionsTable from './ScriptExecutionsTable';
|
import ScriptExecutionsTable from './ScriptExecutionsTable';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export function ScriptManager() {
|
export function ScriptManager() {
|
||||||
const _confirm = useConfirm();
|
const _confirm = useConfirm();
|
||||||
@@ -49,7 +50,7 @@ export function ScriptManager() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await scriptApi.getScripts({ page_size: 100 });
|
const res = await scriptApi.getScripts({ page_size: 100 });
|
||||||
const items = (res as Record<string, any>)?.list || [];
|
const items = (res as JsonData)?.list || [];
|
||||||
setScripts(items.map(mapScript));
|
setScripts(items.map(mapScript));
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('获取脚本列表失败');
|
toast.error('获取脚本列表失败');
|
||||||
@@ -61,7 +62,7 @@ export function ScriptManager() {
|
|||||||
const fetchExecutions = async () => {
|
const fetchExecutions = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await taskExecutionApi.getExecutions({ task_type: 'script', page_size: 50 });
|
const res = await taskExecutionApi.getExecutions({ task_type: 'script', page_size: 50 });
|
||||||
const items = (res as Record<string, any>)?.list || [];
|
const items = (res as JsonData)?.list || [];
|
||||||
setExecutions(items);
|
setExecutions(items);
|
||||||
} catch {
|
} catch {
|
||||||
// 静默失败
|
// 静默失败
|
||||||
@@ -189,7 +190,7 @@ export function ScriptManager() {
|
|||||||
setExecuting(true);
|
setExecuting(true);
|
||||||
try {
|
try {
|
||||||
const res = await scriptApi.executeScript(sc.id);
|
const res = await scriptApi.executeScript(sc.id);
|
||||||
const exec = (res as Record<string, any>)?.data ?? res;
|
const exec = (res as JsonData)?.data ?? res;
|
||||||
if (exec?.status === 'success') {
|
if (exec?.status === 'success') {
|
||||||
toast.success(`执行成功,耗时 ${exec.duration ?? 0}ms`);
|
toast.success(`执行成功,耗时 ${exec.duration ?? 0}ms`);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Clock, Loader2, CheckCircle, XCircle } from 'lucide-react';
|
import { Clock, Loader2, CheckCircle, XCircle } from 'lucide-react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
// ---- 类型定义 ----
|
// ---- 类型定义 ----
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ export interface ExecutionRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 将后端返回的脚本原始对象映射为前端结构
|
// 将后端返回的脚本原始对象映射为前端结构
|
||||||
export function mapScript( raw: any): Script {
|
export function mapScript(raw: JsonData): Script {
|
||||||
return {
|
return {
|
||||||
id: raw.id,
|
id: raw.id,
|
||||||
name: raw.name,
|
name: raw.name,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { Loader2 } from 'lucide-react';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { billingApi } from '@/api';
|
import { billingApi } from '@/api';
|
||||||
import { providerLabels } from './CloudBillingHelpers';
|
import { providerLabels } from './CloudBillingHelpers';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
const emptyRecord = {
|
const emptyRecord = {
|
||||||
provider: 'aliyun',
|
provider: 'aliyun',
|
||||||
@@ -36,12 +37,12 @@ const emptyRecord = {
|
|||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
editingRecord: any | null;
|
editingRecord: JsonData | null;
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BillingRecordDialog({ open, onOpenChange, editingRecord, onSuccess }: Props) {
|
export function BillingRecordDialog({ open, onOpenChange, editingRecord, onSuccess }: Props) {
|
||||||
const [form, setForm] = useState<any>(emptyRecord);
|
const [form, setForm] = useState<JsonData>(emptyRecord);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
// 弹窗打开时初始化表单(新建预填今日日期,编辑回填数据) // Logan.Li
|
// 弹窗打开时初始化表单(新建预填今日日期,编辑回填数据) // Logan.Li
|
||||||
@@ -80,7 +81,7 @@ export function BillingRecordDialog({ open, onOpenChange, editingRecord, onSucce
|
|||||||
onSuccess();
|
onSuccess();
|
||||||
toast('保存成功');
|
toast('保存成功');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error('保存失败', { description: (e as Record<string, any>).message });
|
toast.error('保存失败', { description: (e as JsonData).message });
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,18 +23,19 @@ import { Loader2 } from 'lucide-react';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { billingApi } from '@/api';
|
import { billingApi } from '@/api';
|
||||||
import { providerLabels } from './CloudBillingHelpers';
|
import { providerLabels } from './CloudBillingHelpers';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
const emptyRule = { name: '', provider: '', metric: 'daily', threshold: '', operator: '>', enabled: true };
|
const emptyRule = { name: '', provider: '', metric: 'daily', threshold: '', operator: '>', enabled: true };
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
editingRule: any | null;
|
editingRule: JsonData | null;
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BillingRuleDialog({ open, onOpenChange, editingRule, onSuccess }: Props) {
|
export function BillingRuleDialog({ open, onOpenChange, editingRule, onSuccess }: Props) {
|
||||||
const [form, setForm] = useState<any>(emptyRule);
|
const [form, setForm] = useState<JsonData>(emptyRule);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
// 弹窗打开时初始化表单 // Logan.Li
|
// 弹窗打开时初始化表单 // Logan.Li
|
||||||
@@ -71,7 +72,7 @@ export function BillingRuleDialog({ open, onOpenChange, editingRule, onSuccess }
|
|||||||
onSuccess();
|
onSuccess();
|
||||||
toast('保存成功');
|
toast('保存成功');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error('保存失败', { description: (e as Record<string, any>).message });
|
toast.error('保存失败', { description: (e as JsonData).message });
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cloudApi } from '@/api';
|
import { cloudApi } from '@/api';
|
||||||
import { CloudBrandIcon, getProviderColor } from '@/components/icons/CloudBrandIcon';
|
import { CloudBrandIcon, getProviderColor } from '@/components/icons/CloudBrandIcon';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
const providerLabels: Record<string, string> = {
|
const providerLabels: Record<string, string> = {
|
||||||
aws: 'AWS',
|
aws: 'AWS',
|
||||||
@@ -37,7 +38,7 @@ const providerLabels: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function CloudAccountManager() {
|
export function CloudAccountManager() {
|
||||||
const [accounts, setAccounts] = useState<any[]>([]);
|
const [accounts, setAccounts] = useState<JsonData[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [selectedProvider, setSelectedProvider] = useState<string>('all');
|
const [selectedProvider, setSelectedProvider] = useState<string>('all');
|
||||||
@@ -45,7 +46,7 @@ export function CloudAccountManager() {
|
|||||||
// 创建/编辑账号表单状态
|
// 创建/编辑账号表单状态
|
||||||
const [formOpen, setFormOpen] = useState(false);
|
const [formOpen, setFormOpen] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [editingAccount, setEditingAccount] = useState<any>(null);
|
const [editingAccount, setEditingAccount] = useState<JsonData>(null);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
provider: 'aliyun',
|
provider: 'aliyun',
|
||||||
@@ -72,7 +73,7 @@ export function CloudAccountManager() {
|
|||||||
setFormOpen(true);
|
setFormOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenEdit = ( account: any) => {
|
const handleOpenEdit = (account: JsonData) => {
|
||||||
setEditingAccount(account);
|
setEditingAccount(account);
|
||||||
setFormData({
|
setFormData({
|
||||||
name: account.name || '',
|
name: account.name || '',
|
||||||
@@ -89,7 +90,7 @@ export function CloudAccountManager() {
|
|||||||
if (!formData.name.trim() || !formData.provider.trim()) return;
|
if (!formData.name.trim() || !formData.provider.trim()) return;
|
||||||
try {
|
try {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
const payload: Record<string, any> = {
|
const payload: JsonData = {
|
||||||
name: formData.name.trim(),
|
name: formData.name.trim(),
|
||||||
provider: formData.provider,
|
provider: formData.provider,
|
||||||
access_key: formData.access_key.trim(),
|
access_key: formData.access_key.trim(),
|
||||||
@@ -114,7 +115,7 @@ export function CloudAccountManager() {
|
|||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res: Record<string, any> = await cloudApi.getAccounts();
|
const res: JsonData = await cloudApi.getAccounts();
|
||||||
setAccounts(res?.list || []);
|
setAccounts(res?.list || []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载云账号列表失败:', e);
|
console.error('加载云账号列表失败:', e);
|
||||||
|
|||||||
@@ -28,29 +28,30 @@ import { formatCurrency, TrendChart, providerLabels, providerColors } from './Cl
|
|||||||
import { StatStrip } from '@/components/ui/stat-strip';
|
import { StatStrip } from '@/components/ui/stat-strip';
|
||||||
import { BillingRecordDialog } from './BillingRecordDialog';
|
import { BillingRecordDialog } from './BillingRecordDialog';
|
||||||
import { BillingRuleDialog } from './BillingRuleDialog';
|
import { BillingRuleDialog } from './BillingRuleDialog';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export function CloudBilling() {
|
export function CloudBilling() {
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<'overview' | 'records' | 'alerts'>('overview');
|
const [activeTab, setActiveTab] = useState<'overview' | 'records' | 'alerts'>('overview');
|
||||||
|
|
||||||
const [stats, setStats] = useState<any>(null);
|
const [stats, setStats] = useState<JsonData>(null);
|
||||||
const [statsLoading, setStatsLoading] = useState(true);
|
const [statsLoading, setStatsLoading] = useState(true);
|
||||||
|
|
||||||
const [records, setRecords] = useState<any[]>([]);
|
const [records, setRecords] = useState<JsonData[]>([]);
|
||||||
const [recordsLoading, setRecordsLoading] = useState(true);
|
const [recordsLoading, setRecordsLoading] = useState(true);
|
||||||
const [filterProvider, setFilterProvider] = useState('');
|
const [filterProvider, setFilterProvider] = useState('');
|
||||||
const [filterStart, setFilterStart] = useState('');
|
const [filterStart, setFilterStart] = useState('');
|
||||||
const [filterEnd, setFilterEnd] = useState('');
|
const [filterEnd, setFilterEnd] = useState('');
|
||||||
|
|
||||||
const [rules, setRules] = useState<any[]>([]);
|
const [rules, setRules] = useState<JsonData[]>([]);
|
||||||
const [rulesLoading, setRulesLoading] = useState(true);
|
const [rulesLoading, setRulesLoading] = useState(true);
|
||||||
|
|
||||||
// 弹窗状态(open + 编辑目标) // Logan.Li
|
// 弹窗状态(open + 编辑目标) // Logan.Li
|
||||||
const [recordDialogOpen, setRecordDialogOpen] = useState(false);
|
const [recordDialogOpen, setRecordDialogOpen] = useState(false);
|
||||||
const [editingRecord, setEditingRecord] = useState<any>(null);
|
const [editingRecord, setEditingRecord] = useState<JsonData>(null);
|
||||||
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
|
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
|
||||||
const [editingRule, setEditingRule] = useState<any>(null);
|
const [editingRule, setEditingRule] = useState<JsonData>(null);
|
||||||
|
|
||||||
const loadStats = useCallback(async () => {
|
const loadStats = useCallback(async () => {
|
||||||
setStatsLoading(true);
|
setStatsLoading(true);
|
||||||
@@ -58,7 +59,7 @@ export function CloudBilling() {
|
|||||||
const res = await billingApi.getStats();
|
const res = await billingApi.getStats();
|
||||||
setStats(res);
|
setStats(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error('加载统计失败', { description: (e as Record<string, any>).message });
|
toast.error('加载统计失败', { description: (e as JsonData).message });
|
||||||
} finally {
|
} finally {
|
||||||
setStatsLoading(false);
|
setStatsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -67,14 +68,14 @@ export function CloudBilling() {
|
|||||||
const loadRecords = useCallback(async () => {
|
const loadRecords = useCallback(async () => {
|
||||||
setRecordsLoading(true);
|
setRecordsLoading(true);
|
||||||
try {
|
try {
|
||||||
const params: Record<string, any> = {};
|
const params: JsonData = {};
|
||||||
if (filterProvider) params.provider = filterProvider;
|
if (filterProvider) params.provider = filterProvider;
|
||||||
if (filterStart) params.start_date = filterStart;
|
if (filterStart) params.start_date = filterStart;
|
||||||
if (filterEnd) params.end_date = filterEnd;
|
if (filterEnd) params.end_date = filterEnd;
|
||||||
const res = await billingApi.list(params);
|
const res = await billingApi.list(params);
|
||||||
setRecords(res.list || []);
|
setRecords(res.list || []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error('加载账单失败', { description: (e as Record<string, any>).message });
|
toast.error('加载账单失败', { description: (e as JsonData).message });
|
||||||
} finally {
|
} finally {
|
||||||
setRecordsLoading(false);
|
setRecordsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -86,7 +87,7 @@ export function CloudBilling() {
|
|||||||
const res = await billingApi.listAlertRules();
|
const res = await billingApi.listAlertRules();
|
||||||
setRules(res.list || []);
|
setRules(res.list || []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error('加载预警规则失败', { description: (e as Record<string, any>).message });
|
toast.error('加载预警规则失败', { description: (e as JsonData).message });
|
||||||
} finally {
|
} finally {
|
||||||
setRulesLoading(false);
|
setRulesLoading(false);
|
||||||
}
|
}
|
||||||
@@ -109,7 +110,7 @@ export function CloudBilling() {
|
|||||||
setRecordDialogOpen(true);
|
setRecordDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEditRecord = ( r: any) => {
|
const openEditRecord = (r: JsonData) => {
|
||||||
setEditingRecord(r);
|
setEditingRecord(r);
|
||||||
setRecordDialogOpen(true);
|
setRecordDialogOpen(true);
|
||||||
};
|
};
|
||||||
@@ -119,7 +120,7 @@ export function CloudBilling() {
|
|||||||
setRuleDialogOpen(true);
|
setRuleDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEditRule = ( r: any) => {
|
const openEditRule = (r: JsonData) => {
|
||||||
setEditingRule(r);
|
setEditingRule(r);
|
||||||
setRuleDialogOpen(true);
|
setRuleDialogOpen(true);
|
||||||
};
|
};
|
||||||
@@ -133,7 +134,7 @@ export function CloudBilling() {
|
|||||||
loadStats();
|
loadStats();
|
||||||
toast('删除成功');
|
toast('删除成功');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error('删除失败', { description: (e as Record<string, any>).message });
|
toast.error('删除失败', { description: (e as JsonData).message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -145,7 +146,7 @@ export function CloudBilling() {
|
|||||||
loadRules();
|
loadRules();
|
||||||
toast('删除成功');
|
toast('删除成功');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error('删除失败', { description: (e as Record<string, any>).message });
|
toast.error('删除失败', { description: (e as JsonData).message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -155,7 +156,7 @@ export function CloudBilling() {
|
|||||||
loadRules();
|
loadRules();
|
||||||
loadStats();
|
loadStats();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error('操作失败', { description: (e as Record<string, any>).message });
|
toast.error('操作失败', { description: (e as JsonData).message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -222,7 +223,7 @@ export function CloudBilling() {
|
|||||||
<span className="text-sm font-medium text-amber-300">成本预警</span>
|
<span className="text-sm font-medium text-amber-300">成本预警</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{stats.alerts.map(( a: any, i: number) => (
|
{stats.alerts.map((a: JsonData, i: number) => (
|
||||||
<div key={i} className="flex items-center justify-between text-sm">
|
<div key={i} className="flex items-center justify-between text-sm">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge variant="outline" className="border-amber-500/50 text-amber-300">{a.rule_name}</Badge>
|
<Badge variant="outline" className="border-amber-500/50 text-amber-300">{a.rule_name}</Badge>
|
||||||
@@ -256,7 +257,7 @@ export function CloudBilling() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex h-8 rounded-lg overflow-hidden">
|
<div className="flex h-8 rounded-lg overflow-hidden">
|
||||||
{stats.provider_dist.map(( p: any, i: number) => (
|
{stats.provider_dist.map((p: JsonData, i: number) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className={`${providerColors[p.provider] || 'bg-slate-600'} flex items-center justify-center text-xs text-white font-medium`}
|
className={`${providerColors[p.provider] || 'bg-slate-600'} flex items-center justify-center text-xs text-white font-medium`}
|
||||||
@@ -268,7 +269,7 @@ export function CloudBilling() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||||
{stats.provider_dist.map(( p: any, i: number) => (
|
{stats.provider_dist.map((p: JsonData, i: number) => (
|
||||||
<div key={i} className="flex items-center justify-between text-sm">
|
<div key={i} className="flex items-center justify-between text-sm">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className={`w-3 h-3 rounded ${providerColors[p.provider] || 'bg-slate-600'}`} />
|
<div className={`w-3 h-3 rounded ${providerColors[p.provider] || 'bg-slate-600'}`} />
|
||||||
|
|||||||
@@ -25,9 +25,10 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cloudApi } from '@/api';
|
import { cloudApi } from '@/api';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export function DomainManager() {
|
export function DomainManager() {
|
||||||
const [domains, setDomains] = useState<any[]>([]);
|
const [domains, setDomains] = useState<JsonData[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [selectedStatus, setSelectedStatus] = useState<string>('all');
|
const [selectedStatus, setSelectedStatus] = useState<string>('all');
|
||||||
@@ -35,7 +36,7 @@ export function DomainManager() {
|
|||||||
// 创建/编辑域名表单状态
|
// 创建/编辑域名表单状态
|
||||||
const [formOpen, setFormOpen] = useState(false);
|
const [formOpen, setFormOpen] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [editingDomain, setEditingDomain] = useState<any>(null);
|
const [editingDomain, setEditingDomain] = useState<JsonData>(null);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
registrar: '',
|
registrar: '',
|
||||||
@@ -66,7 +67,7 @@ export function DomainManager() {
|
|||||||
setFormOpen(true);
|
setFormOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenEdit = ( domain: any) => {
|
const handleOpenEdit = (domain: JsonData) => {
|
||||||
setEditingDomain(domain);
|
setEditingDomain(domain);
|
||||||
// 后端返回 RFC3339 时间,截取 YYYY-MM-DD 部分给 date input
|
// 后端返回 RFC3339 时间,截取 YYYY-MM-DD 部分给 date input
|
||||||
const toDateInput = (v: string) => (v ? v.substring(0, 10) : '');
|
const toDateInput = (v: string) => (v ? v.substring(0, 10) : '');
|
||||||
@@ -87,7 +88,7 @@ export function DomainManager() {
|
|||||||
if (!formData.name.trim()) return;
|
if (!formData.name.trim()) return;
|
||||||
try {
|
try {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
const payload: Record<string, any> = {
|
const payload: JsonData = {
|
||||||
name: formData.name.trim(),
|
name: formData.name.trim(),
|
||||||
registrar: formData.registrar.trim(),
|
registrar: formData.registrar.trim(),
|
||||||
status: formData.status,
|
status: formData.status,
|
||||||
@@ -114,7 +115,7 @@ export function DomainManager() {
|
|||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res: Record<string, any> = await cloudApi.getDomains();
|
const res: JsonData = await cloudApi.getDomains();
|
||||||
setDomains(res?.list || []);
|
setDomains(res?.list || []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载域名列表失败:', e);
|
console.error('加载域名列表失败:', e);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { platformApi } from '@/api';
|
import { platformApi } from '@/api';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
const typeLabels: Record<string, string> = {
|
const typeLabels: Record<string, string> = {
|
||||||
public: '公有云',
|
public: '公有云',
|
||||||
@@ -44,12 +45,12 @@ const typeColors: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function PlatformManager() {
|
export function PlatformManager() {
|
||||||
const [platforms, setPlatforms] = useState<any[]>([]);
|
const [platforms, setPlatforms] = useState<JsonData[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [selectedType, setSelectedType] = useState('all');
|
const [selectedType, setSelectedType] = useState('all');
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [editingItem, setEditingItem] = useState<any>(null);
|
const [editingItem, setEditingItem] = useState<JsonData>(null);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '', code: '', type: 'public', status: 'active',
|
name: '', code: '', type: 'public', status: 'active',
|
||||||
@@ -60,7 +61,7 @@ export function PlatformManager() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await platformApi.list();
|
const res = await platformApi.list();
|
||||||
const items = (res as Record<string, any>)?.list || [];
|
const items = (res as JsonData)?.list || [];
|
||||||
setPlatforms(items);
|
setPlatforms(items);
|
||||||
} catch {
|
} catch {
|
||||||
setPlatforms([]);
|
setPlatforms([]);
|
||||||
@@ -84,7 +85,7 @@ export function PlatformManager() {
|
|||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEdit = ( item: any) => {
|
const openEdit = (item: JsonData) => {
|
||||||
setEditingItem(item);
|
setEditingItem(item);
|
||||||
setFormData({
|
setFormData({
|
||||||
name: item.name || '', code: item.code || '', type: item.type || 'public',
|
name: item.name || '', code: item.code || '', type: item.type || 'public',
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
import { regionApi } from '@/api';
|
import { regionApi } from '@/api';
|
||||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
const providerNames: Record<string, string> = {
|
const providerNames: Record<string, string> = {
|
||||||
aws: 'AWS',
|
aws: 'AWS',
|
||||||
@@ -59,13 +60,13 @@ const providerColors: Record<string, string> = {
|
|||||||
|
|
||||||
export function RegionManager() {
|
export function RegionManager() {
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const [regions, setRegions] = useState<any[]>([]);
|
const [regions, setRegions] = useState<JsonData[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [selectedProvider, setSelectedProvider] = useState('all');
|
const [selectedProvider, setSelectedProvider] = useState('all');
|
||||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [editingItem, setEditingItem] = useState<any>(null);
|
const [editingItem, setEditingItem] = useState<JsonData>(null);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '', code: '', provider: 'aliyun', location: '',
|
name: '', code: '', provider: 'aliyun', location: '',
|
||||||
@@ -76,7 +77,7 @@ export function RegionManager() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await regionApi.list();
|
const res = await regionApi.list();
|
||||||
const items = (res as Record<string, any>)?.list || [];
|
const items = (res as JsonData)?.list || [];
|
||||||
setRegions(items);
|
setRegions(items);
|
||||||
} catch {
|
} catch {
|
||||||
setRegions([]);
|
setRegions([]);
|
||||||
@@ -113,7 +114,7 @@ export function RegionManager() {
|
|||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEdit = ( item: any) => {
|
const openEdit = (item: JsonData) => {
|
||||||
setEditingItem(item);
|
setEditingItem(item);
|
||||||
setFormData({
|
setFormData({
|
||||||
name: item.name || '', code: item.code || '', provider: item.provider || 'aliyun',
|
name: item.name || '', code: item.code || '', provider: item.provider || 'aliyun',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
KubernetesClusterCard,
|
KubernetesClusterCard,
|
||||||
} from './DashboardRecent';
|
} from './DashboardRecent';
|
||||||
import { SkeletonDashboard } from '@/components/layout/SkeletonCard';
|
import { SkeletonDashboard } from '@/components/layout/SkeletonCard';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dashboard - 优化后的仪表盘页面
|
* Dashboard - 优化后的仪表盘页面
|
||||||
@@ -30,13 +31,13 @@ export function Dashboard() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [initialLoading, setInitialLoading] = useState(true);
|
const [initialLoading, setInitialLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [stats, setStats] = useState<any>(null);
|
const [stats, setStats] = useState<JsonData>(null);
|
||||||
const [servers, setServers] = useState<any[]>([]);
|
const [servers, setServers] = useState<JsonData[]>([]);
|
||||||
const [clusters, setClusters] = useState<any[]>([]);
|
const [clusters, setClusters] = useState<JsonData[]>([]);
|
||||||
const [alerts, setAlerts] = useState<any[]>([]);
|
const [alerts, setAlerts] = useState<JsonData[]>([]);
|
||||||
const [releases, setReleases] = useState<any[]>([]);
|
const [releases, setReleases] = useState<JsonData[]>([]);
|
||||||
const [expCerts, setExpCerts] = useState<any[]>([]);
|
const [expCerts, setExpCerts] = useState<JsonData[]>([]);
|
||||||
const [metrics, setMetrics] = useState<any[]>([]);
|
const [metrics, setMetrics] = useState<JsonData[]>([]);
|
||||||
|
|
||||||
const refreshData = useCallback(async () => {
|
const refreshData = useCallback(async () => {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
import { Cpu, HardDrive, Activity, MemoryStick, Database } from 'lucide-react';
|
import { Cpu, HardDrive, Activity, MemoryStick, Database } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
// Cloud platform color mapping (theme-aware)
|
// Cloud platform color mapping (theme-aware)
|
||||||
const CLOUD_COLORS: Record<string, string> = {
|
const CLOUD_COLORS: Record<string, string> = {
|
||||||
@@ -32,7 +33,7 @@ const CLOUD_COLORS: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
const FALLBACK_CLOUD_COLORS = ['#ff6a00', '#ff9900', '#006eff', '#ff0000', '#0078d4', '#10b981', '#a855f7', '#ec4899'];
|
const FALLBACK_CLOUD_COLORS = ['#ff6a00', '#ff9900', '#006eff', '#ff0000', '#0078d4', '#10b981', '#a855f7', '#ec4899'];
|
||||||
|
|
||||||
export function computeMonthChange( monthTrend: any[]): {
|
export function computeMonthChange( monthTrend: JsonData[]): {
|
||||||
pct: number;
|
pct: number;
|
||||||
label: string;
|
label: string;
|
||||||
direction: 'up' | 'down' | 'neutral';
|
direction: 'up' | 'down' | 'neutral';
|
||||||
@@ -65,7 +66,7 @@ function generateMetricsData(avgCpu: number, avgMemory: number, avgDisk: number,
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMetricsFromReal( metrics: any[], avgDisk: number) {
|
function buildMetricsFromReal( metrics: JsonData[], avgDisk: number) {
|
||||||
if (!metrics || metrics.length === 0) return [];
|
if (!metrics || metrics.length === 0) return [];
|
||||||
const buckets: Record<string, { cpu?: number; memory?: number }> = {};
|
const buckets: Record<string, { cpu?: number; memory?: number }> = {};
|
||||||
for (const m of metrics) {
|
for (const m of metrics) {
|
||||||
@@ -88,12 +89,12 @@ function buildMetricsFromReal( metrics: any[], avgDisk: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Custom tooltip for charts (theme-aware)
|
// Custom tooltip for charts (theme-aware)
|
||||||
function CustomTooltip({ active, payload, label }: any) {
|
function CustomTooltip({ active, payload, label }: JsonData) {
|
||||||
if (!active || !payload) return null;
|
if (!active || !payload) return null;
|
||||||
return (
|
return (
|
||||||
<div className="chart-tooltip px-3 py-2">
|
<div className="chart-tooltip px-3 py-2">
|
||||||
<p className="text-xs font-medium mb-1.5" style={{ color: 'var(--dg-text-secondary)' }}>{label}</p>
|
<p className="text-xs font-medium mb-1.5" style={{ color: 'var(--dg-text-secondary)' }}>{label}</p>
|
||||||
{payload.map(( entry: any, index: number) => (
|
{payload.map((entry: JsonData, index: number) => (
|
||||||
<div key={index} className="flex items-center gap-2 text-xs">
|
<div key={index} className="flex items-center gap-2 text-xs">
|
||||||
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: entry.color }} />
|
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: entry.color }} />
|
||||||
<span style={{ color: 'var(--dg-text-muted)' }}>{entry.name}:</span>
|
<span style={{ color: 'var(--dg-text-muted)' }}>{entry.name}:</span>
|
||||||
@@ -106,9 +107,9 @@ function CustomTooltip({ active, payload, label }: any) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CostOverviewCard({ costStats }: { costStats: Record<string, any> }) {
|
export function CostOverviewCard({ costStats }: { costStats: JsonData }) {
|
||||||
const cloudCosts: Record<string, any>[] = costStats?.cloud_costs ?? [];
|
const cloudCosts: JsonData[] = costStats?.cloud_costs ?? [];
|
||||||
const monthTrend: Record<string, any>[] = costStats?.month_trend ?? [];
|
const monthTrend: JsonData[] = costStats?.month_trend ?? [];
|
||||||
|
|
||||||
const costData = cloudCosts.map((c, i) => ({
|
const costData = cloudCosts.map((c, i) => ({
|
||||||
name: c.cloud,
|
name: c.cloud,
|
||||||
@@ -201,12 +202,12 @@ export function CostOverviewCard({ costStats }: { costStats: Record<string, any>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TopResourceUsageCard({ topUsage }: { topUsage: any}) {
|
export function TopResourceUsageCard({ topUsage }: { topUsage: JsonData}) {
|
||||||
const topCpuServers = (topUsage?.cpu ?? []).slice(0, 5);
|
const topCpuServers = (topUsage?.cpu ?? []).slice(0, 5);
|
||||||
const topMemoryServers = (topUsage?.memory ?? []).slice(0, 5);
|
const topMemoryServers = (topUsage?.memory ?? []).slice(0, 5);
|
||||||
const topDiskServers = (topUsage?.disk ?? []).slice(0, 5);
|
const topDiskServers = (topUsage?.disk ?? []).slice(0, 5);
|
||||||
|
|
||||||
const renderList = ( items: any[],
|
const renderList = ( items: JsonData[],
|
||||||
color: string,
|
color: string,
|
||||||
BarColor: string,
|
BarColor: string,
|
||||||
icon: React.ReactNode,
|
icon: React.ReactNode,
|
||||||
@@ -280,7 +281,7 @@ export function ResourceUsageCard({
|
|||||||
avgCpu: number;
|
avgCpu: number;
|
||||||
avgMemory: number;
|
avgMemory: number;
|
||||||
avgDisk: number;
|
avgDisk: number;
|
||||||
metrics?: any[];
|
metrics?: JsonData[];
|
||||||
}) {
|
}) {
|
||||||
const realData = buildMetricsFromReal(metrics ?? [], avgDisk);
|
const realData = buildMetricsFromReal(metrics ?? [], avgDisk);
|
||||||
const data = realData.length > 0 ? realData : generateMetricsData(avgCpu, avgMemory, avgDisk, 24);
|
const data = realData.length > 0 ? realData : generateMetricsData(avgCpu, avgMemory, avgDisk, 24);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|||||||
import { Plus, Key, Globe, Container } from 'lucide-react';
|
import { Plus, Key, Globe, Container } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { credentialApi, deployApi } from '@/api';
|
import { credentialApi, deployApi } from '@/api';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
interface Repo {
|
interface Repo {
|
||||||
id: number; name: string; url: string; credential_id: number | null;
|
id: number; name: string; url: string; credential_id: number | null;
|
||||||
@@ -47,7 +48,7 @@ export default function CredentialFormDialog({ open, onOpenChange, type, editId,
|
|||||||
|
|
||||||
const loadCredentials = async () => {
|
const loadCredentials = async () => {
|
||||||
try {
|
try {
|
||||||
const res: Record<string, any> = await credentialApi.list({ page_size: 100 });
|
const res: JsonData = await credentialApi.list({ page_size: 100 });
|
||||||
setCredentials(res?.list || []);
|
setCredentials(res?.list || []);
|
||||||
} catch { /* silent */ }
|
} catch { /* silent */ }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { useConfirm } from '@/components/ui/confirm-dialog';
|
|||||||
import { deployApi } from '@/api';
|
import { deployApi } from '@/api';
|
||||||
import CredentialFormDialog from './CredentialFormDialog';
|
import CredentialFormDialog from './CredentialFormDialog';
|
||||||
import ToolConfigTab from './ToolConfigTab';
|
import ToolConfigTab from './ToolConfigTab';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
interface Repo {
|
interface Repo {
|
||||||
id: number; name: string; description: string; url: string;
|
id: number; name: string; description: string; url: string;
|
||||||
@@ -110,16 +111,16 @@ export default function DeployCredentials() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const [rR, rReg, rEnv] = await Promise.all([deployApi.getRepos(), deployApi.getRegistries(), deployApi.getEnvironments()]);
|
const [rR, rReg, rEnv] = await Promise.all([deployApi.getRepos(), deployApi.getRegistries(), deployApi.getEnvironments()]);
|
||||||
setRepos((rR as Record<string, any>)?.list || []);
|
setRepos((rR as JsonData)?.list || []);
|
||||||
setRegistries((rReg as Record<string, any>)?.list || []);
|
setRegistries((rReg as JsonData)?.list || []);
|
||||||
setEnvironments((rEnv as Record<string, any>)?.list || []);
|
setEnvironments((rEnv as JsonData)?.list || []);
|
||||||
} catch { toast.error('获取数据失败'); }
|
} catch { toast.error('获取数据失败'); }
|
||||||
finally { setLoading(false); }
|
finally { setLoading(false); }
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => { fetchAll(); }, []);
|
useEffect(() => { fetchAll(); }, []);
|
||||||
|
|
||||||
const handleDelete = async (type: 'repo' | 'registry' | 'env', item: any) => {
|
const handleDelete = async (type: 'repo' | 'registry' | 'env', item: JsonData) => {
|
||||||
const ok = await _confirm({ title: '删除', description: `确定删除「${item.name || item.display_name}」?`, confirmText: '删除', variant: 'destructive' });
|
const ok = await _confirm({ title: '删除', description: `确定删除「${item.name || item.display_name}」?`, confirmText: '删除', variant: 'destructive' });
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { deployApi, userApi, pipelineApi } from '@/api';
|
import { deployApi, userApi, pipelineApi } from '@/api';
|
||||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
interface ItemRole {
|
interface ItemRole {
|
||||||
id: number; name: string; code: string; pattern: string; role: string;
|
id: number; name: string; code: string; pattern: string; role: string;
|
||||||
@@ -27,11 +28,11 @@ export default function DeployRBAC() {
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
// Pipeline list for live preview
|
// Pipeline list for live preview
|
||||||
const [pipelines, setPipelines] = useState<any[]>([]);
|
const [pipelines, setPipelines] = useState<JsonData[]>([]);
|
||||||
const [previewRoleId, setPreviewRoleId] = useState<number | null>(null);
|
const [previewRoleId, setPreviewRoleId] = useState<number | null>(null);
|
||||||
|
|
||||||
// User assignment
|
// User assignment
|
||||||
const [users, setUsers] = useState<any[]>([]);
|
const [users, setUsers] = useState<JsonData[]>([]);
|
||||||
const [assignments, setAssignments] = useState<AssignedUser[]>([]);
|
const [assignments, setAssignments] = useState<AssignedUser[]>([]);
|
||||||
const [selectedUserId, setSelectedUserId] = useState<string>('');
|
const [selectedUserId, setSelectedUserId] = useState<string>('');
|
||||||
const [assignRoleId, setAssignRoleId] = useState<string>('');
|
const [assignRoleId, setAssignRoleId] = useState<string>('');
|
||||||
@@ -60,9 +61,9 @@ export default function DeployRBAC() {
|
|||||||
pipelineApi.getPipelines({ page_size: 200 }),
|
pipelineApi.getPipelines({ page_size: 200 }),
|
||||||
userApi.getUsers({ page_size: 500 }),
|
userApi.getUsers({ page_size: 500 }),
|
||||||
]);
|
]);
|
||||||
setRoles(Array.isArray(rolesRes) ? rolesRes : (rolesRes as Record<string, any>)?.list || (rolesRes as Record<string, any>)?.data || []);
|
setRoles(Array.isArray(rolesRes) ? rolesRes : (rolesRes as JsonData)?.list || (rolesRes as JsonData)?.data || []);
|
||||||
setPipelines(Array.isArray(pipesRes) ? pipesRes : (pipesRes as Record<string, any>)?.list || (pipesRes as Record<string, any>)?.data || []);
|
setPipelines(Array.isArray(pipesRes) ? pipesRes : (pipesRes as JsonData)?.list || (pipesRes as JsonData)?.data || []);
|
||||||
const uList = Array.isArray(usersRes) ? usersRes : (usersRes as Record<string, any>)?.list || (usersRes as Record<string, any>)?.data || [];
|
const uList = Array.isArray(usersRes) ? usersRes : (usersRes as JsonData)?.list || (usersRes as JsonData)?.data || [];
|
||||||
setUsers(uList);
|
setUsers(uList);
|
||||||
} catch { /* */ }
|
} catch { /* */ }
|
||||||
finally { setLoading(false); }
|
finally { setLoading(false); }
|
||||||
@@ -73,7 +74,7 @@ export default function DeployRBAC() {
|
|||||||
const loadAssignments = async () => {
|
const loadAssignments = async () => {
|
||||||
if (!selectedUserId) { setAssignments([]); return; }
|
if (!selectedUserId) { setAssignments([]); return; }
|
||||||
try {
|
try {
|
||||||
const res: Record<string, any> = await deployApi.getUserItemRoles(Number(selectedUserId));
|
const res: JsonData = await deployApi.getUserItemRoles(Number(selectedUserId));
|
||||||
setAssignments(Array.isArray(res) ? res : res?.data || res?.list || []);
|
setAssignments(Array.isArray(res) ? res : res?.data || res?.list || []);
|
||||||
} catch { setAssignments([]); }
|
} catch { setAssignments([]); }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import { GitBranch, GitCommit, Clock, ChevronDown, Loader2 } from 'lucide-react';
|
import { GitBranch, GitCommit, Clock, ChevronDown, Loader2 } from 'lucide-react';
|
||||||
import type { PipelineStage } from './pipeline-helpers';
|
import type { PipelineStage } from './pipeline-helpers';
|
||||||
import { parseStages, getStatusBadge, getStageStatusIcon } from './pipeline-helpers';
|
import { parseStages, getStatusBadge, getStageStatusIcon } from './pipeline-helpers';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
// 流水线详情弹窗(阶段/配置/历史三 Tab) // Logan.Li
|
// 流水线详情弹窗(阶段/配置/历史三 Tab) // Logan.Li
|
||||||
export function PipelineDetailDialog({
|
export function PipelineDetailDialog({
|
||||||
@@ -29,8 +30,8 @@ export function PipelineDetailDialog({
|
|||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
selectedPipeline: any;
|
selectedPipeline?: JsonData;
|
||||||
pipelineRuns: any[];
|
pipelineRuns: JsonData[];
|
||||||
loadingRuns: boolean;
|
loadingRuns: boolean;
|
||||||
activeStage: string | null;
|
activeStage: string | null;
|
||||||
setActiveStage: (id: string | null) => void;
|
setActiveStage: (id: string | null) => void;
|
||||||
@@ -131,7 +132,7 @@ export function PipelineDetailDialog({
|
|||||||
<div className="text-[var(--dg-text-secondary)] text-center py-8 text-sm">暂无执行记录</div>
|
<div className="text-[var(--dg-text-secondary)] text-center py-8 text-sm">暂无执行记录</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{pipelineRuns.map(( run: any) => (
|
{pipelineRuns.map((run: JsonData) => (
|
||||||
<div
|
<div
|
||||||
key={run.id}
|
key={run.id}
|
||||||
className="flex items-center gap-3 p-3 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)]"
|
className="flex items-center gap-3 p-3 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)]"
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { pipelineApi, deployApi } from '@/api';
|
import { pipelineApi, deployApi } from '@/api';
|
||||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
// ---- types ----
|
// ---- types ----
|
||||||
interface Task {
|
interface Task {
|
||||||
@@ -58,7 +59,7 @@ interface LogEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const stageTypeLabels: Record<string, string> = { source: '拉取代码', build: '构建', deploy: '部署', test: '测试', custom: '自定义' };
|
const stageTypeLabels: Record<string, string> = { source: '拉取代码', build: '构建', deploy: '部署', test: '测试', custom: '自定义' };
|
||||||
const taskIcons: Record<string, any> = { GitBranch, Wrench, Server, FlaskConical, Zap, Terminal, FileCode, Container };
|
const taskIcons: JsonData = { GitBranch, Wrench, Server, FlaskConical, Zap, Terminal, FileCode, Container };
|
||||||
|
|
||||||
function uid() { return `tsk-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; }
|
function uid() { return `tsk-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; }
|
||||||
function fmtDuration(s: number | undefined): string {
|
function fmtDuration(s: number | undefined): string {
|
||||||
@@ -119,7 +120,7 @@ function TaskEditor({ task, onUpdate }: { task: Task | null; onUpdate: (t: Task)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ====== Log Dialog ======
|
// ====== Log Dialog ======
|
||||||
function LogDialog({ open, onClose, run, logs }: { open: boolean; onClose: () => void; run: any; logs: LogEntry[] }) {
|
function LogDialog({ open, onClose, run, logs }: { open: boolean; onClose: () => void; run?: JsonData; logs: LogEntry[] }) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onClose}>
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
||||||
@@ -160,16 +161,16 @@ function LogDialog({ open, onClose, run, logs }: { open: boolean; onClose: () =>
|
|||||||
export default function PipelineEditor() {
|
export default function PipelineEditor() {
|
||||||
const { code } = useParams<{ code: string }>();
|
const { code } = useParams<{ code: string }>();
|
||||||
const pipelineCode = code || '';
|
const pipelineCode = code || '';
|
||||||
const [pipeline, setPipeline] = useState<any>(null);
|
const [pipeline, setPipeline] = useState<JsonData>(null);
|
||||||
const [stages, setStages] = useState<Stage[]>([]);
|
const [stages, setStages] = useState<Stage[]>([]);
|
||||||
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
|
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
|
||||||
const [expandedStages, setExpandedStages] = useState<Record<string, boolean>>({});
|
const [expandedStages, setExpandedStages] = useState<Record<string, boolean>>({});
|
||||||
const [activeTab, setActiveTab] = useState('flow');
|
const [activeTab, setActiveTab] = useState('flow');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [runs, setRuns] = useState<any[]>([]);
|
const [runs, setRuns] = useState<JsonData[]>([]);
|
||||||
const [environments, setEnvironments] = useState<any[]>([]);
|
const [environments, setEnvironments] = useState<JsonData[]>([]);
|
||||||
const [members, setMembers] = useState<any[]>([]);
|
const [members, setMembers] = useState<JsonData[]>([]);
|
||||||
const [loadingMembers, setLoadingMembers] = useState(false);
|
const [loadingMembers, setLoadingMembers] = useState(false);
|
||||||
|
|
||||||
// trigger config
|
// trigger config
|
||||||
@@ -186,7 +187,7 @@ export default function PipelineEditor() {
|
|||||||
|
|
||||||
// log dialog
|
// log dialog
|
||||||
const [logOpen, setLogOpen] = useState(false);
|
const [logOpen, setLogOpen] = useState(false);
|
||||||
const [logRun, setLogRun] = useState<any>(null);
|
const [logRun, setLogRun] = useState<JsonData>(null);
|
||||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||||
|
|
||||||
const _confirm = useConfirm();
|
const _confirm = useConfirm();
|
||||||
@@ -194,7 +195,7 @@ export default function PipelineEditor() {
|
|||||||
const loadEnvironments = useCallback(async () => {
|
const loadEnvironments = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await deployApi.getEnvironments();
|
const res = await deployApi.getEnvironments();
|
||||||
setEnvironments(Array.isArray(res) ? res : (res as Record<string, any>)?.list || []);
|
setEnvironments(Array.isArray(res) ? res : (res as JsonData)?.list || []);
|
||||||
} catch { /* silent */ }
|
} catch { /* silent */ }
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -202,7 +203,7 @@ export default function PipelineEditor() {
|
|||||||
setLoadingMembers(true);
|
setLoadingMembers(true);
|
||||||
try {
|
try {
|
||||||
const res = await pipelineApi.getMembers(pipelineCode);
|
const res = await pipelineApi.getMembers(pipelineCode);
|
||||||
setMembers(Array.isArray(res) ? res : (res as Record<string, any>)?.data || []);
|
setMembers(Array.isArray(res) ? res : (res as JsonData)?.data || []);
|
||||||
} catch { setMembers([]); }
|
} catch { setMembers([]); }
|
||||||
finally { setLoadingMembers(false); }
|
finally { setLoadingMembers(false); }
|
||||||
}, [pipelineCode]);
|
}, [pipelineCode]);
|
||||||
@@ -210,7 +211,7 @@ export default function PipelineEditor() {
|
|||||||
const loadRuns = useCallback(async () => {
|
const loadRuns = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await pipelineApi.getRuns(pipelineCode);
|
const res = await pipelineApi.getRuns(pipelineCode);
|
||||||
setRuns(Array.isArray(res) ? res : (res as Record<string, any>)?.list || []);
|
setRuns(Array.isArray(res) ? res : (res as JsonData)?.list || []);
|
||||||
} catch { setRuns([]); }
|
} catch { setRuns([]); }
|
||||||
}, [pipelineCode]);
|
}, [pipelineCode]);
|
||||||
|
|
||||||
@@ -218,7 +219,7 @@ export default function PipelineEditor() {
|
|||||||
(async () => {
|
(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const p = await pipelineApi.getPipelineDetail(pipelineCode) as Record<string, any>;
|
const p = await pipelineApi.getPipelineDetail(pipelineCode) as JsonData;
|
||||||
// http.get 已自动解包 json.data,p 即为 pipeline 对象
|
// http.get 已自动解包 json.data,p 即为 pipeline 对象
|
||||||
if (!p || !p.id) { setPipeline(null); setLoading(false); return; }
|
if (!p || !p.id) { setPipeline(null); setLoading(false); return; }
|
||||||
setPipeline(p);
|
setPipeline(p);
|
||||||
@@ -294,11 +295,11 @@ export default function PipelineEditor() {
|
|||||||
} catch { toast.error('停止失败'); }
|
} catch { toast.error('停止失败'); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleViewLog = async ( run: any) => {
|
const handleViewLog = async (run: JsonData) => {
|
||||||
setLogRun(run);
|
setLogRun(run);
|
||||||
setLogOpen(true);
|
setLogOpen(true);
|
||||||
try {
|
try {
|
||||||
const data = await pipelineApi.getRunLog(pipelineCode, run.id) as Record<string, any>;
|
const data = await pipelineApi.getRunLog(pipelineCode, run.id) as JsonData;
|
||||||
setLogs(data?.logs || []);
|
setLogs(data?.logs || []);
|
||||||
} catch { setLogs([]); }
|
} catch { setLogs([]); }
|
||||||
};
|
};
|
||||||
@@ -482,7 +483,7 @@ export default function PipelineEditor() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{runs.map(( r: any) => (
|
{runs.map((r: JsonData) => (
|
||||||
<tr key={r.id} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
<tr key={r.id} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||||
<td className="px-3 py-2 font-medium text-[var(--dg-text-primary)]">#{r.build_number || r.id}</td>
|
<td className="px-3 py-2 font-medium text-[var(--dg-text-primary)]">#{r.build_number || r.id}</td>
|
||||||
<td className="px-3 py-2">
|
<td className="px-3 py-2">
|
||||||
@@ -552,8 +553,8 @@ export default function PipelineEditor() {
|
|||||||
|
|
||||||
{/* Members list — split direct users vs group assignments */}
|
{/* Members list — split direct users vs group assignments */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const directUsers = members.filter(( m: any) => m.username);
|
const directUsers = members.filter((m: JsonData) => m.username);
|
||||||
const groupAssignments = members.filter(( m: any) => m.group_id);
|
const groupAssignments = members.filter((m: JsonData) => m.group_id);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Direct users */}
|
{/* Direct users */}
|
||||||
@@ -568,7 +569,7 @@ export default function PipelineEditor() {
|
|||||||
<div className="py-4 text-center text-[var(--dg-text-secondary)] text-xs">暂无直接用户</div>
|
<div className="py-4 text-center text-[var(--dg-text-secondary)] text-xs">暂无直接用户</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-[var(--dg-border)]">
|
<div className="divide-y divide-[var(--dg-border)]">
|
||||||
{directUsers.map(( m: any) => (
|
{directUsers.map((m: JsonData) => (
|
||||||
<div key={m.id} className="flex items-center justify-between px-4 py-3">
|
<div key={m.id} className="flex items-center justify-between px-4 py-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-8 h-8 rounded-full bg-blue-500/20 flex items-center justify-center"><Users className="w-4 h-4 text-blue-400" /></div>
|
<div className="w-8 h-8 rounded-full bg-blue-500/20 flex items-center justify-center"><Users className="w-4 h-4 text-blue-400" /></div>
|
||||||
@@ -597,7 +598,7 @@ export default function PipelineEditor() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-[var(--dg-border)]">
|
<div className="divide-y divide-[var(--dg-border)]">
|
||||||
{groupAssignments.map(( m: any) => (
|
{groupAssignments.map((m: JsonData) => (
|
||||||
<div key={m.id} className="flex items-center justify-between px-4 py-3">
|
<div key={m.id} className="flex items-center justify-between px-4 py-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-8 h-8 rounded-full bg-purple-500/20 flex items-center justify-center"><Shield className="w-4 h-4 text-purple-400" /></div>
|
<div className="w-8 h-8 rounded-full bg-purple-500/20 flex items-center justify-center"><Shield className="w-4 h-4 text-purple-400" /></div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { pipelineApi } from '@/api';
|
import { pipelineApi } from '@/api';
|
||||||
import { FileBrowserDialog } from './FileBrowserDialog';
|
import { FileBrowserDialog } from './FileBrowserDialog';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
// ═══════════ 阶段定义 (与 PipelineManager 共用) ═══════════
|
// ═══════════ 阶段定义 (与 PipelineManager 共用) ═══════════
|
||||||
const STAGE_DEFS = [
|
const STAGE_DEFS = [
|
||||||
@@ -61,7 +62,7 @@ const STATUS_BADGES: Record<string, string> = {
|
|||||||
cancelled: 'bg-amber-500/20 text-amber-400 border-amber-500/30',
|
cancelled: 'bg-amber-500/20 text-amber-400 border-amber-500/30',
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_ICONS: Record<string, any> = {
|
const STATUS_ICONS: JsonData = {
|
||||||
success: CheckCircle2,
|
success: CheckCircle2,
|
||||||
failed: XCircle,
|
failed: XCircle,
|
||||||
running: Loader2,
|
running: Loader2,
|
||||||
@@ -101,8 +102,8 @@ function getRunStageStats(run: PipelineRun | null): Map<string, string> {
|
|||||||
// 如果 run.stages 有数据,直接用
|
// 如果 run.stages 有数据,直接用
|
||||||
const statuses = parseStageStatuses(run.stages);
|
const statuses = parseStageStatuses(run.stages);
|
||||||
statuses.forEach(s => {
|
statuses.forEach(s => {
|
||||||
if (s.name || (s as Record<string, any>).type) {
|
if (s.name || (s as JsonData).type) {
|
||||||
const key = (s as Record<string, any>).type || (STAGE_DEFS.find(d => d.label === s.name)?.key);
|
const key = (s as JsonData).type || (STAGE_DEFS.find(d => d.label === s.name)?.key);
|
||||||
if (key) map.set(key, s.status);
|
if (key) map.set(key, s.status);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -128,7 +129,7 @@ function getStageLogSummary(logs: LogEntry[], stageKey: string): string {
|
|||||||
|
|
||||||
export default function PipelineRunDetail() {
|
export default function PipelineRunDetail() {
|
||||||
const { code: pipelineCode } = useParams<{ code: string }>();
|
const { code: pipelineCode } = useParams<{ code: string }>();
|
||||||
const [pipeline, setPipeline] = useState<any>(null);
|
const [pipeline, setPipeline] = useState<JsonData>(null);
|
||||||
const [runs, setRuns] = useState<PipelineRun[]>([]);
|
const [runs, setRuns] = useState<PipelineRun[]>([]);
|
||||||
const [selectedRun, setSelectedRun] = useState<PipelineRun | null>(null);
|
const [selectedRun, setSelectedRun] = useState<PipelineRun | null>(null);
|
||||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||||
@@ -147,7 +148,7 @@ export default function PipelineRunDetail() {
|
|||||||
// 加载流水线详情
|
// 加载流水线详情
|
||||||
const fetchPipeline = useCallback(async () => {
|
const fetchPipeline = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const data: Record<string, any> = await pipelineApi.getByCode(pipelineCode);
|
const data: JsonData = await pipelineApi.getByCode(pipelineCode);
|
||||||
setPipeline(data?.data || data);
|
setPipeline(data?.data || data);
|
||||||
} catch { /* silent, pipeline info optional */ }
|
} catch { /* silent, pipeline info optional */ }
|
||||||
}, [pipelineCode]);
|
}, [pipelineCode]);
|
||||||
@@ -156,7 +157,7 @@ export default function PipelineRunDetail() {
|
|||||||
const fetchRuns = useCallback(async () => {
|
const fetchRuns = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const data: Record<string, any> = await pipelineApi.getRunsByCode(pipelineCode, pageSize);
|
const data: JsonData = await pipelineApi.getRunsByCode(pipelineCode, pageSize);
|
||||||
const list = data?.list || [];
|
const list = data?.list || [];
|
||||||
setRuns(list);
|
setRuns(list);
|
||||||
if (!selectedRun && list.length > 0) {
|
if (!selectedRun && list.length > 0) {
|
||||||
@@ -180,7 +181,7 @@ export default function PipelineRunDetail() {
|
|||||||
// 加载日志
|
// 加载日志
|
||||||
const fetchLogs = useCallback(async (run: PipelineRun) => {
|
const fetchLogs = useCallback(async (run: PipelineRun) => {
|
||||||
try {
|
try {
|
||||||
const data: Record<string, any> = await pipelineApi.getRunLogStream(run.id);
|
const data: JsonData = await pipelineApi.getRunLogStream(run.id);
|
||||||
if (data?.logs) {
|
if (data?.logs) {
|
||||||
const parsed = typeof data.logs === 'string' ? JSON.parse(data.logs) : data.logs;
|
const parsed = typeof data.logs === 'string' ? JSON.parse(data.logs) : data.logs;
|
||||||
setLogs(Array.isArray(parsed) ? parsed : []);
|
setLogs(Array.isArray(parsed) ? parsed : []);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Play } from 'lucide-react';
|
import { Play } from 'lucide-react';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
// 执行流水线弹窗(分支/Commit/环境变量) // Logan.Li
|
// 执行流水线弹窗(分支/Commit/环境变量) // Logan.Li
|
||||||
export function PipelineRunDialog({
|
export function PipelineRunDialog({
|
||||||
@@ -21,7 +22,7 @@ export function PipelineRunDialog({
|
|||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
selectedPipeline: any;
|
selectedPipeline?: JsonData;
|
||||||
onRun: () => void;
|
onRun: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { AlertCircle, X, Check, Loader2 } from 'lucide-react';
|
import { AlertCircle, X, Check, Loader2 } from 'lucide-react';
|
||||||
import { releaseApi } from '@/api';
|
import { releaseApi } from '@/api';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
interface ReleaseApproveDialogProps {
|
interface ReleaseApproveDialogProps {
|
||||||
release: any | null;
|
release: JsonData | null;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ import { Rocket, CheckCircle, Loader2, RotateCcw } from 'lucide-react';
|
|||||||
import { releaseApi } from '@/api';
|
import { releaseApi } from '@/api';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { PIPELINE_STEPS } from './releaseHelpers';
|
import { PIPELINE_STEPS } from './releaseHelpers';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
interface ReleaseDetailDialogProps {
|
interface ReleaseDetailDialogProps {
|
||||||
release: any | null;
|
release: JsonData | null;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { deployApi } from '@/api';
|
import { deployApi } from '@/api';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
interface ToolConfig {
|
interface ToolConfig {
|
||||||
id: number; name: string; display_name: string; description: string;
|
id: number; name: string; display_name: string; description: string;
|
||||||
@@ -55,8 +56,8 @@ export default function ToolConfigTab() {
|
|||||||
const fetchTools = async () => {
|
const fetchTools = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res: Record<string, any> = await deployApi.getTools();
|
const res: JsonData = await deployApi.getTools();
|
||||||
setTools((res as Record<string, any>)?.list || []);
|
setTools((res as JsonData)?.list || []);
|
||||||
} catch { toast.error('获取工具配置失败'); }
|
} catch { toast.error('获取工具配置失败'); }
|
||||||
finally { setLoading(false); }
|
finally { setLoading(false); }
|
||||||
};
|
};
|
||||||
@@ -65,8 +66,8 @@ export default function ToolConfigTab() {
|
|||||||
|
|
||||||
const openVersionDialog = async (tool: ToolConfig) => {
|
const openVersionDialog = async (tool: ToolConfig) => {
|
||||||
try {
|
try {
|
||||||
const res: Record<string, any> = await deployApi.getToolVersions(tool.id);
|
const res: JsonData = await deployApi.getToolVersions(tool.id);
|
||||||
const versions = (res as Record<string, any>)?.versions || [];
|
const versions = (res as JsonData)?.versions || [];
|
||||||
if (versions.length === 0) {
|
if (versions.length === 0) {
|
||||||
// No versions — install directly
|
// No versions — install directly
|
||||||
handleInstall(tool);
|
handleInstall(tool);
|
||||||
@@ -99,7 +100,7 @@ export default function ToolConfigTab() {
|
|||||||
|
|
||||||
const handleCheckStatus = async (tool: ToolConfig) => {
|
const handleCheckStatus = async (tool: ToolConfig) => {
|
||||||
try {
|
try {
|
||||||
const res: Record<string, any> = await deployApi.checkTool(tool.id);
|
const res: JsonData = await deployApi.checkTool(tool.id);
|
||||||
toast.success(res?.available ? `${tool.display_name} 可用` : `${tool.display_name} 不可用`);
|
toast.success(res?.available ? `${tool.display_name} 可用` : `${tool.display_name} 不可用`);
|
||||||
fetchTools();
|
fetchTools();
|
||||||
} catch { toast.error('检测失败'); }
|
} catch { toast.error('检测失败'); }
|
||||||
@@ -119,9 +120,9 @@ export default function ToolConfigTab() {
|
|||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
try {
|
try {
|
||||||
// Simulate a quick reachability test by fetching the version URL
|
// Simulate a quick reachability test by fetching the version URL
|
||||||
const res: Record<string, any> = await deployApi.getToolVersions(tool.id);
|
const res: JsonData = await deployApi.getToolVersions(tool.id);
|
||||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||||
const available = (res as Record<string, any>)?.versions?.length > 0;
|
const available = (res as JsonData)?.versions?.length > 0;
|
||||||
if (available) {
|
if (available) {
|
||||||
toast.success(`${tool.display_name} 镜像可访问 (${elapsed}s)`);
|
toast.success(`${tool.display_name} 镜像可访问 (${elapsed}s)`);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||||
import { deployApi } from '@/api';
|
import { deployApi } from '@/api';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
interface Group {
|
interface Group {
|
||||||
id: number; name: string; description: string;
|
id: number; name: string; description: string;
|
||||||
@@ -35,7 +36,7 @@ export default function UserGroupsManager() {
|
|||||||
|
|
||||||
const loadGroups = async () => {
|
const loadGroups = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try { const res = await deployApi.getGroups(); setGroups((res as Record<string, any>)?.list || []); }
|
try { const res = await deployApi.getGroups(); setGroups((res as JsonData)?.list || []); }
|
||||||
catch { toast.error('加载失败'); }
|
catch { toast.error('加载失败'); }
|
||||||
finally { setLoading(false); }
|
finally { setLoading(false); }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Maximize2, Minimize2, X, FolderOpen } from 'lucide-react';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useUserStore } from '@/store';
|
import { useUserStore } from '@/store';
|
||||||
import { pipelineApi } from '@/api';
|
import { pipelineApi } from '@/api';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
interface WorkspaceTermDialogProps {
|
interface WorkspaceTermDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -41,7 +42,7 @@ export function WorkspaceTermDialog({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !runId) return;
|
if (!open || !runId) return;
|
||||||
pipelineApi.getWorkspace(runId)
|
pipelineApi.getWorkspace(runId)
|
||||||
.then(( data: any) => setWorkspace(data?.workspace_path || ''))
|
.then((data: JsonData) => setWorkspace(data?.workspace_path || ''))
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, [open, runId]);
|
}, [open, runId]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { CheckCircle, XCircle, Loader2 } from 'lucide-react';
|
import { CheckCircle, XCircle, Loader2 } from 'lucide-react';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
// 流水线阶段定义 // Logan.Li
|
// 流水线阶段定义 // Logan.Li
|
||||||
export interface PipelineStage {
|
export interface PipelineStage {
|
||||||
@@ -19,7 +20,7 @@ export const pipelineTemplates = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
// 解析 stages 字段(兼容 JSON 数组 / 逗号分隔字符串) // Logan.Li
|
// 解析 stages 字段(兼容 JSON 数组 / 逗号分隔字符串) // Logan.Li
|
||||||
export function parseStages( stages: any): PipelineStage[] {
|
export function parseStages( stages: string): PipelineStage[] {
|
||||||
if (Array.isArray(stages)) return stages;
|
if (Array.isArray(stages)) return stages;
|
||||||
if (typeof stages === 'string') {
|
if (typeof stages === 'string') {
|
||||||
try {
|
try {
|
||||||
@@ -42,7 +43,7 @@ export function nameToCode(name: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 状态徽章(使用后端 last_status 字段) // Logan.Li
|
// 状态徽章(使用后端 last_status 字段) // Logan.Li
|
||||||
export function getStatusBadge( status: any) {
|
export function getStatusBadge( status: string) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'success':
|
case 'success':
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export function PipelineCard({
|
|||||||
#{String(pipeline.run_count)}
|
#{String(pipeline.run_count)}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{getStatusBadge(pipeline.last_status)}
|
{getStatusBadge(String(pipeline.last_status))}
|
||||||
{environment && (
|
{environment && (
|
||||||
<Badge
|
<Badge
|
||||||
style={{ backgroundColor: `${environment.color}20`, color: environment.color as string }}
|
style={{ backgroundColor: `${environment.color}20`, color: environment.color as string }}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// 发布相关共享工具函数与常量 // Logan.Li
|
// 发布相关共享工具函数与常量 // Logan.Li
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
import {
|
import {
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
XCircle,
|
XCircle,
|
||||||
@@ -9,7 +10,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
// 发布状态徽章(列表 + 详情共用) // Logan.Li
|
// 发布状态徽章(列表 + 详情共用) // Logan.Li
|
||||||
export function getStatusBadge( status: any) {
|
export function getStatusBadge( status: string) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'success':
|
case 'success':
|
||||||
return (
|
return (
|
||||||
@@ -59,7 +60,7 @@ export function getStatusBadge( status: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 环境徽章 // Logan.Li
|
// 环境徽章 // Logan.Li
|
||||||
export function getEnvBadge( env: any) {
|
export function getEnvBadge( env: string) {
|
||||||
const colors: Record<string, string> = {
|
const colors: Record<string, string> = {
|
||||||
dev: 'border-slate-500/30 text-slate-400 bg-slate-500/10',
|
dev: 'border-slate-500/30 text-slate-400 bg-slate-500/10',
|
||||||
test: 'border-blue-500/30 text-blue-400 bg-blue-500/10',
|
test: 'border-blue-500/30 text-blue-400 bg-blue-500/10',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// 发布管理(审批流)共享工具函数与常量 // Logan.Li
|
// 发布管理(审批流)共享工具函数与常量 // Logan.Li
|
||||||
// 注意:本文件专用于 ReleaseManager(含审批流,状态标签与 AppRelease 的 releaseHelpers 不同)
|
// 注意:本文件专用于 ReleaseManager(含审批流,状态标签与 AppRelease 的 releaseHelpers 不同)
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
import {
|
import {
|
||||||
Clock,
|
Clock,
|
||||||
Check,
|
Check,
|
||||||
@@ -19,7 +20,7 @@ export const ENVIRONMENTS: Record<string, { label: string; color: string }> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 发布状态徽章(审批流:待审批/已审批/已驳回/发布中/发布成功/发布失败/已回滚) // Logan.Li
|
// 发布状态徽章(审批流:待审批/已审批/已驳回/发布中/发布成功/发布失败/已回滚) // Logan.Li
|
||||||
export function getStatusBadge( status: any) {
|
export function getStatusBadge( status: string) {
|
||||||
const map: Record<string, { icon: any; label: string; cls: string }> = {
|
const map: Record<string, { icon: any; label: string; cls: string }> = {
|
||||||
pending: { icon: Clock, label: '待审批', cls: 'border-amber-500/30 text-amber-400 bg-amber-500/10' },
|
pending: { icon: Clock, label: '待审批', cls: 'border-amber-500/30 text-amber-400 bg-amber-500/10' },
|
||||||
approved: { icon: Check, label: '已审批', cls: 'border-purple-500/30 text-purple-400 bg-purple-500/10' },
|
approved: { icon: Check, label: '已审批', cls: 'border-purple-500/30 text-purple-400 bg-purple-500/10' },
|
||||||
@@ -41,7 +42,7 @@ export function getStatusBadge( status: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 环境徽章 // Logan.Li
|
// 环境徽章 // Logan.Li
|
||||||
export function getEnvironmentBadge( env: any) {
|
export function getEnvironmentBadge( env: string) {
|
||||||
const cfg = ENVIRONMENTS[env];
|
const cfg = ENVIRONMENTS[env];
|
||||||
return (
|
return (
|
||||||
<Badge variant="outline" className={cfg?.color || 'border-slate-500/30 text-slate-400'}>
|
<Badge variant="outline" className={cfg?.color || 'border-slate-500/30 text-slate-400'}>
|
||||||
|
|||||||
@@ -11,19 +11,20 @@ import {
|
|||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { RefreshCw, ChevronRight, BarChart3, Download, Cpu, HardDrive } from 'lucide-react';
|
import { RefreshCw, ChevronRight, BarChart3, Download, Cpu, HardDrive } from 'lucide-react';
|
||||||
import { kubernetesApi } from '@/api';
|
import { kubernetesApi } from '@/api';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export default function CapacityPage() {
|
export default function CapacityPage() {
|
||||||
const { clusterCode } = useParams<{ clusterCode: string }>();
|
const { clusterCode } = useParams<{ clusterCode: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [capacity, setCapacity] = useState<any[]>([]);
|
const [capacity, setCapacity] = useState<JsonData[]>([]);
|
||||||
const [clusters, setClusters] = useState<any[]>([]);
|
const [clusters, setClusters] = useState<JsonData[]>([]);
|
||||||
const [namespaces, setNamespaces] = useState<string[]>([]);
|
const [namespaces, setNamespaces] = useState<string[]>([]);
|
||||||
const [selectedNs, setSelectedNs] = useState('all');
|
const [selectedNs, setSelectedNs] = useState('all');
|
||||||
const [activeCluster, setActiveCluster] = useState(clusterCode || '');
|
const [activeCluster, setActiveCluster] = useState(clusterCode || '');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
kubernetesApi.getClusters().then(( res: any) => {
|
kubernetesApi.getClusters().then((res: JsonData) => {
|
||||||
const list = res?.list || [];
|
const list = res?.list || [];
|
||||||
setClusters(list);
|
setClusters(list);
|
||||||
if (!activeCluster && list.length > 0) {
|
if (!activeCluster && list.length > 0) {
|
||||||
@@ -36,7 +37,7 @@ export default function CapacityPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeCluster) return;
|
if (!activeCluster) return;
|
||||||
kubernetesApi.getNamespaces(activeCluster).then(( res: any) => {
|
kubernetesApi.getNamespaces(activeCluster).then((res: JsonData) => {
|
||||||
setNamespaces(Array.isArray(res) ? res : (res?.list || res?.data || []));
|
setNamespaces(Array.isArray(res) ? res : (res?.list || res?.data || []));
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}, [activeCluster]);
|
}, [activeCluster]);
|
||||||
@@ -46,7 +47,7 @@ export default function CapacityPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const ns = selectedNs === 'all' ? undefined : selectedNs;
|
const ns = selectedNs === 'all' ? undefined : selectedNs;
|
||||||
const res: Record<string, any> = await kubernetesApi.getCapacity(activeCluster, ns);
|
const res: JsonData = await kubernetesApi.getCapacity(activeCluster, ns);
|
||||||
setCapacity(Array.isArray(res) ? res : (res?.list || []));
|
setCapacity(Array.isArray(res) ? res : (res?.list || []));
|
||||||
} catch { setCapacity([]); } finally { setLoading(false); }
|
} catch { setCapacity([]); } finally { setLoading(false); }
|
||||||
}, [activeCluster, selectedNs]);
|
}, [activeCluster, selectedNs]);
|
||||||
@@ -54,8 +55,8 @@ export default function CapacityPage() {
|
|||||||
useEffect(() => { loadCapacity(); }, [loadCapacity]);
|
useEffect(() => { loadCapacity(); }, [loadCapacity]);
|
||||||
|
|
||||||
const rows = useMemo(() =>
|
const rows = useMemo(() =>
|
||||||
capacity.flatMap(( d: any) =>
|
capacity.flatMap((d: JsonData) =>
|
||||||
(d.containers || []).map(( c: any) => ({
|
(d.containers || []).map((c: JsonData) => ({
|
||||||
namespace: d.namespace,
|
namespace: d.namespace,
|
||||||
deployment: d.name,
|
deployment: d.name,
|
||||||
replicas: d.replicas,
|
replicas: d.replicas,
|
||||||
@@ -83,7 +84,7 @@ export default function CapacityPage() {
|
|||||||
<Select value={activeCluster} onValueChange={v => { setActiveCluster(v); navigate(`/kubernetes/${v}/capacity`); }}>
|
<Select value={activeCluster} onValueChange={v => { setActiveCluster(v); navigate(`/kubernetes/${v}/capacity`); }}>
|
||||||
<SelectTrigger className="w-40 h-8 text-xs bg-slate-800 border-slate-700"><SelectValue /></SelectTrigger>
|
<SelectTrigger className="w-40 h-8 text-xs bg-slate-800 border-slate-700"><SelectValue /></SelectTrigger>
|
||||||
<SelectContent className="bg-slate-800 border-slate-700">
|
<SelectContent className="bg-slate-800 border-slate-700">
|
||||||
{clusters.map(( c: any) => <SelectItem key={c.id} value={c.code || String(c.id)} className="text-xs">{c.name || c.code}</SelectItem>)}
|
{clusters.map((c: JsonData) => <SelectItem key={c.id} value={c.code || String(c.id)} className="text-xs">{c.name || c.code}</SelectItem>)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Select value={selectedNs} onValueChange={setSelectedNs}>
|
<Select value={selectedNs} onValueChange={setSelectedNs}>
|
||||||
|
|||||||
@@ -25,20 +25,21 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { kubernetesApi } from '@/api';
|
import { kubernetesApi } from '@/api';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
const CHART_COLORS = ['#3B82F6', '#207060', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#14B8A6', '#F97316'];
|
const CHART_COLORS = ['#3B82F6', '#207060', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#14B8A6', '#F97316'];
|
||||||
|
|
||||||
export default function ClusterComparePage() {
|
export default function ClusterComparePage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [data, setData] = useState<any>(null);
|
const [data, setData] = useState<JsonData>(null);
|
||||||
const [error, setError] = useState<string>('');
|
const [error, setError] = useState<string>('');
|
||||||
|
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const res: Record<string, any> = await kubernetesApi.getMultiClusterComparison();
|
const res: JsonData = await kubernetesApi.getMultiClusterComparison();
|
||||||
setData(res || null);
|
setData(res || null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e?.message || '加载失败');
|
setError(e?.message || '加载失败');
|
||||||
@@ -50,11 +51,11 @@ export default function ClusterComparePage() {
|
|||||||
|
|
||||||
useEffect(() => { loadData(); }, [loadData]);
|
useEffect(() => { loadData(); }, [loadData]);
|
||||||
|
|
||||||
const clusters: Record<string, any>[] = data?.clusters || [];
|
const clusters: JsonData[] = data?.clusters || [];
|
||||||
const totals: Record<string, any> = data?.totals || {};
|
const totals: JsonData = data?.totals || {};
|
||||||
|
|
||||||
// 图表数据准备
|
// 图表数据准备
|
||||||
const resourceBarData = clusters.map(( c: any) => ({
|
const resourceBarData = clusters.map((c: JsonData) => ({
|
||||||
name: c.code || c.name,
|
name: c.code || c.name,
|
||||||
节点: c.node_count || 0,
|
节点: c.node_count || 0,
|
||||||
Pod: c.pod_count || 0,
|
Pod: c.pod_count || 0,
|
||||||
@@ -62,22 +63,22 @@ export default function ClusterComparePage() {
|
|||||||
Service: c.service_count || 0,
|
Service: c.service_count || 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const usageBarData = clusters.map(( c: any) => ({
|
const usageBarData = clusters.map((c: JsonData) => ({
|
||||||
name: c.code || c.name,
|
name: c.code || c.name,
|
||||||
'CPU %': Number((c.cpu_usage || 0).toFixed(1)),
|
'CPU %': Number((c.cpu_usage || 0).toFixed(1)),
|
||||||
'内存 %': Number((c.memory_usage || 0).toFixed(1)),
|
'内存 %': Number((c.memory_usage || 0).toFixed(1)),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const radarData = clusters.length > 0 ? [
|
const radarData = clusters.length > 0 ? [
|
||||||
{ metric: '节点数', ...Object.fromEntries(clusters.map(( c: any) => [c.code || c.name, c.node_count || 0])) },
|
{ metric: '节点数', ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, c.node_count || 0])) },
|
||||||
{ metric: 'Pod数', ...Object.fromEntries(clusters.map(( c: any) => [c.code || c.name, Math.min(c.pod_count || 0, 100)])) },
|
{ metric: 'Pod数', ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, Math.min(c.pod_count || 0, 100)])) },
|
||||||
{ metric: '部署数', ...Object.fromEntries(clusters.map(( c: any) => [c.code || c.name, c.deployment_count || 0])) },
|
{ metric: '部署数', ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, c.deployment_count || 0])) },
|
||||||
{ metric: 'CPU使用', ...Object.fromEntries(clusters.map(( c: any) => [c.code || c.name, Number((c.cpu_usage || 0).toFixed(1))])) },
|
{ metric: 'CPU使用', ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, Number((c.cpu_usage || 0).toFixed(1))])) },
|
||||||
{ metric: '内存使用', ...Object.fromEntries(clusters.map(( c: any) => [c.code || c.name, Number((c.memory_usage || 0).toFixed(1))])) },
|
{ metric: '内存使用', ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, Number((c.memory_usage || 0).toFixed(1))])) },
|
||||||
{ metric: 'CPU核数', ...Object.fromEntries(clusters.map(( c: any) => [c.code || c.name, c.total_cpu_cores || 0])) },
|
{ metric: 'CPU核数', ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, c.total_cpu_cores || 0])) },
|
||||||
] : [];
|
] : [];
|
||||||
|
|
||||||
const nodeRolePie = clusters.flatMap(( c: any) => [
|
const nodeRolePie = clusters.flatMap((c: JsonData) => [
|
||||||
{ name: `${c.code}-Master`, value: c.master_count || 0, fill: CHART_COLORS[1] },
|
{ name: `${c.code}-Master`, value: c.master_count || 0, fill: CHART_COLORS[1] },
|
||||||
{ name: `${c.code}-Worker`, value: c.worker_count || 0, fill: CHART_COLORS[0] },
|
{ name: `${c.code}-Worker`, value: c.worker_count || 0, fill: CHART_COLORS[0] },
|
||||||
]).filter(d => d.value > 0);
|
]).filter(d => d.value > 0);
|
||||||
@@ -221,7 +222,7 @@ export default function ClusterComparePage() {
|
|||||||
<PolarGrid stroke="#1e293b" />
|
<PolarGrid stroke="#1e293b" />
|
||||||
<PolarAngleAxis dataKey="metric" tick={{ fill: '#94a3b8', fontSize: 11 }} />
|
<PolarAngleAxis dataKey="metric" tick={{ fill: '#94a3b8', fontSize: 11 }} />
|
||||||
<PolarRadiusAxis tick={{ fill: '#64748b', fontSize: 10 }} />
|
<PolarRadiusAxis tick={{ fill: '#64748b', fontSize: 10 }} />
|
||||||
{clusters.map(( c: any, i: number) => (
|
{clusters.map((c: JsonData, i: number) => (
|
||||||
<Radar key={i} name={c.code || c.name}
|
<Radar key={i} name={c.code || c.name}
|
||||||
dataKey={c.code || c.name}
|
dataKey={c.code || c.name}
|
||||||
stroke={CHART_COLORS[i % CHART_COLORS.length]}
|
stroke={CHART_COLORS[i % CHART_COLORS.length]}
|
||||||
@@ -248,7 +249,7 @@ export default function ClusterComparePage() {
|
|||||||
<PieChart>
|
<PieChart>
|
||||||
<Pie data={nodeRolePie} cx="50%" cy="45%" innerRadius={50} outerRadius={90}
|
<Pie data={nodeRolePie} cx="50%" cy="45%" innerRadius={50} outerRadius={90}
|
||||||
paddingAngle={3} dataKey="value"
|
paddingAngle={3} dataKey="value"
|
||||||
label={( e: any) => `${e.name}: ${e.value}`}
|
label={(e: JsonData) => `${e.name}: ${e.value}`}
|
||||||
labelLine={{ stroke: '#475569' }}
|
labelLine={{ stroke: '#475569' }}
|
||||||
>
|
>
|
||||||
{nodeRolePie.map((_, i) => (
|
{nodeRolePie.map((_, i) => (
|
||||||
@@ -293,7 +294,7 @@ export default function ClusterComparePage() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{clusters.map(( c: any) => (
|
{clusters.map((c: JsonData) => (
|
||||||
<TableRow key={c.id}
|
<TableRow key={c.id}
|
||||||
className="border-slate-800/50 hover:bg-slate-800/30 cursor-pointer"
|
className="border-slate-800/50 hover:bg-slate-800/30 cursor-pointer"
|
||||||
onClick={() => navigate(`/kubernetes/${c.code}`)}
|
onClick={() => navigate(`/kubernetes/${c.code}`)}
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ import {
|
|||||||
|
|
||||||
import { kubernetesApi } from '@/api';
|
import { kubernetesApi } from '@/api';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
const NAMESPACE_ICON: Record<string, React.ComponentType<any>> = {
|
const NAMESPACE_ICON: Record<string, React.ComponentType<Record<string, unknown>>> = {
|
||||||
'kube-system': Server,
|
'kube-system': Server,
|
||||||
'kube-public': Shield,
|
'kube-public': Shield,
|
||||||
'kube-node-lease': Activity,
|
'kube-node-lease': Activity,
|
||||||
@@ -35,19 +36,19 @@ export default function ClusterOverviewPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const [, setClusters] = useState<any[]>([]);
|
const [, setClusters] = useState<JsonData[]>([]);
|
||||||
const [nodes, setNodes] = useState<any[]>([]);
|
const [nodes, setNodes] = useState<JsonData[]>([]);
|
||||||
const [namespaces, setNamespaces] = useState<string[]>([]);
|
const [namespaces, setNamespaces] = useState<string[]>([]);
|
||||||
const [clusterInfo, setClusterInfo] = useState<any>(null);
|
const [clusterInfo, setClusterInfo] = useState<JsonData>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
// Load cluster list + current cluster info
|
// Load cluster list + current cluster info
|
||||||
const loadClusterInfo = useCallback(async () => {
|
const loadClusterInfo = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res: Record<string, any> = await kubernetesApi.getClusters();
|
const res: JsonData = await kubernetesApi.getClusters();
|
||||||
const list = res?.list || [];
|
const list = res?.list || [];
|
||||||
setClusters(list);
|
setClusters(list);
|
||||||
const current = list.find(( c: any) => (c.code || String(c.id)) === clusterCode);
|
const current = list.find((c: JsonData) => (c.code || String(c.id)) === clusterCode);
|
||||||
setClusterInfo(current || null);
|
setClusterInfo(current || null);
|
||||||
if (!current && list.length > 0 && clusterCode) {
|
if (!current && list.length > 0 && clusterCode) {
|
||||||
toast({ title: '集群未找到', description: `找不到集群 ${clusterCode}`, variant: 'destructive' });
|
toast({ title: '集群未找到', description: `找不到集群 ${clusterCode}`, variant: 'destructive' });
|
||||||
@@ -72,7 +73,7 @@ export default function ClusterOverviewPage() {
|
|||||||
useEffect(() => { loadClusterInfo(); }, [loadClusterInfo]);
|
useEffect(() => { loadClusterInfo(); }, [loadClusterInfo]);
|
||||||
useEffect(() => { loadData(); }, [loadData]);
|
useEffect(() => { loadData(); }, [loadData]);
|
||||||
|
|
||||||
const getNodeRole = ( node: any) => {
|
const getNodeRole = (node: JsonData) => {
|
||||||
if (node?.role === 'master' || node?.role === 'control-plane') return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
|
if (node?.role === 'master' || node?.role === 'control-plane') return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
|
||||||
return 'bg-slate-500/20 text-slate-400 border-slate-500/30';
|
return 'bg-slate-500/20 text-slate-400 border-slate-500/30';
|
||||||
};
|
};
|
||||||
@@ -157,7 +158,7 @@ export default function ClusterOverviewPage() {
|
|||||||
) : nodes.length === 0 ? (
|
) : nodes.length === 0 ? (
|
||||||
<TableRow><TableCell colSpan={8} className="text-center text-slate-500 py-12"><Cpu className="h-8 w-8 mx-auto mb-2 opacity-50" />暂无节点</TableCell></TableRow>
|
<TableRow><TableCell colSpan={8} className="text-center text-slate-500 py-12"><Cpu className="h-8 w-8 mx-auto mb-2 opacity-50" />暂无节点</TableCell></TableRow>
|
||||||
) : (
|
) : (
|
||||||
nodes.map(( n: any) => {
|
nodes.map((n: JsonData) => {
|
||||||
const cpuCores = n.cpu || 0;
|
const cpuCores = n.cpu || 0;
|
||||||
const cpuUsageM = n.cpu_usage_m ? parseFloat(n.cpu_usage_m.replace('m','')) : 0;
|
const cpuUsageM = n.cpu_usage_m ? parseFloat(n.cpu_usage_m.replace('m','')) : 0;
|
||||||
const cpuPct = cpuCores > 0 ? (cpuUsageM / 1000 / cpuCores) * 100 : 0;
|
const cpuPct = cpuCores > 0 ? (cpuUsageM / 1000 / cpuCores) * 100 : 0;
|
||||||
|
|||||||
@@ -15,13 +15,14 @@ import { DeploymentLogsTab } from './deployment-detail/DeploymentLogsTab';
|
|||||||
import { DeploymentHpaTab } from './deployment-detail/DeploymentHpaTab';
|
import { DeploymentHpaTab } from './deployment-detail/DeploymentHpaTab';
|
||||||
import { DeploymentPodModals } from './deployment-detail/DeploymentPodModals';
|
import { DeploymentPodModals } from './deployment-detail/DeploymentPodModals';
|
||||||
import { YamlViewer } from './YamlViewer';
|
import { YamlViewer } from './YamlViewer';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBack }: DeploymentDetailProps) {
|
export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBack }: DeploymentDetailProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const [deployment, setDeployment] = useState<any>(null);
|
const [deployment, setDeployment] = useState<JsonData>(null);
|
||||||
const [pods, setPods] = useState<any[]>([]);
|
const [pods, setPods] = useState<JsonData[]>([]);
|
||||||
const [hpas, setHpas] = useState<any[]>([]);
|
const [hpas, setHpas] = useState<JsonData[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [actionLoading, setActionLoading] = useState('');
|
const [actionLoading, setActionLoading] = useState('');
|
||||||
const [, setScaleValue] = useState(0);
|
const [, setScaleValue] = useState(0);
|
||||||
@@ -45,19 +46,19 @@ export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBac
|
|||||||
kubernetesApi.getNamespacePods(clusterCode, namespace).catch(() => []),
|
kubernetesApi.getNamespacePods(clusterCode, namespace).catch(() => []),
|
||||||
kubernetesApi.getHPAs(clusterCode, namespace).catch(() => ({ items: [] })),
|
kubernetesApi.getHPAs(clusterCode, namespace).catch(() => ({ items: [] })),
|
||||||
]);
|
]);
|
||||||
const toArray = (v: any) => (Array.isArray(v) ? v : (v?.list || v?.items || []));
|
const toArray = (v: JsonData) => (Array.isArray(v) ? v : (v?.list || v?.items || []));
|
||||||
const allDeps = toArray(depRes);
|
const allDeps = toArray(depRes);
|
||||||
const allPods = toArray(podRes);
|
const allPods = toArray(podRes);
|
||||||
const allHpas = toArray(hpaRes);
|
const allHpas = toArray(hpaRes);
|
||||||
|
|
||||||
const dep = allDeps.find((d: any) => d.name === deploymentName && d.namespace === namespace);
|
const dep = allDeps.find((d: JsonData) => d.name === deploymentName && d.namespace === namespace);
|
||||||
setDeployment(dep || null);
|
setDeployment(dep || null);
|
||||||
if (dep) {
|
if (dep) {
|
||||||
setScaleValue(dep.replicas || 0);
|
setScaleValue(dep.replicas || 0);
|
||||||
setNewImage(dep.image || '');
|
setNewImage(dep.image || '');
|
||||||
}
|
}
|
||||||
setPods(allPods.filter((p: any) => p.namespace === namespace && p.name.startsWith(deploymentName)));
|
setPods(allPods.filter((p: JsonData) => p.namespace === namespace && p.name.startsWith(deploymentName)));
|
||||||
setHpas(allHpas.filter((h: any) => h.target_name === deploymentName && h.namespace === namespace));
|
setHpas(allHpas.filter((h: JsonData) => h.target_name === deploymentName && h.namespace === namespace));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载详情失败:', e);
|
console.error('加载详情失败:', e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -103,7 +104,7 @@ export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBac
|
|||||||
} finally { setActionLoading(''); }
|
} finally { setActionLoading(''); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleHpaToggle = async (hpa: any) => {
|
const handleHpaToggle = async (hpa: JsonData) => {
|
||||||
const ok = await confirm({
|
const ok = await confirm({
|
||||||
title: '删除 HPA',
|
title: '删除 HPA',
|
||||||
description: `确定删除 HPA "${hpa.name}"?`,
|
description: `确定删除 HPA "${hpa.name}"?`,
|
||||||
|
|||||||
@@ -14,13 +14,14 @@ import { kubernetesApi } from '@/api';
|
|||||||
import { saveDeploymentManifest } from './deployment-manifest-save';
|
import { saveDeploymentManifest } from './deployment-manifest-save';
|
||||||
import { SchedulingSection, type ScheduleMode } from './deployment-edit-advanced/SchedulingSection';
|
import { SchedulingSection, type ScheduleMode } from './deployment-edit-advanced/SchedulingSection';
|
||||||
import { DnsHostSection } from './deployment-edit-advanced/DnsHostSection';
|
import { DnsHostSection } from './deployment-edit-advanced/DnsHostSection';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
clusterCode: string;
|
clusterCode: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
deploymentName: string;
|
deploymentName: string;
|
||||||
manifest: any | null;
|
manifest: JsonData | null;
|
||||||
onSaved: (updated?: any) => void;
|
onSaved: (updated?: JsonData) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [loading, setLoading] = useState(!manifest);
|
const [loading, setLoading] = useState(!manifest);
|
||||||
const [rawManifest, setRawManifest] = useState<any>(manifest || {});
|
const [rawManifest, setRawManifest] = useState<JsonData>(manifest || {});
|
||||||
|
|
||||||
// 更新策略
|
// 更新策略
|
||||||
const [strategyType, setStrategyType] = useState<'RollingUpdate' | 'Recreate'>('RollingUpdate');
|
const [strategyType, setStrategyType] = useState<'RollingUpdate' | 'Recreate'>('RollingUpdate');
|
||||||
@@ -119,13 +120,13 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
|||||||
// 加载节点列表
|
// 加载节点列表
|
||||||
if (clusterCode) {
|
if (clusterCode) {
|
||||||
setNodesLoading(true);
|
setNodesLoading(true);
|
||||||
kubernetesApi.getClusterNodes(clusterCode).then((res: any) => {
|
kubernetesApi.getClusterNodes(clusterCode).then((res: JsonData) => {
|
||||||
const nodes: Record<string, any>[] = Array.isArray(res) ? res : (res?.items || res?.list || []);
|
const nodes: JsonData[] = Array.isArray(res) ? res : (res?.items || res?.list || []);
|
||||||
const mapped = nodes.map((n: any) => {
|
const mapped = nodes.map((n: JsonData) => {
|
||||||
const meta = n.metadata || {};
|
const meta = n.metadata || {};
|
||||||
const status = n.status || {};
|
const status = n.status || {};
|
||||||
const addresses: Record<string, any>[] = Array.isArray(status.addresses) ? status.addresses : [];
|
const addresses: JsonData[] = Array.isArray(status.addresses) ? status.addresses : [];
|
||||||
const internalIP = addresses.find((a: any) => a.type === 'InternalIP')?.address || '';
|
const internalIP = addresses.find((a: JsonData) => a.type === 'InternalIP')?.address || '';
|
||||||
return { name: meta.name || '', ip: internalIP, labels: meta.labels || {} };
|
return { name: meta.name || '', ip: internalIP, labels: meta.labels || {} };
|
||||||
});
|
});
|
||||||
setAvailableNodes(mapped);
|
setAvailableNodes(mapped);
|
||||||
@@ -143,7 +144,7 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// tolerations
|
// tolerations
|
||||||
const tols: Record<string, any>[] = Array.isArray(podSpec.tolerations) ? podSpec.tolerations : [];
|
const tols: JsonData[] = Array.isArray(podSpec.tolerations) ? podSpec.tolerations : [];
|
||||||
setTolerations(tols.map(t => ({
|
setTolerations(tols.map(t => ({
|
||||||
key: t.key || '',
|
key: t.key || '',
|
||||||
operator: t.operator || 'Equal',
|
operator: t.operator || 'Equal',
|
||||||
@@ -172,7 +173,7 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
|||||||
setShareProcessNamespace(!!podSpec.shareProcessNamespace);
|
setShareProcessNamespace(!!podSpec.shareProcessNamespace);
|
||||||
|
|
||||||
// hostAliases(M8 补全)
|
// hostAliases(M8 补全)
|
||||||
const ha: Record<string, any>[] = Array.isArray(podSpec.hostAliases) ? podSpec.hostAliases : [];
|
const ha: JsonData[] = Array.isArray(podSpec.hostAliases) ? podSpec.hostAliases : [];
|
||||||
setHostAliases(ha.map(a => ({
|
setHostAliases(ha.map(a => ({
|
||||||
ip: a.ip || '',
|
ip: a.ip || '',
|
||||||
hostnames: Array.isArray(a.hostnames) ? a.hostnames.join(', ') : '',
|
hostnames: Array.isArray(a.hostnames) ? a.hostnames.join(', ') : '',
|
||||||
@@ -182,7 +183,7 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
|||||||
const dc = podSpec.dnsConfig || {};
|
const dc = podSpec.dnsConfig || {};
|
||||||
setDnsNameservers(Array.isArray(dc.nameservers) ? dc.nameservers.join('\n') : '');
|
setDnsNameservers(Array.isArray(dc.nameservers) ? dc.nameservers.join('\n') : '');
|
||||||
setDnsSearches(Array.isArray(dc.searches) ? dc.searches.join('\n') : '');
|
setDnsSearches(Array.isArray(dc.searches) ? dc.searches.join('\n') : '');
|
||||||
const opts: Record<string, any>[] = Array.isArray(dc.options) ? dc.options : [];
|
const opts: JsonData[] = Array.isArray(dc.options) ? dc.options : [];
|
||||||
setDnsOptions(opts.map(o => ({ name: o.name || '', value: o.value || '' })));
|
setDnsOptions(opts.map(o => ({ name: o.name || '', value: o.value || '' })));
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -229,7 +230,7 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
|||||||
if (podSpec.nodeName) delete podSpec.nodeName;
|
if (podSpec.nodeName) delete podSpec.nodeName;
|
||||||
if (podSpec.affinity) delete podSpec.affinity.nodeAffinity;
|
if (podSpec.affinity) delete podSpec.affinity.nodeAffinity;
|
||||||
} else if (scheduleMode === 'nodeAffinity') {
|
} else if (scheduleMode === 'nodeAffinity') {
|
||||||
const matchExpressions: Record<string, any>[] = labelSelectorKv
|
const matchExpressions: JsonData[] = labelSelectorKv
|
||||||
.filter(s => s.key.trim())
|
.filter(s => s.key.trim())
|
||||||
.map(s => ({ key: s.key.trim(), operator: 'In', values: [s.value] }));
|
.map(s => ({ key: s.key.trim(), operator: 'In', values: [s.value] }));
|
||||||
if (matchExpressions.length > 0) {
|
if (matchExpressions.length > 0) {
|
||||||
@@ -250,7 +251,7 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
|||||||
const tolsK8s = tolerations
|
const tolsK8s = tolerations
|
||||||
.filter(t => t.key || t.operator === 'Exists')
|
.filter(t => t.key || t.operator === 'Exists')
|
||||||
.map(t => {
|
.map(t => {
|
||||||
const out: Record<string, any> = {};
|
const out: JsonData = {};
|
||||||
if (t.key) out.key = t.key;
|
if (t.key) out.key = t.key;
|
||||||
if (t.operator) out.operator = t.operator;
|
if (t.operator) out.operator = t.operator;
|
||||||
if (t.value && t.operator !== 'Exists') out.value = t.value;
|
if (t.value && t.operator !== 'Exists') out.value = t.value;
|
||||||
@@ -269,7 +270,7 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
|||||||
if (serviceAccountName) podSpec.serviceAccountName = serviceAccountName;
|
if (serviceAccountName) podSpec.serviceAccountName = serviceAccountName;
|
||||||
|
|
||||||
// Pod 安全上下文
|
// Pod 安全上下文
|
||||||
const sc: Record<string, any> = {};
|
const sc: JsonData = {};
|
||||||
if (podFsGroup) sc.fsGroup = Number(podFsGroup);
|
if (podFsGroup) sc.fsGroup = Number(podFsGroup);
|
||||||
if (podRunAsUser) sc.runAsUser = Number(podRunAsUser);
|
if (podRunAsUser) sc.runAsUser = Number(podRunAsUser);
|
||||||
if (podRunAsGroup) sc.runAsGroup = Number(podRunAsGroup);
|
if (podRunAsGroup) sc.runAsGroup = Number(podRunAsGroup);
|
||||||
@@ -295,13 +296,13 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
|||||||
else delete podSpec.hostAliases;
|
else delete podSpec.hostAliases;
|
||||||
|
|
||||||
// dnsConfig(M8 补全)— 仅 dnsPolicy=None 时 dnsConfig 才有意义,但仍允许编辑
|
// dnsConfig(M8 补全)— 仅 dnsPolicy=None 时 dnsConfig 才有意义,但仍允许编辑
|
||||||
const dcObj: Record<string, any> = {};
|
const dcObj: JsonData = {};
|
||||||
const ns = dnsNameservers.split('\n').map(s => s.trim()).filter(Boolean);
|
const ns = dnsNameservers.split('\n').map(s => s.trim()).filter(Boolean);
|
||||||
if (ns.length > 0) dcObj.nameservers = ns;
|
if (ns.length > 0) dcObj.nameservers = ns;
|
||||||
const sr = dnsSearches.split('\n').map(s => s.trim()).filter(Boolean);
|
const sr = dnsSearches.split('\n').map(s => s.trim()).filter(Boolean);
|
||||||
if (sr.length > 0) dcObj.searches = sr;
|
if (sr.length > 0) dcObj.searches = sr;
|
||||||
const dkOpts = dnsOptions.filter(o => o.name.trim()).map(o => {
|
const dkOpts = dnsOptions.filter(o => o.name.trim()).map(o => {
|
||||||
const out: Record<string, any> = { name: o.name.trim() };
|
const out: JsonData = { name: o.name.trim() };
|
||||||
if (o.value.trim()) out.value = o.value.trim();
|
if (o.value.trim()) out.value = o.value.trim();
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,12 +25,13 @@ import {
|
|||||||
toContainerForm, emptyContainer, containerToK8s,
|
toContainerForm, emptyContainer, containerToK8s,
|
||||||
} from './deployment-edit/container-utils';
|
} from './deployment-edit/container-utils';
|
||||||
import type { ContainerForm, DeploymentEditContainersProps } from './deployment-edit/types';
|
import type { ContainerForm, DeploymentEditContainersProps } from './deployment-edit/types';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export function DeploymentEditContainers({ clusterCode, namespace, deploymentName, manifest, onSaved, onCancel }: DeploymentEditContainersProps) {
|
export function DeploymentEditContainers({ clusterCode, namespace, deploymentName, manifest, onSaved, onCancel }: DeploymentEditContainersProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [loading, setLoading] = useState(!manifest);
|
const [loading, setLoading] = useState(!manifest);
|
||||||
const [rawManifest, setRawManifest] = useState<any>(manifest || {});
|
const [rawManifest, setRawManifest] = useState<JsonData>(manifest || {});
|
||||||
const [containers, setContainers] = useState<ContainerForm[]>([]);
|
const [containers, setContainers] = useState<ContainerForm[]>([]);
|
||||||
const [initContainers, setInitContainers] = useState<ContainerForm[]>([]);
|
const [initContainers, setInitContainers] = useState<ContainerForm[]>([]);
|
||||||
const [imagePullSecrets, setImagePullSecrets] = useState<string[]>([]);
|
const [imagePullSecrets, setImagePullSecrets] = useState<string[]>([]);
|
||||||
@@ -41,7 +42,7 @@ export function DeploymentEditContainers({ clusterCode, namespace, deploymentNam
|
|||||||
const podSpec = manifest?.spec?.template?.spec || {};
|
const podSpec = manifest?.spec?.template?.spec || {};
|
||||||
setContainers((Array.isArray(podSpec.containers) ? podSpec.containers : []).map(toContainerForm));
|
setContainers((Array.isArray(podSpec.containers) ? podSpec.containers : []).map(toContainerForm));
|
||||||
setInitContainers((Array.isArray(podSpec.initContainers) ? podSpec.initContainers : []).map(toContainerForm));
|
setInitContainers((Array.isArray(podSpec.initContainers) ? podSpec.initContainers : []).map(toContainerForm));
|
||||||
setImagePullSecrets((Array.isArray(podSpec.imagePullSecrets) ? podSpec.imagePullSecrets : []).map((s: any) => s.name || ''));
|
setImagePullSecrets((Array.isArray(podSpec.imagePullSecrets) ? podSpec.imagePullSecrets : []).map((s: JsonData) => s.name || ''));
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [manifest]);
|
}, [manifest]);
|
||||||
|
|
||||||
|
|||||||
@@ -27,13 +27,14 @@ import {
|
|||||||
emptyVolumeEntry,
|
emptyVolumeEntry,
|
||||||
emptyMountRow,
|
emptyMountRow,
|
||||||
} from './deployment-volume-utils';
|
} from './deployment-volume-utils';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
clusterCode: string;
|
clusterCode: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
deploymentName: string;
|
deploymentName: string;
|
||||||
manifest: any | null;
|
manifest: JsonData | null;
|
||||||
onSaved: (updated?: any) => void;
|
onSaved: (updated?: JsonData) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,10 +55,10 @@ export function DeploymentEditStorage({ clusterCode, namespace, deploymentName,
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [loading, setLoading] = useState(!manifest);
|
const [loading, setLoading] = useState(!manifest);
|
||||||
const [rawManifest, setRawManifest] = useState<any>(manifest || {});
|
const [rawManifest, setRawManifest] = useState<JsonData>(manifest || {});
|
||||||
const [entries, setEntries] = useState<VolumeEntry[]>([]);
|
const [entries, setEntries] = useState<VolumeEntry[]>([]);
|
||||||
const [containers, setContainers] = useState<string[]>([]);
|
const [containers, setContainers] = useState<string[]>([]);
|
||||||
const [passthroughVolumes, setPassthroughVolumes] = useState<Record<string, any>[]>([]);
|
const [passthroughVolumes, setPassthroughVolumes] = useState<JsonData[]>([]);
|
||||||
const [resources, setResources] = useState<NamespaceResourceLists>({ configMaps: [], secrets: [], pvcs: [] });
|
const [resources, setResources] = useState<NamespaceResourceLists>({ configMaps: [], secrets: [], pvcs: [] });
|
||||||
const [resourcesLoading, setResourcesLoading] = useState(false);
|
const [resourcesLoading, setResourcesLoading] = useState(false);
|
||||||
const [createResource, setCreateResource] = useState<CreateResourceKind | null>(null);
|
const [createResource, setCreateResource] = useState<CreateResourceKind | null>(null);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { HistoryTab } from './deployment-view/HistoryTab';
|
|||||||
import type { DeploymentViewProps, SvcItem, IngItem, EventItem } from './deployment-view/types';
|
import type { DeploymentViewProps, SvcItem, IngItem, EventItem } from './deployment-view/types';
|
||||||
|
|
||||||
export type { DeploymentViewProps } from './deployment-view/types';
|
export type { DeploymentViewProps } from './deployment-view/types';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export function DeploymentView(props: DeploymentViewProps) {
|
export function DeploymentView(props: DeploymentViewProps) {
|
||||||
const { clusterCode, namespace, deployment, pods, revisions, onRefresh } = props;
|
const { clusterCode, namespace, deployment, pods, revisions, onRefresh } = props;
|
||||||
@@ -44,8 +45,8 @@ export function DeploymentView(props: DeploymentViewProps) {
|
|||||||
const [tabLoading, setTabLoading] = useState('');
|
const [tabLoading, setTabLoading] = useState('');
|
||||||
|
|
||||||
// HPA / CronHPA state // Logan.Li
|
// HPA / CronHPA state // Logan.Li
|
||||||
const [hpas, setHpas] = useState<any[]>([]);
|
const [hpas, setHpas] = useState<JsonData[]>([]);
|
||||||
const [cronHpas, setCronHpas] = useState<any[]>([]);
|
const [cronHpas, setCronHpas] = useState<JsonData[]>([]);
|
||||||
|
|
||||||
const ready = deployment?.ready_replicas ?? deployment?.readyReplicas ?? 0;
|
const ready = deployment?.ready_replicas ?? deployment?.readyReplicas ?? 0;
|
||||||
const total = deployment?.replicas ?? 0;
|
const total = deployment?.replicas ?? 0;
|
||||||
@@ -59,37 +60,37 @@ export function DeploymentView(props: DeploymentViewProps) {
|
|||||||
kubernetesApi.getNamespaceServices(clusterCode, namespace).catch(() => []),
|
kubernetesApi.getNamespaceServices(clusterCode, namespace).catch(() => []),
|
||||||
kubernetesApi.getNamespaceIngresses(clusterCode, namespace).catch(() => []),
|
kubernetesApi.getNamespaceIngresses(clusterCode, namespace).catch(() => []),
|
||||||
]).then(([svcRes, ingRes]) => {
|
]).then(([svcRes, ingRes]) => {
|
||||||
const toArr = (v: any) => (Array.isArray(v) ? v : (v?.list || v?.items || v?.data || []));
|
const toArr = (v: JsonData) => (Array.isArray(v) ? v : (v?.list || v?.items || v?.data || []));
|
||||||
const allSvcs = toArr(svcRes);
|
const allSvcs = toArr(svcRes);
|
||||||
const allIngs = toArr(ingRes);
|
const allIngs = toArr(ingRes);
|
||||||
|
|
||||||
const selector = deployment?.spec?.selector?.matchLabels || deployment?.spec?.template?.metadata?.labels || {};
|
const selector = deployment?.spec?.selector?.matchLabels || deployment?.spec?.template?.metadata?.labels || {};
|
||||||
const matchedSvcs = allSvcs.filter((s: any) => {
|
const matchedSvcs = allSvcs.filter((s: JsonData) => {
|
||||||
const svcSel = s.spec?.selector || s.selector || {};
|
const svcSel = s.spec?.selector || s.selector || {};
|
||||||
if (Object.keys(svcSel).length === 0) return false;
|
if (Object.keys(svcSel).length === 0) return false;
|
||||||
return Object.entries(svcSel).every(([k, v]) => selector[k] === v);
|
return Object.entries(svcSel).every(([k, v]) => selector[k] === v);
|
||||||
}).map((s: any) => ({
|
}).map((s: JsonData) => ({
|
||||||
name: s.name || s.metadata?.name,
|
name: s.name || s.metadata?.name,
|
||||||
type: s.spec?.type || s.type || 'ClusterIP',
|
type: s.spec?.type || s.type || 'ClusterIP',
|
||||||
clusterIP: s.spec?.clusterIP || s.cluster_ip || '<none>',
|
clusterIP: s.spec?.clusterIP || s.cluster_ip || '<none>',
|
||||||
ports: (s.spec?.ports || s.ports || []).map((p: any) => `${p.port}${p.targetPort ? ':' + p.targetPort : ''}/${p.protocol || 'TCP'}`),
|
ports: (s.spec?.ports || s.ports || []).map((p: JsonData) => `${p.port}${p.targetPort ? ':' + p.targetPort : ''}/${p.protocol || 'TCP'}`),
|
||||||
selector: s.spec?.selector || s.selector || {},
|
selector: s.spec?.selector || s.selector || {},
|
||||||
age: s.age || '',
|
age: s.age || '',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const svcNames = new Set(matchedSvcs.map((s: any) => s.name));
|
const svcNames = new Set(matchedSvcs.map((s: JsonData) => s.name));
|
||||||
const matchedIngs = allIngs.filter((i: any) => {
|
const matchedIngs = allIngs.filter((i: JsonData) => {
|
||||||
const rules = i.spec?.rules || i.rules || [];
|
const rules = i.spec?.rules || i.rules || [];
|
||||||
return rules.some((r: any) =>
|
return rules.some((r: JsonData) =>
|
||||||
(r.http?.paths || r.paths || []).some((p: any) => {
|
(r.http?.paths || r.paths || []).some((p: JsonData) => {
|
||||||
const backendSvc = p.backend?.service?.name || p.backendService || '';
|
const backendSvc = p.backend?.service?.name || p.backendService || '';
|
||||||
return svcNames.has(backendSvc);
|
return svcNames.has(backendSvc);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}).map((i: any) => ({
|
}).map((i: JsonData) => ({
|
||||||
name: i.name || i.metadata?.name,
|
name: i.name || i.metadata?.name,
|
||||||
className: i.spec?.ingressClassName || i.ingressClass || '-',
|
className: i.spec?.ingressClassName || i.ingressClass || '-',
|
||||||
hosts: (i.spec?.rules || i.rules || []).map((r: any) => r.host).filter(Boolean),
|
hosts: (i.spec?.rules || i.rules || []).map((r: JsonData) => r.host).filter(Boolean),
|
||||||
rules: i.spec?.rules || i.rules || [],
|
rules: i.spec?.rules || i.rules || [],
|
||||||
age: i.age || '',
|
age: i.age || '',
|
||||||
}));
|
}));
|
||||||
@@ -103,16 +104,16 @@ export function DeploymentView(props: DeploymentViewProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tab !== 'events') return;
|
if (tab !== 'events') return;
|
||||||
setTabLoading('events');
|
setTabLoading('events');
|
||||||
kubernetesApi.getEvents(clusterCode, namespace, undefined).then((res: any) => {
|
kubernetesApi.getEvents(clusterCode, namespace, undefined).then((res: JsonData) => {
|
||||||
const allEvents = Array.isArray(res) ? res : (res?.list || res?.items || res?.data || []);
|
const allEvents = Array.isArray(res) ? res : (res?.list || res?.items || res?.data || []);
|
||||||
const depName = deployment?.name || '';
|
const depName = deployment?.name || '';
|
||||||
const depEvents = allEvents.filter((e: any) => {
|
const depEvents = allEvents.filter((e: JsonData) => {
|
||||||
const obj = String(e.object || '');
|
const obj = String(e.object || '');
|
||||||
if (!depName || !obj) return false;
|
if (!depName || !obj) return false;
|
||||||
if (obj.endsWith(`/${depName}`)) return true;
|
if (obj.endsWith(`/${depName}`)) return true;
|
||||||
const name = obj.split('/').pop() || '';
|
const name = obj.split('/').pop() || '';
|
||||||
return name === depName || name.startsWith(`${depName}-`);
|
return name === depName || name.startsWith(`${depName}-`);
|
||||||
}).map((e: any) => normalizeK8sEvent(e));
|
}).map((e: JsonData) => normalizeK8sEvent(e));
|
||||||
setEvents(depEvents);
|
setEvents(depEvents);
|
||||||
}).catch(() => {}).finally(() => setTabLoading(''));
|
}).catch(() => {}).finally(() => setTabLoading(''));
|
||||||
}, [tab, clusterCode, namespace, deployment?.name]);
|
}, [tab, clusterCode, namespace, deployment?.name]);
|
||||||
@@ -125,16 +126,16 @@ export function DeploymentView(props: DeploymentViewProps) {
|
|||||||
kubernetesApi.getHPAs(clusterCode, namespace).catch(() => []),
|
kubernetesApi.getHPAs(clusterCode, namespace).catch(() => []),
|
||||||
Promise.resolve([]), // CronHPA 待后端支持
|
Promise.resolve([]), // CronHPA 待后端支持
|
||||||
]).then(([hpaRes]) => {
|
]).then(([hpaRes]) => {
|
||||||
const toArr = (v: any) => (Array.isArray(v) ? v : (v?.list || v?.items || v?.data || []));
|
const toArr = (v: JsonData) => (Array.isArray(v) ? v : (v?.list || v?.items || v?.data || []));
|
||||||
const allHpas = toArr(hpaRes);
|
const allHpas = toArr(hpaRes);
|
||||||
const depName = deployment?.name || '';
|
const depName = deployment?.name || '';
|
||||||
setHpas(allHpas.filter((h: any) => (h.scaleTargetRef?.name || h.targetRef?.name || '') === depName));
|
setHpas(allHpas.filter((h: JsonData) => (h.scaleTargetRef?.name || h.targetRef?.name || '') === depName));
|
||||||
setCronHpas([]);
|
setCronHpas([]);
|
||||||
}).catch(() => {}).finally(() => setTabLoading(''));
|
}).catch(() => {}).finally(() => setTabLoading(''));
|
||||||
}, [tab, clusterCode, namespace, deployment?.name]);
|
}, [tab, clusterCode, namespace, deployment?.name]);
|
||||||
|
|
||||||
// 回滚 // Logan.Li
|
// 回滚 // Logan.Li
|
||||||
const handleRollback = async (rev: any) => {
|
const handleRollback = async (rev: JsonData) => {
|
||||||
const revision = rev.revision ?? rev.revision_number ?? rev.number;
|
const revision = rev.revision ?? rev.revision_number ?? rev.number;
|
||||||
const ok = await confirm({
|
const ok = await confirm({
|
||||||
title: '回滚版本',
|
title: '回滚版本',
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { RefreshCw, ChevronRight, Bell, AlertTriangle, X } from 'lucide-react';
|
|||||||
import { kubernetesApi } from '@/api';
|
import { kubernetesApi } from '@/api';
|
||||||
import { EventTable } from './EventTable';
|
import { EventTable } from './EventTable';
|
||||||
import { normalizeK8sEvent, formatEventRelative } from './k8s-event-utils';
|
import { normalizeK8sEvent, formatEventRelative } from './k8s-event-utils';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
interface RankItem { name: string; count: number }
|
interface RankItem { name: string; count: number }
|
||||||
|
|
||||||
@@ -76,20 +77,20 @@ export default function EventsPage() {
|
|||||||
const { clusterCode } = useParams<{ clusterCode: string }>();
|
const { clusterCode } = useParams<{ clusterCode: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [events, setEvents] = useState<any[]>([]);
|
const [events, setEvents] = useState<JsonData[]>([]);
|
||||||
const [clusters, setClusters] = useState<any[]>([]);
|
const [clusters, setClusters] = useState<JsonData[]>([]);
|
||||||
const [namespaces, setNamespaces] = useState<string[]>([]);
|
const [namespaces, setNamespaces] = useState<string[]>([]);
|
||||||
const [selectedNs, setSelectedNs] = useState('all');
|
const [selectedNs, setSelectedNs] = useState('all');
|
||||||
const [filterType, setFilterType] = useState('all');
|
const [filterType, setFilterType] = useState('all');
|
||||||
const [timeWindow, setTimeWindow] = useState('0');
|
const [timeWindow, setTimeWindow] = useState('0');
|
||||||
const [activeCluster, setActiveCluster] = useState(clusterCode || '');
|
const [activeCluster, setActiveCluster] = useState(clusterCode || '');
|
||||||
const [summary, setSummary] = useState<any>(null);
|
const [summary, setSummary] = useState<JsonData>(null);
|
||||||
const [selectedReason, setSelectedReason] = useState('');
|
const [selectedReason, setSelectedReason] = useState('');
|
||||||
const [selectedKind, setSelectedKind] = useState('');
|
const [selectedKind, setSelectedKind] = useState('');
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
kubernetesApi.getClusters().then((res: any) => {
|
kubernetesApi.getClusters().then((res: JsonData) => {
|
||||||
const list = res?.list || [];
|
const list = res?.list || [];
|
||||||
setClusters(list);
|
setClusters(list);
|
||||||
if (!activeCluster && list.length > 0) {
|
if (!activeCluster && list.length > 0) {
|
||||||
@@ -102,7 +103,7 @@ export default function EventsPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeCluster) return;
|
if (!activeCluster) return;
|
||||||
kubernetesApi.getNamespaces(activeCluster).then((res: any) => {
|
kubernetesApi.getNamespaces(activeCluster).then((res: JsonData) => {
|
||||||
setNamespaces(Array.isArray(res) ? res : (res?.list || res?.data || []));
|
setNamespaces(Array.isArray(res) ? res : (res?.list || res?.data || []));
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}, [activeCluster]);
|
}, [activeCluster]);
|
||||||
@@ -113,7 +114,7 @@ export default function EventsPage() {
|
|||||||
try {
|
try {
|
||||||
const ns = selectedNs === 'all' ? undefined : selectedNs;
|
const ns = selectedNs === 'all' ? undefined : selectedNs;
|
||||||
const ft = filterType === 'all' ? undefined : filterType;
|
const ft = filterType === 'all' ? undefined : filterType;
|
||||||
const res: Record<string, any> = await kubernetesApi.getEvents(activeCluster, ns, ft);
|
const res: JsonData = await kubernetesApi.getEvents(activeCluster, ns, ft);
|
||||||
setEvents(Array.isArray(res) ? res : (res?.list || []));
|
setEvents(Array.isArray(res) ? res : (res?.list || []));
|
||||||
} catch { setEvents([]); } finally { setLoading(false); }
|
} catch { setEvents([]); } finally { setLoading(false); }
|
||||||
}, [activeCluster, selectedNs, filterType]);
|
}, [activeCluster, selectedNs, filterType]);
|
||||||
@@ -123,7 +124,7 @@ export default function EventsPage() {
|
|||||||
try {
|
try {
|
||||||
const ns = selectedNs === 'all' ? undefined : selectedNs;
|
const ns = selectedNs === 'all' ? undefined : selectedNs;
|
||||||
const hours = parseInt(timeWindow) || undefined;
|
const hours = parseInt(timeWindow) || undefined;
|
||||||
const res: Record<string, any> = await kubernetesApi.getEventsSummary(activeCluster, ns, hours);
|
const res: JsonData = await kubernetesApi.getEventsSummary(activeCluster, ns, hours);
|
||||||
setSummary(res || null);
|
setSummary(res || null);
|
||||||
} catch { setSummary(null); }
|
} catch { setSummary(null); }
|
||||||
}, [activeCluster, selectedNs, timeWindow]);
|
}, [activeCluster, selectedNs, timeWindow]);
|
||||||
@@ -151,15 +152,15 @@ export default function EventsPage() {
|
|||||||
const normalCount = useMemo(() => events.filter((e) => e.type === 'Normal').length, [events]);
|
const normalCount = useMemo(() => events.filter((e) => e.type === 'Normal').length, [events]);
|
||||||
|
|
||||||
const reasonData = useMemo<RankItem[]>(() =>
|
const reasonData = useMemo<RankItem[]>(() =>
|
||||||
(summary?.by_reason || []).map((r: any) => ({ name: r.reason || 'Unknown', count: r.count })),
|
(summary?.by_reason || []).map((r: JsonData) => ({ name: r.reason || 'Unknown', count: r.count })),
|
||||||
[summary]);
|
[summary]);
|
||||||
|
|
||||||
const nsData = useMemo<RankItem[]>(() =>
|
const nsData = useMemo<RankItem[]>(() =>
|
||||||
(summary?.by_namespace || []).map((n: any) => ({ name: n.namespace || '<cluster>', count: n.count })),
|
(summary?.by_namespace || []).map((n: JsonData) => ({ name: n.namespace || '<cluster>', count: n.count })),
|
||||||
[summary]);
|
[summary]);
|
||||||
|
|
||||||
const kindData = useMemo<RankItem[]>(() =>
|
const kindData = useMemo<RankItem[]>(() =>
|
||||||
(summary?.by_kind || []).map((k: any) => ({ name: k.kind || 'Unknown', count: k.count })),
|
(summary?.by_kind || []).map((k: JsonData) => ({ name: k.kind || 'Unknown', count: k.count })),
|
||||||
[summary]);
|
[summary]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -185,7 +186,7 @@ export default function EventsPage() {
|
|||||||
<Select value={activeCluster} onValueChange={(v) => { setActiveCluster(v); navigate(`/kubernetes/${v}/events`); }}>
|
<Select value={activeCluster} onValueChange={(v) => { setActiveCluster(v); navigate(`/kubernetes/${v}/events`); }}>
|
||||||
<SelectTrigger className="w-40 h-8 text-xs"><SelectValue /></SelectTrigger>
|
<SelectTrigger className="w-40 h-8 text-xs"><SelectValue /></SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{clusters.map((c: any) => (
|
{clusters.map((c: JsonData) => (
|
||||||
<SelectItem key={c.id} value={c.code || String(c.id)} className="text-xs">{c.name || c.code}</SelectItem>
|
<SelectItem key={c.id} value={c.code || String(c.id)} className="text-xs">{c.name || c.code}</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -260,7 +261,7 @@ export default function EventsPage() {
|
|||||||
最新告警 ({summary.recent_warnings.length})
|
最新告警 ({summary.recent_warnings.length})
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{summary.recent_warnings.map((w: any, i: number) => (
|
{summary.recent_warnings.map((w: JsonData, i: number) => (
|
||||||
<div key={i} className="text-xs py-1.5 px-2 rounded hover:bg-[var(--dg-bg-tertiary)]/50">
|
<div key={i} className="text-xs py-1.5 px-2 rounded hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<span className="shrink-0 font-mono text-[var(--dg-warning)]">{w.reason}</span>
|
<span className="shrink-0 font-mono text-[var(--dg-warning)]">{w.reason}</span>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useEffect } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import { kubernetesApi } from '@/api';
|
import { kubernetesApi } from '@/api';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** 集群下路径,如 events、namespace/default/deployments */
|
/** 集群下路径,如 events、namespace/default/deployments */
|
||||||
@@ -14,7 +15,7 @@ export default function K8sShortcutRedirect({ target }: Props) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
kubernetesApi.getClusters()
|
kubernetesApi.getClusters()
|
||||||
.then((res: any) => {
|
.then((res: JsonData) => {
|
||||||
const list = res?.list || [];
|
const list = res?.list || [];
|
||||||
if (list.length > 0) {
|
if (list.length > 0) {
|
||||||
const code = list[0].code || String(list[0].id);
|
const code = list[0].code || String(list[0].id);
|
||||||
|
|||||||
@@ -14,17 +14,18 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
// 资源使用趋势数据
|
// 资源使用趋势数据
|
||||||
// 由于当前无持久化时序存储,这里基于当前节点 metrics 构造一个简短的模拟趋势(最近几分钟变化)
|
// 由于当前无持久化时序存储,这里基于当前节点 metrics 构造一个简短的模拟趋势(最近几分钟变化)
|
||||||
// 真正历史数据需要后端记录指标快照后才能实现 // Logan.Li
|
// 真正历史数据需要后端记录指标快照后才能实现 // Logan.Li
|
||||||
|
|
||||||
export function KubernetesClusterDetail({ cluster, onClose }: { cluster: any; onClose: () => void }) {
|
export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonData; onClose: () => void }) {
|
||||||
const [nodes, setNodes] = useState<any[]>([]);
|
const [nodes, setNodes] = useState<JsonData[]>([]);
|
||||||
const [pods, setPods] = useState<any[]>([]);
|
const [pods, setPods] = useState<JsonData[]>([]);
|
||||||
const [deployments, setDeployments] = useState<any[]>([]);
|
const [deployments, setDeployments] = useState<JsonData[]>([]);
|
||||||
const [services, setServices] = useState<any[]>([]);
|
const [services, setServices] = useState<JsonData[]>([]);
|
||||||
const [ingresses, setIngresses] = useState<any[]>([]);
|
const [ingresses, setIngresses] = useState<JsonData[]>([]);
|
||||||
|
|
||||||
// 从节点数据计算集群级 CPU/内存使用率(实时快照) // Logan.Li
|
// 从节点数据计算集群级 CPU/内存使用率(实时快照) // Logan.Li
|
||||||
const clusterMetrics = useMemo(() => {
|
const clusterMetrics = useMemo(() => {
|
||||||
@@ -76,7 +77,7 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster: any; on
|
|||||||
kubernetesApi.getServices(clusterId).catch(() => []),
|
kubernetesApi.getServices(clusterId).catch(() => []),
|
||||||
kubernetesApi.getIngresses(clusterId).catch(() => []),
|
kubernetesApi.getIngresses(clusterId).catch(() => []),
|
||||||
]);
|
]);
|
||||||
const toArray = ( v: any) => (Array.isArray(v) ? v : (v?.list || []));
|
const toArray = (v: JsonData) => (Array.isArray(v) ? v : (v?.list || []));
|
||||||
setNodes(toArray(nodesRes));
|
setNodes(toArray(nodesRes));
|
||||||
setPods(toArray(podsRes));
|
setPods(toArray(podsRes));
|
||||||
setDeployments(toArray(depRes));
|
setDeployments(toArray(depRes));
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { kubernetesApi } from '@/api';
|
import { kubernetesApi } from '@/api';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
// 名称即 code,只允许英文/数字/连字符
|
// 名称即 code,只允许英文/数字/连字符
|
||||||
const NAME_RE = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
|
const NAME_RE = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
|
||||||
@@ -36,7 +37,7 @@ export function KubernetesClusterDialog({
|
|||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
editingCluster: any;
|
editingCluster?: JsonData;
|
||||||
onSaved: () => void;
|
onSaved: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [formData, setFormData] = useState({ ...emptyForm });
|
const [formData, setFormData] = useState({ ...emptyForm });
|
||||||
@@ -48,7 +49,7 @@ export function KubernetesClusterDialog({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
if (editingCluster && editingCluster.id) {
|
if (editingCluster && editingCluster.id) {
|
||||||
kubernetesApi.getClusterDetail(editingCluster.id).then(( res: any) => {
|
kubernetesApi.getClusterDetail(editingCluster.id).then((res: JsonData) => {
|
||||||
const d = res || {};
|
const d = res || {};
|
||||||
setHasToken(!!d.has_token);
|
setHasToken(!!d.has_token);
|
||||||
setHasKubeConfig(!!d.has_kube_config);
|
setHasKubeConfig(!!d.has_kube_config);
|
||||||
|
|||||||
@@ -34,29 +34,30 @@ import type { KubernetesCluster } from '@/types';
|
|||||||
import { KubernetesClusterDetail } from './KubernetesClusterDetail';
|
import { KubernetesClusterDetail } from './KubernetesClusterDetail';
|
||||||
import { KubernetesClusterDialog } from './KubernetesClusterDialog';
|
import { KubernetesClusterDialog } from './KubernetesClusterDialog';
|
||||||
import { KubectlTerminal } from './KubectlTerminal';
|
import { KubectlTerminal } from './KubectlTerminal';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export function KubernetesClusters() {
|
export function KubernetesClusters() {
|
||||||
const [clusters, setClusters] = useState<any[]>([]);
|
const [clusters, setClusters] = useState<JsonData[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [selectedCluster, setSelectedCluster] = useState<string | null>(null);
|
const [selectedCluster, setSelectedCluster] = useState<string | null>(null);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
// 创建/编辑集群弹窗
|
// 创建/编辑集群弹窗
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [editingCluster, setEditingCluster] = useState<any>(null);
|
const [editingCluster, setEditingCluster] = useState<JsonData>(null);
|
||||||
|
|
||||||
// kubectl 终端
|
// kubectl 终端
|
||||||
const [kubectlCluster, setKubectlCluster] = useState<{id: number; name: string} | null>(null);
|
const [kubectlCluster, setKubectlCluster] = useState<{id: number; name: string} | null>(null);
|
||||||
const [kubectlTerminalOpen, setKubectlTerminalOpen] = useState(false);
|
const [kubectlTerminalOpen, setKubectlTerminalOpen] = useState(false);
|
||||||
|
|
||||||
// 导出资源弹窗
|
// 导出资源弹窗
|
||||||
const [exportCluster, setExportCluster] = useState<any>(null);
|
const [exportCluster, setExportCluster] = useState<JsonData>(null);
|
||||||
const [exporting, setExporting] = useState(false);
|
const [exporting, setExporting] = useState(false);
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res: Record<string, any> = await kubernetesApi.getClusters();
|
const res: JsonData = await kubernetesApi.getClusters();
|
||||||
setClusters(res?.list || []);
|
setClusters(res?.list || []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载集群列表失败:', e);
|
console.error('加载集群列表失败:', e);
|
||||||
@@ -78,7 +79,7 @@ export function KubernetesClusters() {
|
|||||||
if (!editingCluster) {
|
if (!editingCluster) {
|
||||||
// 新建集群后,等列表刷新完再取最新一条同步
|
// 新建集群后,等列表刷新完再取最新一条同步
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const res: Record<string, any> = await kubernetesApi.getClusters();
|
const res: JsonData = await kubernetesApi.getClusters();
|
||||||
const list = res?.list || [];
|
const list = res?.list || [];
|
||||||
if (list.length > 0) {
|
if (list.length > 0) {
|
||||||
handleSync(list[list.length - 1].id, true);
|
handleSync(list[list.length - 1].id, true);
|
||||||
@@ -100,7 +101,7 @@ export function KubernetesClusters() {
|
|||||||
const handleSync = async (id: number | string, silent?: boolean) => {
|
const handleSync = async (id: number | string, silent?: boolean) => {
|
||||||
try {
|
try {
|
||||||
if (!silent) toast.info('正在同步集群资源...');
|
if (!silent) toast.info('正在同步集群资源...');
|
||||||
const res: Record<string, any> = await kubernetesApi.syncCluster(id);
|
const res: JsonData = await kubernetesApi.syncCluster(id);
|
||||||
if (!silent) toast.success(res?.message || '同步完成');
|
if (!silent) toast.success(res?.message || '同步完成');
|
||||||
loadData();
|
loadData();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -11,18 +11,19 @@ import {
|
|||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { RefreshCw, Search, Server, Loader2 } from 'lucide-react';
|
import { RefreshCw, Search, Server, Loader2 } from 'lucide-react';
|
||||||
import { kubernetesApi } from '@/api';
|
import { kubernetesApi } from '@/api';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export function KubernetesNodes() {
|
export function KubernetesNodes() {
|
||||||
const [clusters, setClusters] = useState<any[]>([]);
|
const [clusters, setClusters] = useState<JsonData[]>([]);
|
||||||
const [selectedCluster, setSelectedCluster] = useState('');
|
const [selectedCluster, setSelectedCluster] = useState('');
|
||||||
const [nodes, setNodes] = useState<any[]>([]);
|
const [nodes, setNodes] = useState<JsonData[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [roleFilter, setRoleFilter] = useState('all');
|
const [roleFilter, setRoleFilter] = useState('all');
|
||||||
|
|
||||||
const loadClusters = useCallback(async () => {
|
const loadClusters = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res: Record<string, any> = await kubernetesApi.getClusters();
|
const res: JsonData = await kubernetesApi.getClusters();
|
||||||
const list = res?.list || [];
|
const list = res?.list || [];
|
||||||
setClusters(list);
|
setClusters(list);
|
||||||
if (list.length > 0 && !selectedCluster) setSelectedCluster(String(list[0].id));
|
if (list.length > 0 && !selectedCluster) setSelectedCluster(String(list[0].id));
|
||||||
@@ -35,7 +36,7 @@ export function KubernetesNodes() {
|
|||||||
if (!selectedCluster) return;
|
if (!selectedCluster) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res: Record<string, any> = await kubernetesApi.getNodes(selectedCluster);
|
const res: JsonData = await kubernetesApi.getNodes(selectedCluster);
|
||||||
setNodes(Array.isArray(res) ? res : (res?.list || []));
|
setNodes(Array.isArray(res) ? res : (res?.list || []));
|
||||||
} catch { setNodes([]); } finally { setLoading(false); }
|
} catch { setNodes([]); } finally { setLoading(false); }
|
||||||
}, [selectedCluster]);
|
}, [selectedCluster]);
|
||||||
|
|||||||
@@ -25,15 +25,16 @@ import {
|
|||||||
import { kubernetesApi } from '@/api';
|
import { kubernetesApi } from '@/api';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { DeploymentDetail } from './DeploymentDetail';
|
import { DeploymentDetail } from './DeploymentDetail';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export function KubernetesWorkloads() {
|
export function KubernetesWorkloads() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [clusters, setClusters] = useState<any[]>([]);
|
const [clusters, setClusters] = useState<JsonData[]>([]);
|
||||||
const [selectedCluster, setSelectedCluster] = useState('');
|
const [selectedCluster, setSelectedCluster] = useState('');
|
||||||
const [namespaces, setNamespaces] = useState<string[]>([]);
|
const [namespaces, setNamespaces] = useState<string[]>([]);
|
||||||
const [selectedNamespace, setSelectedNamespace] = useState('all');
|
const [selectedNamespace, setSelectedNamespace] = useState('all');
|
||||||
const [deployments, setDeployments] = useState<any[]>([]);
|
const [deployments, setDeployments] = useState<JsonData[]>([]);
|
||||||
const [pods, setPods] = useState<any[]>([]);
|
const [pods, setPods] = useState<JsonData[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [activeTab, setActiveTab] = useState('deployments');
|
const [activeTab, setActiveTab] = useState('deployments');
|
||||||
@@ -44,7 +45,7 @@ export function KubernetesWorkloads() {
|
|||||||
|
|
||||||
const loadClusters = useCallback(async () => {
|
const loadClusters = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res: Record<string, any> = await kubernetesApi.getClusters();
|
const res: JsonData = await kubernetesApi.getClusters();
|
||||||
const list = res?.list || [];
|
const list = res?.list || [];
|
||||||
setClusters(list);
|
setClusters(list);
|
||||||
if (list.length > 0 && !selectedCluster) setSelectedCluster(list[0].code || String(list[0].id));
|
if (list.length > 0 && !selectedCluster) setSelectedCluster(list[0].code || String(list[0].id));
|
||||||
@@ -57,7 +58,7 @@ export function KubernetesWorkloads() {
|
|||||||
const loadNamespaces = useCallback(async () => {
|
const loadNamespaces = useCallback(async () => {
|
||||||
if (!selectedCluster) return;
|
if (!selectedCluster) return;
|
||||||
try {
|
try {
|
||||||
const res: Record<string, any> = await kubernetesApi.getNamespaces(selectedCluster);
|
const res: JsonData = await kubernetesApi.getNamespaces(selectedCluster);
|
||||||
const nsList: string[] = Array.isArray(res) ? res : (res?.list || res?.data || []);
|
const nsList: string[] = Array.isArray(res) ? res : (res?.list || res?.data || []);
|
||||||
setNamespaces(nsList);
|
setNamespaces(nsList);
|
||||||
} catch { setNamespaces([]); }
|
} catch { setNamespaces([]); }
|
||||||
@@ -75,7 +76,7 @@ export function KubernetesWorkloads() {
|
|||||||
kubernetesApi.getNamespaceDeployments(clusterCode, selectedNamespace).catch(() => []),
|
kubernetesApi.getNamespaceDeployments(clusterCode, selectedNamespace).catch(() => []),
|
||||||
kubernetesApi.getNamespacePods(clusterCode, selectedNamespace).catch(() => []),
|
kubernetesApi.getNamespacePods(clusterCode, selectedNamespace).catch(() => []),
|
||||||
]);
|
]);
|
||||||
const toArray = ( v: any) => (Array.isArray(v) ? v : (v?.list || []));
|
const toArray = (v: JsonData) => (Array.isArray(v) ? v : (v?.list || []));
|
||||||
setDeployments(toArray(depRes));
|
setDeployments(toArray(depRes));
|
||||||
setPods(toArray(podRes));
|
setPods(toArray(podRes));
|
||||||
} else {
|
} else {
|
||||||
@@ -83,7 +84,7 @@ export function KubernetesWorkloads() {
|
|||||||
kubernetesApi.getAllDeployments(clusterCode).catch(() => []),
|
kubernetesApi.getAllDeployments(clusterCode).catch(() => []),
|
||||||
kubernetesApi.getAllPods(clusterCode).catch(() => []),
|
kubernetesApi.getAllPods(clusterCode).catch(() => []),
|
||||||
]);
|
]);
|
||||||
const toArray = ( v: any) => (Array.isArray(v) ? v : (v?.list || []));
|
const toArray = (v: JsonData) => (Array.isArray(v) ? v : (v?.list || []));
|
||||||
setDeployments(toArray(depRes));
|
setDeployments(toArray(depRes));
|
||||||
setPods(toArray(podRes));
|
setPods(toArray(podRes));
|
||||||
}
|
}
|
||||||
@@ -111,7 +112,7 @@ export function KubernetesWorkloads() {
|
|||||||
return <Badge className={map[status] || 'bg-slate-500/20 text-slate-400 border-slate-500/30'}>{status || 'Unknown'}</Badge>;
|
return <Badge className={map[status] || 'bg-slate-500/20 text-slate-400 border-slate-500/30'}>{status || 'Unknown'}</Badge>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleQuickScale = async ( dep: any, delta: number) => {
|
const handleQuickScale = async (dep: JsonData, delta: number) => {
|
||||||
const key = `scale-${dep.name}`;
|
const key = `scale-${dep.name}`;
|
||||||
setActionLoading((prev) => ({ ...prev, [key]: 'loading' }));
|
setActionLoading((prev) => ({ ...prev, [key]: 'loading' }));
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { kubernetesApi } from '@/api';
|
import { kubernetesApi } from '@/api';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
const RESOURCE_LINKS = [
|
const RESOURCE_LINKS = [
|
||||||
{ type: 'deployments', label: 'Deployments', icon: Boxes, desc: '工作负载副本管理' },
|
{ type: 'deployments', label: 'Deployments', icon: Boxes, desc: '工作负载副本管理' },
|
||||||
@@ -45,7 +46,7 @@ export default function NamespaceOverviewPage() {
|
|||||||
kubernetesApi.getHPAs(clusterCode, namespace!),
|
kubernetesApi.getHPAs(clusterCode, namespace!),
|
||||||
kubernetesApi.getCronJobs(clusterCode, namespace!),
|
kubernetesApi.getCronJobs(clusterCode, namespace!),
|
||||||
]);
|
]);
|
||||||
const toArr = ( v: any) => Array.isArray(v) ? v : (v?.list || v?.data || []);
|
const toArr = (v: JsonData) => Array.isArray(v) ? v : (v?.list || v?.data || []);
|
||||||
setCounts({
|
setCounts({
|
||||||
deployments: toArr(results[0].status === 'fulfilled' ? results[0].value : []).length,
|
deployments: toArr(results[0].status === 'fulfilled' ? results[0].value : []).length,
|
||||||
pods: toArr(results[1].status === 'fulfilled' ? results[1].value : []).length,
|
pods: toArr(results[1].status === 'fulfilled' ? results[1].value : []).length,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
HardDrive,
|
HardDrive,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
|
|
||||||
export interface PodFileBrowserProps {
|
export interface PodFileBrowserProps {
|
||||||
@@ -54,10 +55,10 @@ export function PodFileBrowser({
|
|||||||
setError('');
|
setError('');
|
||||||
setPreview(null);
|
setPreview(null);
|
||||||
try {
|
try {
|
||||||
const res: Record<string, any> = await kubernetesApi.listPodFiles(clusterCode, namespace, podName, path, container || undefined);
|
const res: JsonData = await kubernetesApi.listPodFiles(clusterCode, namespace, podName, path, container || undefined);
|
||||||
// 兼容后端 {files: [...]} 和 {entries: [...]} 两种返回格式
|
// 兼容后端 {files: [...]} 和 {entries: [...]} 两种返回格式
|
||||||
const raw: Record<string, any>[] = res?.entries || res?.files || res?.data?.entries || res?.data?.files || (Array.isArray(res) ? res : []);
|
const raw: JsonData[] = res?.entries || res?.files || res?.data?.entries || res?.data?.files || (Array.isArray(res) ? res : []);
|
||||||
const list: FileEntry[] = raw.map(( it: any) => ({
|
const list: FileEntry[] = raw.map((it: JsonData) => ({
|
||||||
name: it.name ?? it.Name ?? '',
|
name: it.name ?? it.Name ?? '',
|
||||||
isDir: Boolean(it.isDir ?? it.is_dir ?? it.IsDir ?? it.dir ?? it.directory),
|
isDir: Boolean(it.isDir ?? it.is_dir ?? it.IsDir ?? it.dir ?? it.directory),
|
||||||
size: typeof it.size === 'number' ? it.size : (it.Size ? Number(it.Size) : undefined),
|
size: typeof it.size === 'number' ? it.size : (it.Size ? Number(it.Size) : undefined),
|
||||||
@@ -66,7 +67,7 @@ export function PodFileBrowser({
|
|||||||
setEntries(list);
|
setEntries(list);
|
||||||
setCurrentPath(path);
|
setCurrentPath(path);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError((e as Record<string, any>).message || '加载文件列表失败');
|
setError((e as JsonData).message || '加载文件列表失败');
|
||||||
setEntries([]);
|
setEntries([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -83,7 +84,7 @@ export function PodFileBrowser({
|
|||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res: Record<string, any> = await kubernetesApi.execPod(clusterCode, namespace, podName, ['pwd'], container || undefined);
|
const res: JsonData = await kubernetesApi.execPod(clusterCode, namespace, podName, ['pwd'], container || undefined);
|
||||||
const stdout = (res?.stdout || res?.data?.stdout || '').trim();
|
const stdout = (res?.stdout || res?.data?.stdout || '').trim();
|
||||||
const workdir = stdout && stdout.startsWith('/') ? stdout : '/';
|
const workdir = stdout && stdout.startsWith('/') ? stdout : '/';
|
||||||
workdirDetected.current = true;
|
workdirDetected.current = true;
|
||||||
@@ -116,7 +117,7 @@ export function PodFileBrowser({
|
|||||||
const text = await kubernetesApi.readPodFileText(clusterCode, namespace, podName, filePath, container || undefined);
|
const text = await kubernetesApi.readPodFileText(clusterCode, namespace, podName, filePath, container || undefined);
|
||||||
setPreview({ file: fileName, content: text });
|
setPreview({ file: fileName, content: text });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setPreview({ file: fileName, content: `读取失败: ${(e as Record<string, any>).message}` });
|
setPreview({ file: fileName, content: `读取失败: ${(e as JsonData).message}` });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -134,7 +135,7 @@ export function PodFileBrowser({
|
|||||||
await kubernetesApi.uploadPodFile(clusterCode, namespace, podName, currentPath, file, container || undefined);
|
await kubernetesApi.uploadPodFile(clusterCode, namespace, podName, currentPath, file, container || undefined);
|
||||||
loadFiles(currentPath);
|
loadFiles(currentPath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(`上传失败: ${(err as Record<string, any>).message}`);
|
setError(`上传失败: ${(err as JsonData).message}`);
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { kubernetesApi } from '@/api';
|
import { kubernetesApi } from '@/api';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 增强版 Pod 日志查看器 — 增强 DeploymentDetail 的日志体验
|
* 增强版 Pod 日志查看器 — 增强 DeploymentDetail 的日志体验
|
||||||
@@ -153,7 +154,7 @@ export function PodLogsViewer({
|
|||||||
linesRef.current = [];
|
linesRef.current = [];
|
||||||
setLines([]);
|
setLines([]);
|
||||||
try {
|
try {
|
||||||
const res: Record<string, any> = await kubernetesApi.getPodLogsEnhanced(clusterCode, namespace, podName, {
|
const res: JsonData = await kubernetesApi.getPodLogsEnhanced(clusterCode, namespace, podName, {
|
||||||
container: container || undefined,
|
container: container || undefined,
|
||||||
tailLines: tailLines,
|
tailLines: tailLines,
|
||||||
previous,
|
previous,
|
||||||
|
|||||||
@@ -11,19 +11,20 @@ import {
|
|||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { Cpu, HardDrive, RefreshCw, ChevronRight, Activity } from 'lucide-react';
|
import { Cpu, HardDrive, RefreshCw, ChevronRight, Activity } from 'lucide-react';
|
||||||
import { kubernetesApi } from '@/api';
|
import { kubernetesApi } from '@/api';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export default function PodMetricsPage() {
|
export default function PodMetricsPage() {
|
||||||
const { clusterCode } = useParams<{ clusterCode: string }>();
|
const { clusterCode } = useParams<{ clusterCode: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [metrics, setMetrics] = useState<any[]>([]);
|
const [metrics, setMetrics] = useState<JsonData[]>([]);
|
||||||
const [clusters, setClusters] = useState<any[]>([]);
|
const [clusters, setClusters] = useState<JsonData[]>([]);
|
||||||
const [namespaces, setNamespaces] = useState<string[]>([]);
|
const [namespaces, setNamespaces] = useState<string[]>([]);
|
||||||
const [selectedNs, setSelectedNs] = useState('all');
|
const [selectedNs, setSelectedNs] = useState('all');
|
||||||
const [activeCluster, setActiveCluster] = useState(clusterCode || '');
|
const [activeCluster, setActiveCluster] = useState(clusterCode || '');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
kubernetesApi.getClusters().then(( res: any) => {
|
kubernetesApi.getClusters().then((res: JsonData) => {
|
||||||
const list = res?.list || [];
|
const list = res?.list || [];
|
||||||
setClusters(list);
|
setClusters(list);
|
||||||
if (!activeCluster && list.length > 0) {
|
if (!activeCluster && list.length > 0) {
|
||||||
@@ -36,7 +37,7 @@ export default function PodMetricsPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeCluster) return;
|
if (!activeCluster) return;
|
||||||
kubernetesApi.getNamespaces(activeCluster).then(( res: any) => {
|
kubernetesApi.getNamespaces(activeCluster).then((res: JsonData) => {
|
||||||
setNamespaces(Array.isArray(res) ? res : (res?.list || res?.data || []));
|
setNamespaces(Array.isArray(res) ? res : (res?.list || res?.data || []));
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}, [activeCluster]);
|
}, [activeCluster]);
|
||||||
@@ -46,7 +47,7 @@ export default function PodMetricsPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const ns = selectedNs === 'all' ? undefined : selectedNs;
|
const ns = selectedNs === 'all' ? undefined : selectedNs;
|
||||||
const res: Record<string, any> = await kubernetesApi.getPodMetrics(activeCluster, ns);
|
const res: JsonData = await kubernetesApi.getPodMetrics(activeCluster, ns);
|
||||||
setMetrics(Array.isArray(res) ? res : (res?.list || []));
|
setMetrics(Array.isArray(res) ? res : (res?.list || []));
|
||||||
} catch { setMetrics([]); } finally { setLoading(false); }
|
} catch { setMetrics([]); } finally { setLoading(false); }
|
||||||
}, [activeCluster, selectedNs]);
|
}, [activeCluster, selectedNs]);
|
||||||
@@ -55,8 +56,8 @@ export default function PodMetricsPage() {
|
|||||||
|
|
||||||
// Flatten for table
|
// Flatten for table
|
||||||
const rows = useMemo(() =>
|
const rows = useMemo(() =>
|
||||||
metrics.flatMap(( pm: any) =>
|
metrics.flatMap((pm: JsonData) =>
|
||||||
(pm.containers || []).map(( c: any) => ({
|
(pm.containers || []).map((c: JsonData) => ({
|
||||||
pod: pm.name,
|
pod: pm.name,
|
||||||
namespace: pm.namespace,
|
namespace: pm.namespace,
|
||||||
container: c.name,
|
container: c.name,
|
||||||
@@ -91,7 +92,7 @@ export default function PodMetricsPage() {
|
|||||||
<Select value={activeCluster} onValueChange={v => { setActiveCluster(v); navigate(`/kubernetes/${v}/metrics/pods`); }}>
|
<Select value={activeCluster} onValueChange={v => { setActiveCluster(v); navigate(`/kubernetes/${v}/metrics/pods`); }}>
|
||||||
<SelectTrigger className="w-44 h-8 text-xs bg-slate-800 border-slate-700"><SelectValue /></SelectTrigger>
|
<SelectTrigger className="w-44 h-8 text-xs bg-slate-800 border-slate-700"><SelectValue /></SelectTrigger>
|
||||||
<SelectContent className="bg-slate-800 border-slate-700">
|
<SelectContent className="bg-slate-800 border-slate-700">
|
||||||
{clusters.map(( c: any) => <SelectItem key={c.id} value={c.code || String(c.id)} className="text-xs">{c.name || c.code}</SelectItem>)}
|
{clusters.map((c: JsonData) => <SelectItem key={c.id} value={c.code || String(c.id)} className="text-xs">{c.name || c.code}</SelectItem>)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Select value={selectedNs} onValueChange={setSelectedNs}>
|
<Select value={selectedNs} onValueChange={setSelectedNs}>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { YamlViewer } from './YamlViewer';
|
|||||||
import { PodExecTerminal } from './PodExecTerminal';
|
import { PodExecTerminal } from './PodExecTerminal';
|
||||||
import { PodFileBrowser } from './PodFileBrowser';
|
import { PodFileBrowser } from './PodFileBrowser';
|
||||||
import { formatK8sTime, statusColor, STATUS_COLORS } from './k8s-yaml-utils';
|
import { formatK8sTime, statusColor, STATUS_COLORS } from './k8s-yaml-utils';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
type WorkloadKind = 'StatefulSet' | 'DaemonSet';
|
type WorkloadKind = 'StatefulSet' | 'DaemonSet';
|
||||||
type ViewMode = 'view' | 'yaml';
|
type ViewMode = 'view' | 'yaml';
|
||||||
@@ -67,14 +68,14 @@ export default function WorkloadViewPage() {
|
|||||||
const kind: WorkloadKind = location.pathname.includes('/DaemonSet/') ? 'DaemonSet' : 'StatefulSet';
|
const kind: WorkloadKind = location.pathname.includes('/DaemonSet/') ? 'DaemonSet' : 'StatefulSet';
|
||||||
const mode: ViewMode = params.mode === 'yaml' ? 'yaml' : 'view';
|
const mode: ViewMode = params.mode === 'yaml' ? 'yaml' : 'view';
|
||||||
|
|
||||||
const [manifest, setManifest] = useState<any>(null);
|
const [manifest, setManifest] = useState<JsonData>(null);
|
||||||
const [pods, setPods] = useState<any[]>([]);
|
const [pods, setPods] = useState<JsonData[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [scaleModal, setScaleModal] = useState(false);
|
const [scaleModal, setScaleModal] = useState(false);
|
||||||
const [scaleValue, setScaleValue] = useState(1);
|
const [scaleValue, setScaleValue] = useState(1);
|
||||||
const [actionLoading, setActionLoading] = useState(false);
|
const [actionLoading, setActionLoading] = useState(false);
|
||||||
const [terminalPod, setTerminalPod] = useState<any>(null);
|
const [terminalPod, setTerminalPod] = useState<JsonData>(null);
|
||||||
const [fileBrowserPod, setFileBrowserPod] = useState<any>(null);
|
const [fileBrowserPod, setFileBrowserPod] = useState<JsonData>(null);
|
||||||
const [yamlModal, setYamlModal] = useState(false);
|
const [yamlModal, setYamlModal] = useState(false);
|
||||||
|
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
@@ -95,7 +96,7 @@ export default function WorkloadViewPage() {
|
|||||||
|
|
||||||
// Filter pods by name prefix and namespace
|
// Filter pods by name prefix and namespace
|
||||||
const podList = Array.isArray(podsRes) ? podsRes : (podsRes?.list || podsRes?.data || []);
|
const podList = Array.isArray(podsRes) ? podsRes : (podsRes?.list || podsRes?.data || []);
|
||||||
const filtered = podList.filter(( p: any) => {
|
const filtered = podList.filter((p: JsonData) => {
|
||||||
const podName = p.name || p.metadata?.name || '';
|
const podName = p.name || p.metadata?.name || '';
|
||||||
const podNs = p.namespace || p.namespace_name || '';
|
const podNs = p.namespace || p.namespace_name || '';
|
||||||
return podName.startsWith(name) && (!podNs || podNs === namespace);
|
return podName.startsWith(name) && (!podNs || podNs === namespace);
|
||||||
@@ -384,7 +385,7 @@ export default function WorkloadViewPage() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{vcts.map(( vct: any, idx: number) => {
|
{vcts.map((vct: JsonData, idx: number) => {
|
||||||
const accessModes = (vct.spec?.accessModes || []).join(', ');
|
const accessModes = (vct.spec?.accessModes || []).join(', ');
|
||||||
const storageClass = vct.spec?.storageClassName || vct.spec?.storageClassName || '-';
|
const storageClass = vct.spec?.storageClassName || vct.spec?.storageClassName || '-';
|
||||||
const size = vct.spec?.resources?.requests?.storage || '-';
|
const size = vct.spec?.resources?.requests?.storage || '-';
|
||||||
@@ -415,7 +416,7 @@ export default function WorkloadViewPage() {
|
|||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<p className="text-[11px] text-slate-500 mb-2 uppercase tracking-wide">Init Containers</p>
|
<p className="text-[11px] text-slate-500 mb-2 uppercase tracking-wide">Init Containers</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{initContainers.map(( c: any, idx: number) => (
|
{initContainers.map((c: JsonData, idx: number) => (
|
||||||
<ContainerInfo key={`init-${idx}`} c={c} />
|
<ContainerInfo key={`init-${idx}`} c={c} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -426,7 +427,7 @@ export default function WorkloadViewPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<p className="text-[11px] text-slate-500 mb-1 uppercase tracking-wide">Containers</p>
|
<p className="text-[11px] text-slate-500 mb-1 uppercase tracking-wide">Containers</p>
|
||||||
{containers.map(( c: any, idx: number) => (
|
{containers.map((c: JsonData, idx: number) => (
|
||||||
<ContainerInfo key={idx} c={c} />
|
<ContainerInfo key={idx} c={c} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -456,7 +457,7 @@ export default function WorkloadViewPage() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{pods.length === 0 ? (
|
{pods.length === 0 ? (
|
||||||
<TableRow><TableCell colSpan={6} className="text-center text-slate-500 py-8">暂无 Pod</TableCell></TableRow>
|
<TableRow><TableCell colSpan={6} className="text-center text-slate-500 py-8">暂无 Pod</TableCell></TableRow>
|
||||||
) : pods.map(( pod: any) => (
|
) : pods.map((pod: JsonData) => (
|
||||||
<TableRow key={pod.name} className="border-slate-800 hover:bg-slate-800/30">
|
<TableRow key={pod.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||||
<TableCell className="font-medium text-slate-200">{pod.name}</TableCell>
|
<TableCell className="font-medium text-slate-200">{pod.name}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -531,7 +532,7 @@ export default function WorkloadViewPage() {
|
|||||||
clusterId={clusterCode}
|
clusterId={clusterCode}
|
||||||
namespace={namespace}
|
namespace={namespace}
|
||||||
podName={terminalPod.name}
|
podName={terminalPod.name}
|
||||||
containers={terminalPod.containers?.map(( c: any) => c.name).filter(Boolean) || []}
|
containers={terminalPod.containers?.map((c: JsonData) => c.name).filter(Boolean) || []}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -543,7 +544,7 @@ export default function WorkloadViewPage() {
|
|||||||
clusterCode={clusterCode}
|
clusterCode={clusterCode}
|
||||||
namespace={namespace}
|
namespace={namespace}
|
||||||
podName={fileBrowserPod.name}
|
podName={fileBrowserPod.name}
|
||||||
containers={fileBrowserPod.containers?.map(( c: any) => c.name).filter(Boolean) || []}
|
containers={fileBrowserPod.containers?.map((c: JsonData) => c.name).filter(Boolean) || []}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -551,7 +552,7 @@ export default function WorkloadViewPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Container info sub-component
|
// Container info sub-component
|
||||||
function ContainerInfo({ c }: { c: any}) {
|
function ContainerInfo({ c }: { c: JsonData}) {
|
||||||
return (
|
return (
|
||||||
<div className="border border-slate-800 rounded-lg p-3 bg-slate-950/30">
|
<div className="border border-slate-800 rounded-lg p-3 bg-slate-950/30">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
@@ -570,7 +571,7 @@ function ContainerInfo({ c }: { c: any}) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-slate-400">
|
<div className="text-slate-400">
|
||||||
端口: <span className="text-slate-200 font-mono">
|
端口: <span className="text-slate-200 font-mono">
|
||||||
{(c.ports || []).map(( p: any) => p.containerPort || p.port).join(', ') || '-'}
|
{(c.ports || []).map((p: JsonData) => p.containerPort || p.port).join(', ') || '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ModalOverlay } from '@/components/ui/modal-overlay';
|
|||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { kubernetesApi } from '@/api';
|
import { kubernetesApi } from '@/api';
|
||||||
import { YamlCodeMirror } from './YamlCodeMirror';
|
import { YamlCodeMirror } from './YamlCodeMirror';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
interface YamlImportDialogProps {
|
interface YamlImportDialogProps {
|
||||||
clusterCode: string;
|
clusterCode: string;
|
||||||
@@ -30,7 +31,7 @@ export function YamlImportDialog({ clusterCode, onClose, onApplied }: YamlImport
|
|||||||
onApplied?.();
|
onApplied?.();
|
||||||
onClose();
|
onClose();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: '导入失败', description: (e as Record<string, any>).message, variant: 'destructive' });
|
toast({ title: '导入失败', description: (e as JsonData).message, variant: 'destructive' });
|
||||||
} finally { setApplying(false); }
|
} finally { setApplying(false); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { getYamlResourceConfig, yamlDownloadFilename } from './k8s-yaml-resource
|
|||||||
import { YamlEditorToolbar } from './YamlEditorToolbar';
|
import { YamlEditorToolbar } from './YamlEditorToolbar';
|
||||||
import { YamlCodeMirror } from './YamlCodeMirror';
|
import { YamlCodeMirror } from './YamlCodeMirror';
|
||||||
import { downloadYamlFile } from '@/utils/export';
|
import { downloadYamlFile } from '@/utils/export';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
interface YamlViewerProps {
|
interface YamlViewerProps {
|
||||||
clusterCode: string;
|
clusterCode: string;
|
||||||
@@ -72,7 +73,7 @@ export function YamlViewer({
|
|||||||
setRawYaml(text);
|
setRawYaml(text);
|
||||||
await applyStripped(text);
|
await applyStripped(text);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: '获取YAML失败', description: (e as Record<string, any>).message, variant: 'destructive' });
|
toast({ title: '获取YAML失败', description: (e as JsonData).message, variant: 'destructive' });
|
||||||
const err = `# Error: ${(e as Error).message}`;
|
const err = `# Error: ${(e as Error).message}`;
|
||||||
setRawYaml(err);
|
setRawYaml(err);
|
||||||
setDisplayYaml(err);
|
setDisplayYaml(err);
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ import {
|
|||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { TabsContent } from '@/components/ui/tabs';
|
import { TabsContent } from '@/components/ui/tabs';
|
||||||
import { CreateHPADialog } from './CreateHPADialog';
|
import { CreateHPADialog } from './CreateHPADialog';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export interface DeploymentHpaTabProps {
|
export interface DeploymentHpaTabProps {
|
||||||
hpas: any[];
|
hpas: JsonData[];
|
||||||
clusterCode: string;
|
clusterCode: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
deploymentName: string;
|
deploymentName: string;
|
||||||
onCreated: () => void;
|
onCreated: () => void;
|
||||||
onDeleteHpa: (hpa: any) => void;
|
onDeleteHpa: (hpa: JsonData) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DeploymentHpaTab({
|
export function DeploymentHpaTab({
|
||||||
@@ -58,7 +59,7 @@ export function DeploymentHpaTab({
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{hpas.map((hpa: any) => {
|
{hpas.map((hpa: JsonData) => {
|
||||||
const cpu = hpa.cpu_percent || 0;
|
const cpu = hpa.cpu_percent || 0;
|
||||||
const mem = hpa.mem_percent || 0;
|
const mem = hpa.mem_percent || 0;
|
||||||
const metricsParts = [];
|
const metricsParts = [];
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ import {
|
|||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { TabsContent } from '@/components/ui/tabs';
|
import { TabsContent } from '@/components/ui/tabs';
|
||||||
import { extractContainers } from './helpers';
|
import { extractContainers } from './helpers';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export interface DeploymentLogsTabProps {
|
export interface DeploymentLogsTabProps {
|
||||||
pods: any[];
|
pods: JsonData[];
|
||||||
logTail: number;
|
logTail: number;
|
||||||
onLogTailChange: (value: number) => void;
|
onLogTailChange: (value: number) => void;
|
||||||
logsTabPod: string;
|
logsTabPod: string;
|
||||||
@@ -61,7 +62,7 @@ export function DeploymentLogsTab({
|
|||||||
<SelectValue placeholder="选择 Pod…" />
|
<SelectValue placeholder="选择 Pod…" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{pods.map((p: any) => (
|
{pods.map((p: JsonData) => (
|
||||||
<SelectItem key={p.name} value={p.name}>{p.name}</SelectItem>
|
<SelectItem key={p.name} value={p.name}>{p.name}</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -71,7 +72,7 @@ export function DeploymentLogsTab({
|
|||||||
className="bg-blue-600 hover:bg-blue-700 h-8"
|
className="bg-blue-600 hover:bg-blue-700 h-8"
|
||||||
disabled={!logsTabPod}
|
disabled={!logsTabPod}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const pod = pods.find((p: any) => p.name === logsTabPod);
|
const pod = pods.find((p: JsonData) => p.name === logsTabPod);
|
||||||
onOpenLogsViewer(logsTabPod, pod ? extractContainers(pod) : []);
|
onOpenLogsViewer(logsTabPod, pod ? extractContainers(pod) : []);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { CreateHPADialog } from './CreateHPADialog';
|
import { CreateHPADialog } from './CreateHPADialog';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export interface DeploymentOverviewProps {
|
export interface DeploymentOverviewProps {
|
||||||
deploymentName: string;
|
deploymentName: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
deployment: any;
|
deployment?: JsonData;
|
||||||
hpas: any[];
|
hpas: JsonData[];
|
||||||
clusterCode: string;
|
clusterCode: string;
|
||||||
ready: number;
|
ready: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import {
|
|||||||
import { TabsContent } from '@/components/ui/tabs';
|
import { TabsContent } from '@/components/ui/tabs';
|
||||||
import { extractContainers } from './helpers';
|
import { extractContainers } from './helpers';
|
||||||
import type { PodWithContainers } from './types';
|
import type { PodWithContainers } from './types';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export interface DeploymentPodsTabProps {
|
export interface DeploymentPodsTabProps {
|
||||||
pods: any[];
|
pods: JsonData[];
|
||||||
onOpenLogsViewer: (pod: PodWithContainers) => void;
|
onOpenLogsViewer: (pod: PodWithContainers) => void;
|
||||||
onOpenExec: (pod: PodWithContainers) => void;
|
onOpenExec: (pod: PodWithContainers) => void;
|
||||||
onOpenFileBrowser: (pod: PodWithContainers) => void;
|
onOpenFileBrowser: (pod: PodWithContainers) => void;
|
||||||
@@ -42,7 +43,7 @@ export function DeploymentPodsTab({ pods, onOpenLogsViewer, onOpenExec, onOpenFi
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
pods.map((pod: any) => (
|
pods.map((pod: JsonData) => (
|
||||||
<TableRow key={pod.name} className="border-slate-800 hover:bg-slate-800/30">
|
<TableRow key={pod.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||||
<TableCell className="font-medium text-slate-200">{pod.name}</TableCell>
|
<TableCell className="font-medium text-slate-200">{pod.name}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import type { JsonData } from "@/types/common";
|
||||||
// helpers.ts — DeploymentDetail 工具函数 // Logan.Li
|
// helpers.ts — DeploymentDetail 工具函数 // Logan.Li
|
||||||
|
|
||||||
/** 从 Pod 对象提取容器名列表(兼容多种字段) */
|
/** 从 Pod 对象提取容器名列表(兼容多种字段) */
|
||||||
export function extractContainers(pod: any): string[] {
|
export function extractContainers(pod: JsonData): string[] {
|
||||||
const c = pod?.containers;
|
const c = pod?.containers;
|
||||||
if (Array.isArray(c)) return c.map((x: any) => typeof x === 'string' ? x : (x?.name || x?.Name)).filter(Boolean);
|
if (Array.isArray(c)) return c.map((x: JsonData) => typeof x === 'string' ? x : (x?.name || x?.Name)).filter(Boolean);
|
||||||
if (Array.isArray(pod?.container_statuses)) return pod.container_statuses.map((x: any) => x?.name).filter(Boolean);
|
if (Array.isArray(pod?.container_statuses)) return pod.container_statuses.map((x: JsonData) => x?.name).filter(Boolean);
|
||||||
if (Array.isArray(pod?.containerNames)) return pod.containerNames.filter(Boolean);
|
if (Array.isArray(pod?.containerNames)) return pod.containerNames.filter(Boolean);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,11 @@
|
|||||||
import type {
|
import type {
|
||||||
ContainerForm, ProbeDef, LifecycleHandler,
|
ContainerForm, ProbeDef, LifecycleHandler,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
// K8s container spec → form conversion // Logan.Li
|
// K8s container spec → form conversion // Logan.Li
|
||||||
export function toContainerForm(c: any): ContainerForm {
|
export function toContainerForm(c: JsonData): ContainerForm {
|
||||||
const probe = (p: any): ProbeDef | null => {
|
const probe = (p: JsonData): ProbeDef | null => {
|
||||||
if (!p) return null;
|
if (!p) return null;
|
||||||
let type: ProbeDef['type'] = 'none';
|
let type: ProbeDef['type'] = 'none';
|
||||||
if (p.httpGet) type = 'httpGet';
|
if (p.httpGet) type = 'httpGet';
|
||||||
@@ -26,7 +27,7 @@ export function toContainerForm(c: any): ContainerForm {
|
|||||||
failureThreshold: p.failureThreshold,
|
failureThreshold: p.failureThreshold,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const lh = (h: any): LifecycleHandler => {
|
const lh = (h: JsonData): LifecycleHandler => {
|
||||||
if (!h) return { type: 'none' };
|
if (!h) return { type: 'none' };
|
||||||
let type: LifecycleHandler['type'] = 'none';
|
let type: LifecycleHandler['type'] = 'none';
|
||||||
if (h.exec) type = 'exec';
|
if (h.exec) type = 'exec';
|
||||||
@@ -48,8 +49,8 @@ export function toContainerForm(c: any): ContainerForm {
|
|||||||
command: Array.isArray(c.command) ? c.command : [],
|
command: Array.isArray(c.command) ? c.command : [],
|
||||||
args: Array.isArray(c.args) ? c.args : [],
|
args: Array.isArray(c.args) ? c.args : [],
|
||||||
workingDir: c.workingDir || '',
|
workingDir: c.workingDir || '',
|
||||||
env: (Array.isArray(c.env) ? c.env : []).map((e: any) => ({ name: e.name || '', value: e.value ?? '' })),
|
env: (Array.isArray(c.env) ? c.env : []).map((e: JsonData) => ({ name: e.name || '', value: e.value ?? '' })),
|
||||||
ports: (Array.isArray(c.ports) ? c.ports : []).map((p: any) => ({ name: p.name || '', containerPort: p.containerPort ?? '', protocol: p.protocol || 'TCP' })),
|
ports: (Array.isArray(c.ports) ? c.ports : []).map((p: JsonData) => ({ name: p.name || '', containerPort: p.containerPort ?? '', protocol: p.protocol || 'TCP' })),
|
||||||
resources: {
|
resources: {
|
||||||
requests: c.resources?.requests || {},
|
requests: c.resources?.requests || {},
|
||||||
limits: c.resources?.limits || {},
|
limits: c.resources?.limits || {},
|
||||||
@@ -71,7 +72,7 @@ export function toContainerForm(c: any): ContainerForm {
|
|||||||
capabilitiesAdd: sc.capabilities?.add || [],
|
capabilitiesAdd: sc.capabilities?.add || [],
|
||||||
capabilitiesDrop: sc.capabilities?.drop || [],
|
capabilitiesDrop: sc.capabilities?.drop || [],
|
||||||
} : null,
|
} : null,
|
||||||
volumeMounts: (Array.isArray(c.volumeMounts) ? c.volumeMounts : []).map((m: any) => ({
|
volumeMounts: (Array.isArray(c.volumeMounts) ? c.volumeMounts : []).map((m: JsonData) => ({
|
||||||
name: m.name || '', mountPath: m.mountPath || '', subPath: m.subPath || '', readOnly: m.readOnly || false,
|
name: m.name || '', mountPath: m.mountPath || '', subPath: m.subPath || '', readOnly: m.readOnly || false,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
@@ -88,9 +89,9 @@ export function emptyContainer(name = ''): ContainerForm {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function probeToK8s(p: ProbeDef | null | undefined): any {
|
export function probeToK8s(p: ProbeDef | null | undefined): JsonData {
|
||||||
if (!p || p.type === 'none' || !p.type) return undefined;
|
if (!p || p.type === 'none' || !p.type) return undefined;
|
||||||
const out: Record<string, any> = {};
|
const out: JsonData = {};
|
||||||
if (p.type === 'httpGet') {
|
if (p.type === 'httpGet') {
|
||||||
out.httpGet = {};
|
out.httpGet = {};
|
||||||
if (p.httpPath) out.httpGet.path = p.httpPath;
|
if (p.httpPath) out.httpGet.path = p.httpPath;
|
||||||
@@ -111,28 +112,28 @@ export function probeToK8s(p: ProbeDef | null | undefined): any {
|
|||||||
return Object.keys(out).length > 0 ? out : undefined;
|
return Object.keys(out).length > 0 ? out : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handlerToK8s(h: LifecycleHandler | undefined): any {
|
export function handlerToK8s(h: LifecycleHandler | undefined): JsonData {
|
||||||
if (!h || h.type === 'none' || !h.type) return undefined;
|
if (!h || h.type === 'none' || !h.type) return undefined;
|
||||||
if (h.type === 'exec') {
|
if (h.type === 'exec') {
|
||||||
const cmds = (h.execCommand || '').split('\n').map(s => s.trim()).filter(Boolean);
|
const cmds = (h.execCommand || '').split('\n').map(s => s.trim()).filter(Boolean);
|
||||||
return cmds.length ? { exec: { command: cmds } } : undefined;
|
return cmds.length ? { exec: { command: cmds } } : undefined;
|
||||||
}
|
}
|
||||||
if (h.type === 'httpGet') {
|
if (h.type === 'httpGet') {
|
||||||
const o: Record<string, any> = { httpGet: {} };
|
const o: JsonData = { httpGet: {} };
|
||||||
if (h.httpPath) o.httpGet.path = h.httpPath;
|
if (h.httpPath) o.httpGet.path = h.httpPath;
|
||||||
if (h.httpPort !== '' && h.httpPort != null) o.httpGet.port = Number(h.httpPort);
|
if (h.httpPort !== '' && h.httpPort != null) o.httpGet.port = Number(h.httpPort);
|
||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
if (h.type === 'tcpSocket') {
|
if (h.type === 'tcpSocket') {
|
||||||
const o: Record<string, any> = { tcpSocket: {} };
|
const o: JsonData = { tcpSocket: {} };
|
||||||
if (h.tcpPort !== '' && h.tcpPort != null) o.tcpSocket.port = Number(h.tcpPort);
|
if (h.tcpPort !== '' && h.tcpPort != null) o.tcpSocket.port = Number(h.tcpPort);
|
||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function containerToK8s(c: ContainerForm): any {
|
export function containerToK8s(c: ContainerForm): JsonData {
|
||||||
const out: Record<string, any> = { name: c.name };
|
const out: JsonData = { name: c.name };
|
||||||
if (c.image) out.image = c.image;
|
if (c.image) out.image = c.image;
|
||||||
if (c.imagePullPolicy) out.imagePullPolicy = c.imagePullPolicy;
|
if (c.imagePullPolicy) out.imagePullPolicy = c.imagePullPolicy;
|
||||||
if (c.workingDir) out.workingDir = c.workingDir;
|
if (c.workingDir) out.workingDir = c.workingDir;
|
||||||
@@ -140,14 +141,14 @@ export function containerToK8s(c: ContainerForm): any {
|
|||||||
if (c.args.length > 0) out.args = c.args;
|
if (c.args.length > 0) out.args = c.args;
|
||||||
if (c.env.length > 0) {
|
if (c.env.length > 0) {
|
||||||
out.env = c.env.filter(e => e.name.trim()).map(e => {
|
out.env = c.env.filter(e => e.name.trim()).map(e => {
|
||||||
const o: Record<string, any> = { name: e.name };
|
const o: JsonData = { name: e.name };
|
||||||
if (e.value != null) o.value = e.value;
|
if (e.value != null) o.value = e.value;
|
||||||
return o;
|
return o;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (c.ports.length > 0) {
|
if (c.ports.length > 0) {
|
||||||
out.ports = c.ports.filter(p => p.containerPort !== '').map(p => {
|
out.ports = c.ports.filter(p => p.containerPort !== '').map(p => {
|
||||||
const o: Record<string, any> = { containerPort: typeof p.containerPort === 'number' ? p.containerPort : Number(p.containerPort) };
|
const o: JsonData = { containerPort: typeof p.containerPort === 'number' ? p.containerPort : Number(p.containerPort) };
|
||||||
if (p.name) o.name = p.name;
|
if (p.name) o.name = p.name;
|
||||||
if (p.protocol) o.protocol = p.protocol;
|
if (p.protocol) o.protocol = p.protocol;
|
||||||
return o;
|
return o;
|
||||||
@@ -174,14 +175,14 @@ export function containerToK8s(c: ContainerForm): any {
|
|||||||
const rp = probeToK8s(c.readinessProbe); if (rp) out.readinessProbe = rp;
|
const rp = probeToK8s(c.readinessProbe); if (rp) out.readinessProbe = rp;
|
||||||
const sp = probeToK8s(c.startupProbe); if (sp) out.startupProbe = sp;
|
const sp = probeToK8s(c.startupProbe); if (sp) out.startupProbe = sp;
|
||||||
if (c.lifecycle) {
|
if (c.lifecycle) {
|
||||||
const lc: Record<string, any> = {};
|
const lc: JsonData = {};
|
||||||
const ps = handlerToK8s(c.lifecycle.postStart); if (ps) lc.postStart = ps;
|
const ps = handlerToK8s(c.lifecycle.postStart); if (ps) lc.postStart = ps;
|
||||||
const pst = handlerToK8s(c.lifecycle.preStop); if (pst) lc.preStop = pst;
|
const pst = handlerToK8s(c.lifecycle.preStop); if (pst) lc.preStop = pst;
|
||||||
if (Object.keys(lc).length > 0) out.lifecycle = lc;
|
if (Object.keys(lc).length > 0) out.lifecycle = lc;
|
||||||
}
|
}
|
||||||
if (c.securityContext) {
|
if (c.securityContext) {
|
||||||
const s = c.securityContext;
|
const s = c.securityContext;
|
||||||
const sc: Record<string, any> = {};
|
const sc: JsonData = {};
|
||||||
if (s.runAsUser !== '' && s.runAsUser != null) sc.runAsUser = Number(s.runAsUser);
|
if (s.runAsUser !== '' && s.runAsUser != null) sc.runAsUser = Number(s.runAsUser);
|
||||||
if (s.runAsGroup !== '' && s.runAsGroup != null) sc.runAsGroup = Number(s.runAsGroup);
|
if (s.runAsGroup !== '' && s.runAsGroup != null) sc.runAsGroup = Number(s.runAsGroup);
|
||||||
if (s.runAsNonRoot != null) sc.runAsNonRoot = s.runAsNonRoot;
|
if (s.runAsNonRoot != null) sc.runAsNonRoot = s.runAsNonRoot;
|
||||||
@@ -199,7 +200,7 @@ export function containerToK8s(c: ContainerForm): any {
|
|||||||
}
|
}
|
||||||
if (c.volumeMounts.length > 0) {
|
if (c.volumeMounts.length > 0) {
|
||||||
out.volumeMounts = c.volumeMounts.filter(m => m.name.trim() && m.mountPath.trim()).map(m => {
|
out.volumeMounts = c.volumeMounts.filter(m => m.name.trim() && m.mountPath.trim()).map(m => {
|
||||||
const o: Record<string, any> = { name: m.name, mountPath: m.mountPath };
|
const o: JsonData = { name: m.name, mountPath: m.mountPath };
|
||||||
if (m.subPath) o.subPath = m.subPath;
|
if (m.subPath) o.subPath = m.subPath;
|
||||||
if (m.readOnly) o.readOnly = true;
|
if (m.readOnly) o.readOnly = true;
|
||||||
return o;
|
return o;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { JsonData } from "@/types/common";
|
||||||
// deployment-edit/types.ts — 容器编辑器类型定义
|
// deployment-edit/types.ts — 容器编辑器类型定义
|
||||||
// 作者: Logan <admin@attacker.club>
|
// 作者: Logan <admin@attacker.club>
|
||||||
|
|
||||||
@@ -64,8 +65,8 @@ export interface DeploymentEditContainersProps {
|
|||||||
clusterCode: string;
|
clusterCode: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
deploymentName: string;
|
deploymentName: string;
|
||||||
manifest: any | null;
|
manifest: JsonData | null;
|
||||||
onSaved: (updated?: any) => void;
|
onSaved: (updated?: JsonData) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import {
|
|||||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { Activity, Clock, Loader2 } from 'lucide-react';
|
import { Activity, Clock, Loader2 } from 'lucide-react';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: any[]; cronHpas: any[]; loading: string; replicas: number }) {
|
export function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: JsonData[]; cronHpas: JsonData[]; loading: string; replicas: number }) {
|
||||||
if (loading === 'scale') {
|
if (loading === 'scale') {
|
||||||
return <div className="flex items-center justify-center py-12"><Loader2 className="h-6 w-6 animate-spin text-[var(--dg-text-muted)]" /></div>;
|
return <div className="flex items-center justify-center py-12"><Loader2 className="h-6 w-6 animate-spin text-[var(--dg-text-muted)]" /></div>;
|
||||||
}
|
}
|
||||||
@@ -42,20 +43,20 @@ export function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: any
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{hpas.map((hpa: any, i: number) => {
|
{hpas.map((hpa: JsonData, i: number) => {
|
||||||
const minR = hpa.spec?.minReplicas ?? hpa.minReplicas ?? 1;
|
const minR = hpa.spec?.minReplicas ?? hpa.minReplicas ?? 1;
|
||||||
const maxR = hpa.spec?.maxReplicas ?? hpa.maxReplicas ?? 10;
|
const maxR = hpa.spec?.maxReplicas ?? hpa.maxReplicas ?? 10;
|
||||||
const metrics = hpa.spec?.metrics || hpa.metrics || [];
|
const metrics = hpa.spec?.metrics || hpa.metrics || [];
|
||||||
const curR = hpa.status?.currentReplicas ?? hpa.currentReplicas ?? 0;
|
const curR = hpa.status?.currentReplicas ?? hpa.currentReplicas ?? 0;
|
||||||
const conditions = hpa.status?.conditions || hpa.conditions || [];
|
const conditions = hpa.status?.conditions || hpa.conditions || [];
|
||||||
const ableToScale = conditions.find((c: any) => c.type === 'AbleToScale');
|
const ableToScale = conditions.find((c: JsonData) => c.type === 'AbleToScale');
|
||||||
const ok = ableToScale?.status === 'True';
|
const ok = ableToScale?.status === 'True';
|
||||||
return (
|
return (
|
||||||
<TableRow key={hpa.name || i} className="border-[var(--dg-border)]">
|
<TableRow key={hpa.name || i} className="border-[var(--dg-border)]">
|
||||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{hpa.name || hpa.metadata?.name}</TableCell>
|
<TableCell className="font-medium text-[var(--dg-text-primary)]">{hpa.name || hpa.metadata?.name}</TableCell>
|
||||||
<TableCell className="text-sm text-[var(--dg-text-secondary)] font-mono">{minR} → {maxR}</TableCell>
|
<TableCell className="text-sm text-[var(--dg-text-secondary)] font-mono">{minR} → {maxR}</TableCell>
|
||||||
<TableCell className="text-xs text-[var(--dg-text-muted)]">
|
<TableCell className="text-xs text-[var(--dg-text-muted)]">
|
||||||
{metrics.map((m: any, j: number) => {
|
{metrics.map((m: JsonData, j: number) => {
|
||||||
const resource = m.resource || m.object?.metric?.name || '';
|
const resource = m.resource || m.object?.metric?.name || '';
|
||||||
const target = m.resource?.target?.averageUtilization || m.resource?.target?.averageValue || m.resource?.target?.type || '-';
|
const target = m.resource?.target?.averageUtilization || m.resource?.target?.averageValue || m.resource?.target?.type || '-';
|
||||||
return <div key={j}>{resource?.name || resource}: {target}{m.resource?.target?.averageUtilization ? '%' : ''}</div>;
|
return <div key={j}>{resource?.name || resource}: {target}{m.resource?.target?.averageUtilization ? '%' : ''}</div>;
|
||||||
@@ -98,16 +99,16 @@ export function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: any
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{cronHpas.map((cron: any, i: number) => {
|
{cronHpas.map((cron: JsonData, i: number) => {
|
||||||
const jobs = cron.spec?.jobs || cron.jobs || [];
|
const jobs = cron.spec?.jobs || cron.jobs || [];
|
||||||
const tz = cron.spec?.timezone || 'UTC';
|
const tz = cron.spec?.timezone || 'UTC';
|
||||||
return (
|
return (
|
||||||
<TableRow key={cron.name || i} className="border-[var(--dg-border)]">
|
<TableRow key={cron.name || i} className="border-[var(--dg-border)]">
|
||||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{cron.name || cron.metadata?.name}</TableCell>
|
<TableCell className="font-medium text-[var(--dg-text-primary)]">{cron.name || cron.metadata?.name}</TableCell>
|
||||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{
|
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{
|
||||||
jobs.map((j: any) => j.targetSize || j.targetReplicas || '?').join(', ')
|
jobs.map((j: JsonData) => j.targetSize || j.targetReplicas || '?').join(', ')
|
||||||
}</TableCell>
|
}</TableCell>
|
||||||
<TableCell className="text-xs font-mono text-[var(--dg-text-muted)]">{jobs.map((j: any) => j.schedule || '?').join(', ')}</TableCell>
|
<TableCell className="text-xs font-mono text-[var(--dg-text-muted)]">{jobs.map((j: JsonData) => j.schedule || '?').join(', ')}</TableCell>
|
||||||
<TableCell className="text-xs text-[var(--dg-text-muted)]">{tz}</TableCell>
|
<TableCell className="text-xs text-[var(--dg-text-muted)]">{tz}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge className="bg-[var(--dg-green)]/20 text-[var(--dg-green)]">已启用</Badge>
|
<Badge className="bg-[var(--dg-green)]/20 text-[var(--dg-green)]">已启用</Badge>
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import {
|
|||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { RotateCcw, Loader2 } from 'lucide-react';
|
import { RotateCcw, Loader2 } from 'lucide-react';
|
||||||
import { formatK8sTime } from '../k8s-yaml-utils';
|
import { formatK8sTime } from '../k8s-yaml-utils';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export function HistoryTab({ revisions, rollbackLoading, onRollback }: { revisions: any[]; rollbackLoading: number | null; onRollback: (rev: any) => void }) {
|
export function HistoryTab({ revisions, rollbackLoading, onRollback }: { revisions: JsonData[]; rollbackLoading: number | null; onRollback: (rev: JsonData) => void }) {
|
||||||
if (revisions.length === 0) {
|
if (revisions.length === 0) {
|
||||||
return <div className="text-center text-[var(--dg-text-muted)] py-12">暂无历史版本</div>;
|
return <div className="text-center text-[var(--dg-text-muted)] py-12">暂无历史版本</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { Network, ExternalLink, Loader2 } from 'lucide-react';
|
import { Network, ExternalLink, Loader2 } from 'lucide-react';
|
||||||
import type { SvcItem, IngItem } from './types';
|
import type { SvcItem, IngItem } from './types';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export function ServicesTab({ services, ingresses, loading }: { services: SvcItem[]; ingresses: IngItem[]; loading: string; namespace: string }) {
|
export function ServicesTab({ services, ingresses, loading }: { services: SvcItem[]; ingresses: IngItem[]; loading: string; namespace: string }) {
|
||||||
if (loading === 'services') {
|
if (loading === 'services') {
|
||||||
@@ -76,7 +77,7 @@ export function ServicesTab({ services, ingresses, loading }: { services: SvcIte
|
|||||||
<TableCell className="text-sm text-[var(--dg-text-muted)]">{ing.className}</TableCell>
|
<TableCell className="text-sm text-[var(--dg-text-muted)]">{ing.className}</TableCell>
|
||||||
<TableCell className="text-sm text-[var(--dg-text-muted)]">{ing.hosts.join(', ')}</TableCell>
|
<TableCell className="text-sm text-[var(--dg-text-muted)]">{ing.hosts.join(', ')}</TableCell>
|
||||||
<TableCell className="text-xs text-[var(--dg-text-muted)]">
|
<TableCell className="text-xs text-[var(--dg-text-muted)]">
|
||||||
{ing.rules.flatMap((r: any) => (r.http?.paths || r.paths || []).map((p: any) => p.path || '/')).join(', ')}
|
{ing.rules.flatMap((r: JsonData) => (r.http?.paths || r.paths || []).map((p: JsonData) => p.path || '/')).join(', ')}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
formatK8sTime, formatDuration, elapsedSeconds, STATUS_COLORS,
|
formatK8sTime, formatDuration, elapsedSeconds, STATUS_COLORS,
|
||||||
} from '../k8s-yaml-utils';
|
} from '../k8s-yaml-utils';
|
||||||
import type { PodItem } from './types';
|
import type { PodItem } from './types';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export interface PodStage {
|
export interface PodStage {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -13,15 +14,15 @@ export interface PodStage {
|
|||||||
|
|
||||||
// Pod 条件时间线提取 // Logan.Li
|
// Pod 条件时间线提取 // Logan.Li
|
||||||
export function getPodTimeline(pod: PodItem) {
|
export function getPodTimeline(pod: PodItem) {
|
||||||
const cond: Record<string, any>[] = (pod as Record<string, any>).conditions || (pod as Record<string, any>).pod_conditions || [];
|
const cond: JsonData[] = (pod as JsonData).conditions || (pod as JsonData).pod_conditions || [];
|
||||||
const find = (t: string) => cond.find((c: any) => (c.type || c.Type) === t);
|
const find = (t: string) => cond.find((c: JsonData) => (c.type || c.Type) === t);
|
||||||
const ts = (c: any) => c?.lastTransitionTime || c?.LastTransitionTime || c?.last_transition_time;
|
const ts = (c: JsonData) => c?.lastTransitionTime || c?.LastTransitionTime || c?.last_transition_time;
|
||||||
return {
|
return {
|
||||||
scheduled: ts(find('PodScheduled') || {}),
|
scheduled: ts(find('PodScheduled') || {}),
|
||||||
initialized: ts(find('Initialized') || {}),
|
initialized: ts(find('Initialized') || {}),
|
||||||
containersReady: ts(find('ContainersReady') || {}),
|
containersReady: ts(find('ContainersReady') || {}),
|
||||||
ready: ts(find('Ready') || {}),
|
ready: ts(find('Ready') || {}),
|
||||||
startTime: pod.start_time || (pod as Record<string, any>).startTime,
|
startTime: pod.start_time || (pod as JsonData).startTime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import type { JsonData } from "@/types/common";
|
||||||
// deployment-view/types.ts — Deployment 详情看板类型定义 // Logan.Li
|
// deployment-view/types.ts — Deployment 详情看板类型定义 // Logan.Li
|
||||||
|
|
||||||
export interface PodItem {
|
export interface PodItem {
|
||||||
name: string; namespace?: string; status?: string; node_name?: string; node?: string;
|
name: string; namespace?: string; status?: string; node_name?: string; node?: string;
|
||||||
ip?: string; pod_ip?: string; restart_count?: number; age?: string; created_at?: string;
|
ip?: string; pod_ip?: string; restart_count?: number; age?: string; created_at?: string;
|
||||||
start_time?: string; containers?: any[]; image?: string; images?: string[];
|
start_time?: string; containers?: JsonData[]; image?: string; images?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReplicaSetItem {
|
export interface ReplicaSetItem {
|
||||||
@@ -12,18 +13,18 @@ export interface ReplicaSetItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SvcItem {
|
export interface SvcItem {
|
||||||
name: string; type: string; clusterIP: string; ports: any[]; selector: Record<string, string>; age: string;
|
name: string; type: string; clusterIP: string; ports: JsonData[]; selector: Record<string, string>; age: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IngItem {
|
export interface IngItem {
|
||||||
name: string; className: string; hosts: string[]; rules: any[]; age: string;
|
name: string; className: string; hosts: string[]; rules: JsonData[]; age: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventItem = import('../k8s-event-utils').NormalizedEvent;
|
export type EventItem = import('../k8s-event-utils').NormalizedEvent;
|
||||||
|
|
||||||
export interface DeploymentViewProps {
|
export interface DeploymentViewProps {
|
||||||
clusterCode: string; namespace: string; deployment: any;
|
clusterCode: string; namespace: string; deployment?: JsonData;
|
||||||
pods: PodItem[]; replicaSets: ReplicaSetItem[]; revisions: any[];
|
pods: PodItem[]; replicaSets: ReplicaSetItem[]; revisions: JsonData[];
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
onShowPodLogs: (pod: PodItem) => void;
|
onShowPodLogs: (pod: PodItem) => void;
|
||||||
onShowPodTerminal: (pod: PodItem) => void;
|
onShowPodTerminal: (pod: PodItem) => void;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { JsonData } from "@/types/common";
|
||||||
// Deployment Pod 数据卷 ↔ K8s YAML 双向映射 // Logan.Li
|
// Deployment Pod 数据卷 ↔ K8s YAML 双向映射 // Logan.Li
|
||||||
|
|
||||||
export type VolumeType =
|
export type VolumeType =
|
||||||
@@ -26,7 +27,7 @@ export interface VolumeForm {
|
|||||||
secretName?: string;
|
secretName?: string;
|
||||||
secItems?: string;
|
secItems?: string;
|
||||||
/** 未识别卷类型时保留原始对象,避免保存丢失 // Logan.Li */
|
/** 未识别卷类型时保留原始对象,避免保存丢失 // Logan.Li */
|
||||||
_raw?: Record<string, any>;
|
_raw?: JsonData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MountForm {
|
export interface MountForm {
|
||||||
@@ -65,7 +66,7 @@ export function emptyVolumeEntry(defaultContainer = ''): VolumeEntry {
|
|||||||
const SYSTEM_VOLUME_PREFIXES = ['kube-api-access-', 'default-token-', 'kube-'];
|
const SYSTEM_VOLUME_PREFIXES = ['kube-api-access-', 'default-token-', 'kube-'];
|
||||||
|
|
||||||
/** 是否系统/不可编辑卷(保存时保留)// Logan.Li */
|
/** 是否系统/不可编辑卷(保存时保留)// Logan.Li */
|
||||||
export function isPassthroughVolume(v: Record<string, any>): boolean {
|
export function isPassthroughVolume(v: JsonData): boolean {
|
||||||
if (!v || typeof v !== 'object') return false;
|
if (!v || typeof v !== 'object') return false;
|
||||||
const name = String(v.name || '');
|
const name = String(v.name || '');
|
||||||
if (SYSTEM_VOLUME_PREFIXES.some((p) => name.startsWith(p))) return true;
|
if (SYSTEM_VOLUME_PREFIXES.some((p) => name.startsWith(p))) return true;
|
||||||
@@ -91,7 +92,7 @@ export function parseKeyToPathItems(text?: string): Array<{ key: string; path: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** K8s volume → 表单 // Logan.Li */
|
/** K8s volume → 表单 // Logan.Li */
|
||||||
export function volumeToForm(v: Record<string, any>): VolumeForm {
|
export function volumeToForm(v: JsonData): VolumeForm {
|
||||||
if (!v || typeof v !== 'object') return { name: '', type: 'emptyDir' };
|
if (!v || typeof v !== 'object') return { name: '', type: 'emptyDir' };
|
||||||
const name = v.name || '';
|
const name = v.name || '';
|
||||||
if (v.persistentVolumeClaim) {
|
if (v.persistentVolumeClaim) {
|
||||||
@@ -150,22 +151,22 @@ export function volumeToForm(v: Record<string, any>): VolumeForm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 表单 → K8s volume // Logan.Li */
|
/** 表单 → K8s volume // Logan.Li */
|
||||||
export function formToVolume(f: VolumeForm): Record<string, any> | null {
|
export function formToVolume(f: VolumeForm): JsonData | null {
|
||||||
if (f.type === 'custom' && f._raw) return JSON.parse(JSON.stringify(f._raw));
|
if (f.type === 'custom' && f._raw) return JSON.parse(JSON.stringify(f._raw));
|
||||||
const volName = f.name?.trim();
|
const volName = f.name?.trim();
|
||||||
if (!volName) return null;
|
if (!volName) return null;
|
||||||
|
|
||||||
const v: Record<string, any> = { name: volName };
|
const v: JsonData = { name: volName };
|
||||||
switch (f.type) {
|
switch (f.type) {
|
||||||
case 'emptyDir': {
|
case 'emptyDir': {
|
||||||
const ed: Record<string, any> = {};
|
const ed: JsonData = {};
|
||||||
if (f.medium?.trim()) ed.medium = f.medium.trim();
|
if (f.medium?.trim()) ed.medium = f.medium.trim();
|
||||||
if (f.sizeLimit?.trim()) ed.sizeLimit = f.sizeLimit.trim();
|
if (f.sizeLimit?.trim()) ed.sizeLimit = f.sizeLimit.trim();
|
||||||
v.emptyDir = ed;
|
v.emptyDir = ed;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'hostPath': {
|
case 'hostPath': {
|
||||||
const hp: Record<string, any> = { path: f.path?.trim() || '/' };
|
const hp: JsonData = { path: f.path?.trim() || '/' };
|
||||||
if (f.pathType?.trim()) hp.type = f.pathType.trim();
|
if (f.pathType?.trim()) hp.type = f.pathType.trim();
|
||||||
v.hostPath = hp;
|
v.hostPath = hp;
|
||||||
break;
|
break;
|
||||||
@@ -173,7 +174,7 @@ export function formToVolume(f: VolumeForm): Record<string, any> | null {
|
|||||||
case 'configMap': {
|
case 'configMap': {
|
||||||
const cmName = f.configMapName?.trim();
|
const cmName = f.configMapName?.trim();
|
||||||
if (!cmName) return null;
|
if (!cmName) return null;
|
||||||
const cm: Record<string, any> = { name: cmName };
|
const cm: JsonData = { name: cmName };
|
||||||
const items = parseKeyToPathItems(f.cmItems);
|
const items = parseKeyToPathItems(f.cmItems);
|
||||||
if (items) cm.items = items;
|
if (items) cm.items = items;
|
||||||
v.configMap = cm;
|
v.configMap = cm;
|
||||||
@@ -182,7 +183,7 @@ export function formToVolume(f: VolumeForm): Record<string, any> | null {
|
|||||||
case 'secret': {
|
case 'secret': {
|
||||||
const secName = f.secretName?.trim();
|
const secName = f.secretName?.trim();
|
||||||
if (!secName) return null;
|
if (!secName) return null;
|
||||||
const sec: Record<string, any> = { secretName: secName };
|
const sec: JsonData = { secretName: secName };
|
||||||
const items = parseKeyToPathItems(f.secItems);
|
const items = parseKeyToPathItems(f.secItems);
|
||||||
if (items) sec.items = items;
|
if (items) sec.items = items;
|
||||||
v.secret = sec;
|
v.secret = sec;
|
||||||
@@ -191,7 +192,7 @@ export function formToVolume(f: VolumeForm): Record<string, any> | null {
|
|||||||
case 'persistentVolumeClaim': {
|
case 'persistentVolumeClaim': {
|
||||||
const claim = f.pvcName?.trim();
|
const claim = f.pvcName?.trim();
|
||||||
if (!claim) return null;
|
if (!claim) return null;
|
||||||
const pvc: Record<string, any> = { claimName: claim };
|
const pvc: JsonData = { claimName: claim };
|
||||||
if (f.pvcReadOnly) pvc.readOnly = true;
|
if (f.pvcReadOnly) pvc.readOnly = true;
|
||||||
v.persistentVolumeClaim = pvc;
|
v.persistentVolumeClaim = pvc;
|
||||||
break;
|
break;
|
||||||
@@ -200,7 +201,7 @@ export function formToVolume(f: VolumeForm): Record<string, any> | null {
|
|||||||
const server = f.nfsServer?.trim();
|
const server = f.nfsServer?.trim();
|
||||||
const path = f.nfsPath?.trim();
|
const path = f.nfsPath?.trim();
|
||||||
if (!server || !path) return null;
|
if (!server || !path) return null;
|
||||||
const nfs: Record<string, any> = { server, path };
|
const nfs: JsonData = { server, path };
|
||||||
if (f.nfsReadOnly) nfs.readOnly = true;
|
if (f.nfsReadOnly) nfs.readOnly = true;
|
||||||
v.nfs = nfs;
|
v.nfs = nfs;
|
||||||
break;
|
break;
|
||||||
@@ -211,7 +212,7 @@ export function formToVolume(f: VolumeForm): Record<string, any> | null {
|
|||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mountToForm(containerName: string, vm: Record<string, any>): MountForm {
|
export function mountToForm(containerName: string, vm: JsonData): MountForm {
|
||||||
return {
|
return {
|
||||||
containerName,
|
containerName,
|
||||||
volumeName: vm.name || '',
|
volumeName: vm.name || '',
|
||||||
@@ -221,9 +222,9 @@ export function mountToForm(containerName: string, vm: Record<string, any>): Mou
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formToMount(m: MountForm): Record<string, any> | null {
|
export function formToMount(m: MountForm): JsonData | null {
|
||||||
if (!m.volumeName?.trim() || !m.mountPath?.trim()) return null;
|
if (!m.volumeName?.trim() || !m.mountPath?.trim()) return null;
|
||||||
const vm: Record<string, any> = { name: m.volumeName.trim(), mountPath: m.mountPath.trim() };
|
const vm: JsonData = { name: m.volumeName.trim(), mountPath: m.mountPath.trim() };
|
||||||
if (m.subPath?.trim()) vm.subPath = m.subPath.trim();
|
if (m.subPath?.trim()) vm.subPath = m.subPath.trim();
|
||||||
if (m.readOnly) vm.readOnly = true;
|
if (m.readOnly) vm.readOnly = true;
|
||||||
return vm;
|
return vm;
|
||||||
@@ -231,7 +232,7 @@ export function formToMount(m: MountForm): Record<string, any> | null {
|
|||||||
|
|
||||||
const SYSTEM_MOUNT_PREFIXES = ['/var/run/secrets/kubernetes.io/serviceaccount'];
|
const SYSTEM_MOUNT_PREFIXES = ['/var/run/secrets/kubernetes.io/serviceaccount'];
|
||||||
|
|
||||||
export function isPassthroughMount(vm: Record<string, any>, passthroughVolumeNames: Set<string>): boolean {
|
export function isPassthroughMount(vm: JsonData, passthroughVolumeNames: Set<string>): boolean {
|
||||||
if (!vm) return false;
|
if (!vm) return false;
|
||||||
if (passthroughVolumeNames.has(vm.name)) return true;
|
if (passthroughVolumeNames.has(vm.name)) return true;
|
||||||
const mp = vm.mountPath || '';
|
const mp = vm.mountPath || '';
|
||||||
@@ -239,14 +240,14 @@ export function isPassthroughMount(vm: Record<string, any>, passthroughVolumeNam
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 从 manifest 解析卷与挂载表单 // Logan.Li */
|
/** 从 manifest 解析卷与挂载表单 // Logan.Li */
|
||||||
export function parseStorageFromManifest(manifest: Record<string, any>): {
|
export function parseStorageFromManifest(manifest: JsonData): {
|
||||||
volumes: VolumeForm[];
|
volumes: VolumeForm[];
|
||||||
mounts: MountForm[];
|
mounts: MountForm[];
|
||||||
containers: string[];
|
containers: string[];
|
||||||
passthroughVolumes: Record<string, any>[];
|
passthroughVolumes: JsonData[];
|
||||||
} {
|
} {
|
||||||
const podSpec = manifest?.spec?.template?.spec || {};
|
const podSpec = manifest?.spec?.template?.spec || {};
|
||||||
const rawVols: Record<string, any>[] = Array.isArray(podSpec.volumes) ? podSpec.volumes : [];
|
const rawVols: JsonData[] = Array.isArray(podSpec.volumes) ? podSpec.volumes : [];
|
||||||
const passthroughVolumes = rawVols.filter(isPassthroughVolume);
|
const passthroughVolumes = rawVols.filter(isPassthroughVolume);
|
||||||
const editableVols = rawVols.filter((v) => !isPassthroughVolume(v));
|
const editableVols = rawVols.filter((v) => !isPassthroughVolume(v));
|
||||||
|
|
||||||
@@ -260,7 +261,7 @@ export function parseStorageFromManifest(manifest: Record<string, any>): {
|
|||||||
const mounts: MountForm[] = [];
|
const mounts: MountForm[] = [];
|
||||||
for (const c of allContainers) {
|
for (const c of allContainers) {
|
||||||
const cName = c.name || '(unnamed)';
|
const cName = c.name || '(unnamed)';
|
||||||
const vms: Record<string, any>[] = Array.isArray(c.volumeMounts) ? c.volumeMounts : [];
|
const vms: JsonData[] = Array.isArray(c.volumeMounts) ? c.volumeMounts : [];
|
||||||
for (const vm of vms) {
|
for (const vm of vms) {
|
||||||
if (isPassthroughMount(vm, passthroughNames)) continue;
|
if (isPassthroughMount(vm, passthroughNames)) continue;
|
||||||
mounts.push(mountToForm(cName, vm));
|
mounts.push(mountToForm(cName, vm));
|
||||||
@@ -276,10 +277,10 @@ export function parseStorageFromManifest(manifest: Record<string, any>): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 解析为 Kuboard 一卷一卡条目(支持一卷多容器挂载)// Logan.Li */
|
/** 解析为 Kuboard 一卷一卡条目(支持一卷多容器挂载)// Logan.Li */
|
||||||
export function parseVolumeEntries(manifest: Record<string, any>): {
|
export function parseVolumeEntries(manifest: JsonData): {
|
||||||
entries: VolumeEntry[];
|
entries: VolumeEntry[];
|
||||||
containers: string[];
|
containers: string[];
|
||||||
passthroughVolumes: Record<string, any>[];
|
passthroughVolumes: JsonData[];
|
||||||
} {
|
} {
|
||||||
const { volumes, mounts, containers, passthroughVolumes } = parseStorageFromManifest(manifest);
|
const { volumes, mounts, containers, passthroughVolumes } = parseStorageFromManifest(manifest);
|
||||||
const defaultContainer = containers.find((c) => c !== '(unnamed)') || containers[0] || '';
|
const defaultContainer = containers.find((c) => c !== '(unnamed)') || containers[0] || '';
|
||||||
@@ -326,32 +327,32 @@ export function volumeEntriesToStorage(entries: VolumeEntry[]): {
|
|||||||
|
|
||||||
/** 将表单写回 manifest.spec.template.spec // Logan.Li */
|
/** 将表单写回 manifest.spec.template.spec // Logan.Li */
|
||||||
export function applyStorageToManifest(
|
export function applyStorageToManifest(
|
||||||
manifest: Record<string, any>,
|
manifest: JsonData,
|
||||||
volumes: VolumeForm[],
|
volumes: VolumeForm[],
|
||||||
mounts: MountForm[],
|
mounts: MountForm[],
|
||||||
passthroughVolumes: Record<string, any>[],
|
passthroughVolumes: JsonData[],
|
||||||
): Record<string, any> {
|
): JsonData {
|
||||||
const base = JSON.parse(JSON.stringify(manifest));
|
const base = JSON.parse(JSON.stringify(manifest));
|
||||||
base.spec = base.spec || {};
|
base.spec = base.spec || {};
|
||||||
base.spec.template = base.spec.template || {};
|
base.spec.template = base.spec.template || {};
|
||||||
base.spec.template.spec = base.spec.template.spec || {};
|
base.spec.template.spec = base.spec.template.spec || {};
|
||||||
const podSpec = base.spec.template.spec;
|
const podSpec = base.spec.template.spec;
|
||||||
|
|
||||||
const userVols = volumes.map(formToVolume).filter(Boolean) as Record<string, any>[];
|
const userVols = volumes.map(formToVolume).filter(Boolean) as JsonData[];
|
||||||
const mergedVols = [...passthroughVolumes, ...userVols];
|
const mergedVols = [...passthroughVolumes, ...userVols];
|
||||||
if (mergedVols.length > 0) podSpec.volumes = mergedVols;
|
if (mergedVols.length > 0) podSpec.volumes = mergedVols;
|
||||||
else delete podSpec.volumes;
|
else delete podSpec.volumes;
|
||||||
|
|
||||||
const passthroughNames = new Set(passthroughVolumes.map((v) => v.name));
|
const passthroughNames = new Set(passthroughVolumes.map((v) => v.name));
|
||||||
|
|
||||||
const applyMounts = (c: Record<string, any>) => {
|
const applyMounts = (c: JsonData) => {
|
||||||
const cName = c.name || '(unnamed)';
|
const cName = c.name || '(unnamed)';
|
||||||
const existing: Record<string, any>[] = Array.isArray(c.volumeMounts) ? c.volumeMounts : [];
|
const existing: JsonData[] = Array.isArray(c.volumeMounts) ? c.volumeMounts : [];
|
||||||
const kept = existing.filter((vm) => isPassthroughMount(vm, passthroughNames));
|
const kept = existing.filter((vm) => isPassthroughMount(vm, passthroughNames));
|
||||||
const user = mounts
|
const user = mounts
|
||||||
.filter((m) => m.containerName === cName)
|
.filter((m) => m.containerName === cName)
|
||||||
.map(formToMount)
|
.map(formToMount)
|
||||||
.filter(Boolean) as Record<string, any>[];
|
.filter(Boolean) as JsonData[];
|
||||||
const merged = [...kept, ...user];
|
const merged = [...kept, ...user];
|
||||||
if (merged.length > 0) c.volumeMounts = merged;
|
if (merged.length > 0) c.volumeMounts = merged;
|
||||||
else delete c.volumeMounts;
|
else delete c.volumeMounts;
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import { AlertTriangle, Loader2, Minus, Plus, Trash2 } from 'lucide-react';
|
|||||||
import { ModalShell } from './ModalShell';
|
import { ModalShell } from './ModalShell';
|
||||||
import { UpdateImageModal } from './UpdateImageModal';
|
import { UpdateImageModal } from './UpdateImageModal';
|
||||||
import type { ActionModal } from './types';
|
import type { ActionModal } from './types';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export interface ActionModalsProps {
|
export interface ActionModalsProps {
|
||||||
modal: ActionModal;
|
modal: ActionModal;
|
||||||
deployment: any;
|
deployment?: JsonData;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
deploymentName: string;
|
deploymentName: string;
|
||||||
newImage: string;
|
newImage: string;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { FileText, RefreshCw, Loader2, X } from 'lucide-react';
|
|||||||
import { kubernetesApi } from '@/api';
|
import { kubernetesApi } from '@/api';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { useUserStore } from '@/store';
|
import { useUserStore } from '@/store';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export interface PodLogsModalProps {
|
export interface PodLogsModalProps {
|
||||||
clusterCode: string;
|
clusterCode: string;
|
||||||
@@ -28,7 +29,7 @@ export function PodLogsModal({ clusterCode, namespace, podName, containers, onCl
|
|||||||
const loadLogs = async () => {
|
const loadLogs = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res: Record<string, any> = await kubernetesApi.getPodLogs(clusterCode, namespace, podName, tail, container || undefined);
|
const res: JsonData = await kubernetesApi.getPodLogs(clusterCode, namespace, podName, tail, container || undefined);
|
||||||
setLogs(res?.data?.logs || res?.logs || res || '无日志');
|
setLogs(res?.data?.logs || res?.logs || res || '无日志');
|
||||||
} catch {
|
} catch {
|
||||||
setLogs('获取日志失败');
|
setLogs('获取日志失败');
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// k8s-event-utils — K8s Event 字段归一化与展示 // Logan.Li
|
// k8s-event-utils — K8s Event 字段归一化与展示 // Logan.Li
|
||||||
import { elapsedSeconds, formatDuration, formatK8sTime } from './k8s-yaml-utils';
|
import { elapsedSeconds, formatDuration, formatK8sTime } from './k8s-yaml-utils';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export interface NormalizedEvent {
|
export interface NormalizedEvent {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -18,7 +19,7 @@ export interface NormalizedEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 后端/API 原始事件 → 统一结构 // Logan.Li */
|
/** 后端/API 原始事件 → 统一结构 // Logan.Li */
|
||||||
export function normalizeK8sEvent(raw: Record<string, any>): NormalizedEvent {
|
export function normalizeK8sEvent(raw: JsonData): NormalizedEvent {
|
||||||
const objectKind = raw.object_kind || raw.objectKind || '';
|
const objectKind = raw.object_kind || raw.objectKind || '';
|
||||||
const objectName = raw.object_name || raw.objectName || '';
|
const objectName = raw.object_name || raw.objectName || '';
|
||||||
const object = raw.object
|
const object = raw.object
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import type { JsonData } from "@/types/common";
|
||||||
// 网络/配置/存储资源新建时的空 manifest 模板 // Logan.Li
|
// 网络/配置/存储资源新建时的空 manifest 模板 // Logan.Li
|
||||||
|
|
||||||
export function createEmptyManifest(resource: string, namespace: string, name: string): Record<string, any> {
|
export function createEmptyManifest(resource: string, namespace: string, name: string): JsonData {
|
||||||
switch (resource) {
|
switch (resource) {
|
||||||
case 'services':
|
case 'services':
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Sidebar } from './Sidebar';
|
|||||||
import { Header } from './Header';
|
import { Header } from './Header';
|
||||||
import { Breadcrumb } from './Breadcrumb';
|
import { Breadcrumb } from './Breadcrumb';
|
||||||
import { userApi } from '@/api';
|
import { userApi } from '@/api';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MainLayout - 优化后的主布局组件
|
* MainLayout - 优化后的主布局组件
|
||||||
@@ -30,7 +31,7 @@ export function MainLayout() {
|
|||||||
if (!token || permissions !== null) return;
|
if (!token || permissions !== null) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
userApi.getMyPermissions()
|
userApi.getMyPermissions()
|
||||||
.then((data: any) => {
|
.then((data: JsonData) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
const perms: string[] = data?.permissions ?? [];
|
const perms: string[] = data?.permissions ?? [];
|
||||||
setPermissions(Array.isArray(perms) ? perms : []);
|
setPermissions(Array.isArray(perms) ? perms : []);
|
||||||
|
|||||||
@@ -17,16 +17,17 @@ import { AlertEventsTab } from './AlertEventsTab';
|
|||||||
import { AlertRulesTab } from './AlertRulesTab';
|
import { AlertRulesTab } from './AlertRulesTab';
|
||||||
import { AlertRuleDialog } from './AlertRuleDialog';
|
import { AlertRuleDialog } from './AlertRuleDialog';
|
||||||
import { AlertChannelDialog } from './AlertChannelDialog';
|
import { AlertChannelDialog } from './AlertChannelDialog';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export function AlertManager() {
|
export function AlertManager() {
|
||||||
const [activeTab, setActiveTab] = useState('events');
|
const [activeTab, setActiveTab] = useState('events');
|
||||||
const [alerts, setAlerts] = useState<any[]>([]);
|
const [alerts, setAlerts] = useState<JsonData[]>([]);
|
||||||
const [rules, setRules] = useState<any[]>([]);
|
const [rules, setRules] = useState<JsonData[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
|
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
|
||||||
const [editingRule, setEditingRule] = useState<any>(null);
|
const [editingRule, setEditingRule] = useState<JsonData>(null);
|
||||||
const [channelDialogOpen, setChannelDialogOpen] = useState(false);
|
const [channelDialogOpen, setChannelDialogOpen] = useState(false);
|
||||||
const [notifyChannels, setNotifyChannels] = useState<any[]>([]);
|
const [notifyChannels, setNotifyChannels] = useState<JsonData[]>([]);
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -36,9 +37,9 @@ export function AlertManager() {
|
|||||||
alertApi.getRules(),
|
alertApi.getRules(),
|
||||||
alertApi.getNotifyChannels(),
|
alertApi.getNotifyChannels(),
|
||||||
]);
|
]);
|
||||||
setAlerts((alertsData as Record<string, any>)?.list || []);
|
setAlerts((alertsData as JsonData)?.list || []);
|
||||||
setRules((rulesData as Record<string, any>)?.list || (Array.isArray(rulesData) ? rulesData : []));
|
setRules((rulesData as JsonData)?.list || (Array.isArray(rulesData) ? rulesData : []));
|
||||||
setNotifyChannels((channelsData as Record<string, any>)?.list || []);
|
setNotifyChannels((channelsData as JsonData)?.list || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load alert data:', error);
|
console.error('Failed to load alert data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -55,7 +56,7 @@ export function AlertManager() {
|
|||||||
setRuleDialogOpen(true);
|
setRuleDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEditRule = ( rule: any) => {
|
const openEditRule = (rule: JsonData) => {
|
||||||
setEditingRule(rule);
|
setEditingRule(rule);
|
||||||
setRuleDialogOpen(true);
|
setRuleDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import { alertApi } from '@/api';
|
import { alertApi } from '@/api';
|
||||||
import { parseChannels, getChannelIcon, getChannelTypeLabel } from './alertShared';
|
import { parseChannels, getChannelIcon, getChannelTypeLabel } from './alertShared';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
const emptyRuleForm = {
|
const emptyRuleForm = {
|
||||||
name: '',
|
name: '',
|
||||||
@@ -37,8 +38,8 @@ export function AlertRuleDialog({
|
|||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
editingRule: any;
|
editingRule?: JsonData;
|
||||||
notifyChannels: any[];
|
notifyChannels: JsonData[];
|
||||||
onSaved: () => void;
|
onSaved: () => void;
|
||||||
/** 新建时默认规则类型 // Logan.Li */
|
/** 新建时默认规则类型 // Logan.Li */
|
||||||
defaultType?: 'metric' | 'log' | 'event';
|
defaultType?: 'metric' | 'log' | 'event';
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { ContactDialog } from './ContactDialog';
|
|||||||
import { ContactGroupDialog } from './ContactGroupDialog';
|
import { ContactGroupDialog } from './ContactGroupDialog';
|
||||||
import SilenceRuleTab from './SilenceRuleTab';
|
import SilenceRuleTab from './SilenceRuleTab';
|
||||||
import type { Contact, ContactGroup } from './alert-center-helpers';
|
import type { Contact, ContactGroup } from './alert-center-helpers';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
const AlertCenter: React.FC = () => {
|
const AlertCenter: React.FC = () => {
|
||||||
const _confirm = useConfirm();
|
const _confirm = useConfirm();
|
||||||
@@ -40,7 +41,7 @@ const AlertCenter: React.FC = () => {
|
|||||||
const [silenceOpen, setSilenceOpen] = useState(false);
|
const [silenceOpen, setSilenceOpen] = useState(false);
|
||||||
// 规则弹框
|
// 规则弹框
|
||||||
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
|
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
|
||||||
const [editingRule, setEditingRule] = useState<any>(null);
|
const [editingRule, setEditingRule] = useState<JsonData>(null);
|
||||||
// 联系人 & 联系组
|
// 联系人 & 联系组
|
||||||
const [contacts, setContacts] = useState<Contact[]>([]);
|
const [contacts, setContacts] = useState<Contact[]>([]);
|
||||||
const [contactGroups, setContactGroups] = useState<ContactGroup[]>([]);
|
const [contactGroups, setContactGroups] = useState<ContactGroup[]>([]);
|
||||||
@@ -81,14 +82,14 @@ const AlertCenter: React.FC = () => {
|
|||||||
|
|
||||||
const fetchContacts = async () => {
|
const fetchContacts = async () => {
|
||||||
try {
|
try {
|
||||||
const res: Record<string, any> = await alertApi.getContacts();
|
const res: JsonData = await alertApi.getContacts();
|
||||||
setContacts((res?.data?.list || res?.list || []) as Contact[]);
|
setContacts((res?.data?.list || res?.list || []) as Contact[]);
|
||||||
} catch { /* silent */ }
|
} catch { /* silent */ }
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchContactGroups = async () => {
|
const fetchContactGroups = async () => {
|
||||||
try {
|
try {
|
||||||
const res: Record<string, any> = await alertApi.getContactGroups();
|
const res: JsonData = await alertApi.getContactGroups();
|
||||||
setContactGroups((res?.data?.list || res?.list || []) as ContactGroup[]);
|
setContactGroups((res?.data?.list || res?.list || []) as ContactGroup[]);
|
||||||
} catch { /* silent */ }
|
} catch { /* silent */ }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
type NotificationChannel, getChannelIcon, getChannelTypeLabel,
|
type NotificationChannel, getChannelIcon, getChannelTypeLabel,
|
||||||
} from './alert-center-helpers';
|
} from './alert-center-helpers';
|
||||||
import { AlertChannelDialog } from './AlertChannelDialog';
|
import { AlertChannelDialog } from './AlertChannelDialog';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
export const AlertChannelsTab: React.FC<{ refreshKey: number }> = ({ refreshKey }) => {
|
export const AlertChannelsTab: React.FC<{ refreshKey: number }> = ({ refreshKey }) => {
|
||||||
const _confirm = useConfirm();
|
const _confirm = useConfirm();
|
||||||
@@ -24,7 +25,7 @@ export const AlertChannelsTab: React.FC<{ refreshKey: number }> = ({ refreshKey
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await alertApi.getNotifyChannels();
|
const res = await alertApi.getNotifyChannels();
|
||||||
const list = (res as Record<string, any>)?.list || [];
|
const list = (res as JsonData)?.list || [];
|
||||||
setChannels(list);
|
setChannels(list);
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('获取通知渠道失败');
|
toast.error('获取通知渠道失败');
|
||||||
@@ -60,7 +61,7 @@ export const AlertChannelsTab: React.FC<{ refreshKey: number }> = ({ refreshKey
|
|||||||
const handleTest = async (id: number) => {
|
const handleTest = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
const res = await alertApi.testNotifyChannel(id);
|
const res = await alertApi.testNotifyChannel(id);
|
||||||
const status = (res as Record<string, any>)?.status;
|
const status = (res as JsonData)?.status;
|
||||||
if (status === 'success') {
|
if (status === 'success') {
|
||||||
toast.success('测试通知已发送');
|
toast.success('测试通知已发送');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { alertCenterApi } from '@/api';
|
import { alertCenterApi } from '@/api';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
// 静默类型中文标签
|
// 静默类型中文标签
|
||||||
const SILENCE_TYPE_LABELS: Record<string, string> = {
|
const SILENCE_TYPE_LABELS: Record<string, string> = {
|
||||||
@@ -49,7 +50,7 @@ export function SilenceDialog({
|
|||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
editingSilence: any;
|
editingSilence?: JsonData;
|
||||||
onSaved: () => void;
|
onSaved: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { toast } from 'sonner';
|
|||||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||||
import { alertCenterApi } from '@/api';
|
import { alertCenterApi } from '@/api';
|
||||||
import { SilenceDialog } from './SilenceDialog';
|
import { SilenceDialog } from './SilenceDialog';
|
||||||
|
import type { JsonData } from '@/types/common';
|
||||||
|
|
||||||
const SILENCE_TYPE_LABELS: Record<string, string> = {
|
const SILENCE_TYPE_LABELS: Record<string, string> = {
|
||||||
onetime: '一次性',
|
onetime: '一次性',
|
||||||
@@ -30,15 +31,15 @@ function formatTime(t: string | Date): string {
|
|||||||
|
|
||||||
const SilenceRuleTab: React.FC = () => {
|
const SilenceRuleTab: React.FC = () => {
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const [silences, setSilences] = useState<any[]>([]);
|
const [silences, setSilences] = useState<JsonData[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<any>(null);
|
const [editing, setEditing] = useState<JsonData>(null);
|
||||||
|
|
||||||
const fetchSilences = useCallback(async () => {
|
const fetchSilences = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res: Record<string, any> = await alertCenterApi.getSilences();
|
const res: JsonData = await alertCenterApi.getSilences();
|
||||||
const list = res?.list || res?.data?.list || [];
|
const list = res?.list || res?.data?.list || [];
|
||||||
setSilences(list);
|
setSilences(list);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -57,12 +58,12 @@ const SilenceRuleTab: React.FC = () => {
|
|||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (silence: any) => {
|
const handleEdit = (silence: JsonData) => {
|
||||||
setEditing(silence);
|
setEditing(silence);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (silence: any) => {
|
const handleDelete = async (silence: JsonData) => {
|
||||||
const ok = await confirm({
|
const ok = await confirm({
|
||||||
title: '删除静默规则',
|
title: '删除静默规则',
|
||||||
description: `确定删除「${silence.name}」?删除后该规则匹配的告警将恢复通知。`,
|
description: `确定删除「${silence.name}」?删除后该规则匹配的告警将恢复通知。`,
|
||||||
@@ -79,7 +80,7 @@ const SilenceRuleTab: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggle = async (silence: any) => {
|
const handleToggle = async (silence: JsonData) => {
|
||||||
try {
|
try {
|
||||||
await alertCenterApi.toggleSilence(silence.id);
|
await alertCenterApi.toggleSilence(silence.id);
|
||||||
toast.success(silence.status === 1 ? '已禁用' : '已启用');
|
toast.success(silence.status === 1 ? '已禁用' : '已启用');
|
||||||
@@ -89,7 +90,7 @@ const SilenceRuleTab: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isCurrentlyActive = (s: any): boolean => {
|
const isCurrentlyActive = (s: JsonData): boolean => {
|
||||||
if (s.is_active !== undefined) return s.is_active;
|
if (s.is_active !== undefined) return s.is_active;
|
||||||
if (s.status !== 1) return false;
|
if (s.status !== 1) return false;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|||||||
Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user