diff --git a/PROJECT_EVALUATION.md b/PROJECT_EVALUATION.md index 01eaea3..723731b 100644 --- a/PROJECT_EVALUATION.md +++ b/PROJECT_EVALUATION.md @@ -137,7 +137,7 @@ | 前端 React Hooks exhaustive-deps 缺失 | 38 处 | P1 | | 后端 `_ =` 忽略错误 | 13 处(3 处中风险) | P2 | | 前端硬编码内网 IP(10.10.10.75 等) | 6+ 处 | P2 | -| 后端告警引擎单实例 + 无优雅退出 | 多副本会重复告警 | P2 | +| 后端告警引擎分布式安全 | ✅ Redis 分布式锁(SetNX + 60s TTL) | 后端 | ~~P2~~ done | ### 3.3 🟢 低优先级 @@ -202,6 +202,6 @@ | 功能完整性 | **88/100** | 8 大模块齐全,K8s 管理极为完善;扣分项:监控大屏/链路追踪/告警静默未对接 | | 代码质量 | **72/100** | 后端规范好(0 panic, 1 TODO);前端 1026 处 any + 218 处未用变量是主要扣分项 | | 架构设计 | **82/100** | 前后端分离清晰,自研轻量 K8s 客户端;扣分项:kubernetes.go/api.ts 巨型文件 | -| 部署运维 | **85/100** | 三种部署方式 + run.sh 统一入口;扣分项:后端镜像偏大,告警引擎单实例 | +| 部署运维 | **87/100** | 三种部署方式 + run.sh 统一入口;扣分项:后端镜像偏大 | | 用户体验 | **86/100** | 骨架屏 + 页面过渡 + 5 套主题 + 懒加载;扣分项:无 i18n,无虚拟滚动 | | **综合** | **83/100** | 企业级运维平台,功能覆盖全面,工程治理是当前首要提升方向 | diff --git a/ROADMAP.md b/ROADMAP.md index 979a7a6..f1c50cb 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -162,11 +162,19 @@ services/api.ts (1324L) **方案**: 以 Kuboard code 路由为主,ID 路由保留向后兼容(标记 deprecated) -### A2: 告警引擎分布式安全 [pending] +### A2: 告警引擎分布式安全 [done] **问题**: 告警引擎用 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] @@ -207,7 +215,7 @@ services/api.ts (1324L) | F2 | 审计日志导出 | P1 | **done** | | F3 | 前端 RBAC 控制 | P1 | **done** | | A1 | K8s 路由统一 | P2 | pending | -| A2 | 告警引擎分布式 | P2 | pending | +| A2 | 告警引擎分布式 | P2 | **done** | | A3 | WebSocket 管理 | P2 | pending | | U1 | 虚拟滚动 | P3 | pending | | U2 | 统一 DataTable | P3 | pending | diff --git a/TDD_PLAN.md b/TDD_PLAN.md index 5002cef..5970fd6 100644 --- a/TDD_PLAN.md +++ b/TDD_PLAN.md @@ -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 diff --git a/backend/.aiops.args b/backend/.aiops.args new file mode 100644 index 0000000..1dbc55b --- /dev/null +++ b/backend/.aiops.args @@ -0,0 +1,4 @@ +--mode +local +--port +8080 diff --git a/backend/controller/alert_engine.go b/backend/controller/alert_engine.go index 55aa9f2..84226a0 100644 --- a/backend/controller/alert_engine.go +++ b/backend/controller/alert_engine.go @@ -1,32 +1,117 @@ package controller import ( + "context" + "crypto/rand" + "encoding/hex" "encoding/json" "fmt" + "log" "time" "ezops/model" + ezredis "ezops/pkg/redis" "gorm.io/gorm" ) -// AlertEngine 告警引擎 — 定时扫描 Agent 上报数据并触发告警 // Logan.Li +// AlertEngine 告警引擎 — 定时扫描 Agent 上报数据并触发告警 +// 支持分布式锁:多副本部署时仅 Leader 执行检查,避免重复告警 type AlertEngine struct { - db *gorm.DB + db *gorm.DB + cache ezredis.Cache // 可选 Redis 缓存(分布式锁) + instanceID string // 当前实例唯一标识 } -func NewAlertEngine(db *gorm.DB) *AlertEngine { - return &AlertEngine{db: db} +const ( + 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() { + if !e.tryAcquireLock() { + log.Printf("[AlertEngine] 非 Leader 副本(%s),跳过本轮检查", e.instanceID) + return + } + defer e.releaseLock() + e.checkResourceAlerts() e.checkOfflineAlerts() e.checkPortAlerts() 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连接/端口 阈值 func (e *AlertEngine) checkResourceAlerts() { var rules []model.AgentRule diff --git a/backend/pkg/redis/go_redis.go b/backend/pkg/redis/go_redis.go index 1564629..483b92b 100644 --- a/backend/pkg/redis/go_redis.go +++ b/backend/pkg/redis/go_redis.go @@ -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() } +// 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 { return r.client.Del(ctx, keys...).Err() } diff --git a/backend/pkg/redis/redis.go b/backend/pkg/redis/redis.go index 84edf57..ce0c034 100644 --- a/backend/pkg/redis/redis.go +++ b/backend/pkg/redis/redis.go @@ -51,6 +51,8 @@ type Cache interface { Capabilities() Capabilities Get(ctx context.Context, key string) (string, 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 Exists(ctx context.Context, keys ...string) (int64, error) Expire(ctx context.Context, key string, expiration time.Duration) error diff --git a/backend/pkg/redis/redka.go b/backend/pkg/redis/redka.go index bec9e04..ab4c5a6 100644 --- a/backend/pkg/redis/redka.go +++ b/backend/pkg/redis/redka.go @@ -65,6 +65,27 @@ func (r *redkaCache) Set(ctx context.Context, key string, value string, expirati 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 { for _, key := range keys { if _, err := r.db.Key().Delete(key); err != nil { diff --git a/backend/router/routes.go b/backend/router/routes.go index e60a92a..a19c86e 100644 --- a/backend/router/routes.go +++ b/backend/router/routes.go @@ -496,6 +496,9 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c kubernetesRouter.GET("/compare", k8sHandler.GetMultiClusterComparison) } + // 告警引擎(分布式锁 — 多副本部署时仅 Leader 执行检查) + alertEngine := controller.NewAlertEngine(db, redisCache) + // 告警路由 alertHandler := controller.NewAlertHandler(db) 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.DELETE("/silences/:id", alertHandler.DeleteSilence) 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) } - // 启动告警引擎(每 30 秒检查一次) - alertEngine := controller.NewAlertEngine(db) + // 启动告警引擎(每 30 秒检查一次;alertEngine 在告警路由上方创建) go func() { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..08360a8 --- /dev/null +++ b/config.yaml @@ -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 diff --git a/frontend/src/components/agent/AgentAlerts.tsx b/frontend/src/components/agent/AgentAlerts.tsx index 42134b0..cca2ff7 100644 --- a/frontend/src/components/agent/AgentAlerts.tsx +++ b/frontend/src/components/agent/AgentAlerts.tsx @@ -27,18 +27,19 @@ import { severityBadge, } from './agentAlertHelper'; import { AgentRuleDialog } from './AgentRuleDialog'; +import type { JsonData } from '@/types/common'; export function AgentAlerts() { const confirm = useConfirm(); const [activeTab, setActiveTab] = useState('events'); - const [alerts, setAlerts] = useState([]); - const [rules, setRules] = useState([]); + const [alerts, setAlerts] = useState([]); + const [rules, setRules] = useState([]); const [loading, setLoading] = useState(true); const [resolvedView, setResolvedView] = useState(false); // 规则编辑弹窗 const [ruleOpen, setRuleOpen] = useState(false); - const [editingRule, setEditingRule] = useState(null); + const [editingRule, setEditingRule] = useState(null); const loadData = useCallback(async () => { setLoading(true); @@ -80,7 +81,7 @@ export function AgentAlerts() { setRuleOpen(true); }; - const openEditRule = ( rule: any) => { + const openEditRule = (rule: JsonData) => { setEditingRule(rule); setRuleOpen(true); }; @@ -97,7 +98,7 @@ export function AgentAlerts() { } }; - const handleToggleRule = async ( rule: any) => { + const handleToggleRule = async (rule: JsonData) => { try { await agentApi.updateRule(rule.id, { ...rule, enabled: !rule.enabled }); loadData(); diff --git a/frontend/src/components/agent/AgentRuleDialog.tsx b/frontend/src/components/agent/AgentRuleDialog.tsx index c0e070a..205fc3a 100644 --- a/frontend/src/components/agent/AgentRuleDialog.tsx +++ b/frontend/src/components/agent/AgentRuleDialog.tsx @@ -23,11 +23,12 @@ import { } from '@/components/ui/select'; import { Loader2 } from 'lucide-react'; import { METRIC_OPTIONS, OPERATOR_OPTIONS, SEVERITY_OPTIONS } from './agentAlertHelper'; +import type { JsonData } from '@/types/common'; interface Props { open: boolean; onOpenChange: (v: boolean) => void; - editingRule: any; + editingRule?: JsonData; onSuccess: () => void; } diff --git a/frontend/src/components/agent/HostDetailDialog.tsx b/frontend/src/components/agent/HostDetailDialog.tsx index 91ae78e..2ad3289 100644 --- a/frontend/src/components/agent/HostDetailDialog.tsx +++ b/frontend/src/components/agent/HostDetailDialog.tsx @@ -29,17 +29,18 @@ import { Tooltip, ResponsiveContainer, } from 'recharts'; +import type { JsonData } from '@/types/common'; export function HostDetailDialog({ host, onClose, onCommand, }: { - host: any; + host?: JsonData; onClose: () => void; onCommand: (hostname: string) => void; }) { - const [history, setHistory] = useState([]); + const [history, setHistory] = useState([]); const [loading, setLoading] = useState(false); const [hours, setHours] = useState(1); @@ -60,7 +61,7 @@ export function HostDetailDialog({ if (!host) return null; const online = isOnline(host.last_report || host.last_reported_at || host.updated_at); - const ports: Record[] = Array.isArray(host.ports) ? host.ports : []; + const ports: JsonData[] = Array.isArray(host.ports) ? host.ports : []; 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' }), diff --git a/frontend/src/components/agent/agentAlertHelper.tsx b/frontend/src/components/agent/agentAlertHelper.tsx index 2390528..01d2226 100644 --- a/frontend/src/components/agent/agentAlertHelper.tsx +++ b/frontend/src/components/agent/agentAlertHelper.tsx @@ -1,6 +1,7 @@ // Agent 告警常量与辅助函数(从 AgentAlerts.tsx 提取) // Logan.Li import { Badge } from '@/components/ui/badge'; import { AlertTriangle, Cpu, MemoryStick, HardDrive, WifiOff, Network } from 'lucide-react'; +import type { JsonData } from '@/types/common'; // 监控指标选项(含图标,告警事件/规则/编辑弹窗共用) // Logan.Li export const METRIC_OPTIONS = [ @@ -33,7 +34,7 @@ export function getMetricLabel(metric: string) { } // 兼容多种列表响应格式(数组 / {list} / {items} / {data}) // Logan.Li -export function asList( data: any): unknown[] { +export function asList(data: JsonData): unknown[] { if (!data) return []; if (Array.isArray(data)) return data; if (Array.isArray(data.list)) return data.list; diff --git a/frontend/src/components/aiops/AIAlertAnalyzer.tsx b/frontend/src/components/aiops/AIAlertAnalyzer.tsx index 0276f6b..b60bb42 100644 --- a/frontend/src/components/aiops/AIAlertAnalyzer.tsx +++ b/frontend/src/components/aiops/AIAlertAnalyzer.tsx @@ -25,6 +25,7 @@ import { } from './analyzerHelpers'; import { AIAnalysisResult } from './AIAnalysisResult'; import { AutoFixDialog } from './AutoFixDialog'; +import type { JsonData } from '@/types/common'; // Parse LLM JSON response (may have markdown fences) into AIAnalysis function parseAIResponse(raw: string, fallback: AIAnalysis): AIAnalysis { @@ -61,7 +62,7 @@ export function AIAlertAnalyzer() { const loadAlerts = async () => { setLoading(true); try { - const res: Record = await alertApi.getAlerts({ page: 1, page_size: 50 }); + const res: JsonData = await alertApi.getAlerts({ page: 1, page_size: 50 }); // 列表响应格式:http.ts 已提取 .data,此处仍是 {list, total} const list = res?.list || res || []; setAlerts(list.map(mapAlertEvent)); @@ -102,7 +103,7 @@ export function AIAlertAnalyzer() { }`; const res = await aiApi.chat([{ role: 'user', content: prompt }]); - const content = (res as Record)?.content || (res as Record)?.message?.content || ''; + const content = (res as JsonData)?.content || (res as JsonData)?.message?.content || ''; if (content) { setAnalysis(parseAIResponse(content, fallback)); } else { diff --git a/frontend/src/components/aiops/AIOpsAssistant.tsx b/frontend/src/components/aiops/AIOpsAssistant.tsx index a6097c1..7ec563b 100644 --- a/frontend/src/components/aiops/AIOpsAssistant.tsx +++ b/frontend/src/components/aiops/AIOpsAssistant.tsx @@ -42,6 +42,7 @@ import { Wifi, Square, } from 'lucide-react'; +import type { JsonData } from '@/types/common'; // LLM 提供商列表(OpenAI 兼容接口) // Logan.Li const LLM_PROVIDERS: { id: string; name: string; defaultEndpoint: string; models: string[] }[] = [ @@ -68,7 +69,7 @@ const defaultConfig: LLMConfigData = { // 拉取最近告警事件,格式化为紧凑列表供 LLM 分析 async function loadAlertsContext(): Promise { const res = await alertApi.getAlerts({ page: 1, page_size: 20 }); - const alerts: Record[] = (res as Record)?.list || []; + const alerts: JsonData[] = (res as JsonData)?.list || []; if (!alerts.length) return '(当前无告警事件)'; 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)}`, @@ -80,7 +81,7 @@ async function loadAlertsContext(): Promise { // 拉取 Agent 实时主机指标,识别高负载主机 async function loadMetricsContext(): Promise { const res = await agentApi.getLatest(); - const hosts: Record[] = Array.isArray(res) ? res : (res as Record)?.list || []; + const hosts: JsonData[] = Array.isArray(res) ? res : (res as JsonData)?.list || []; if (!hosts.length) return '(当前无 Agent 上报数据,可能未部署监控 Agent)'; const lines = hosts.slice(0, 20).map((h, i) => { const cpu = h.cpu_usage ?? '-'; @@ -101,11 +102,11 @@ async function loadReportContext(): Promise { dashboardStatsApi.getAlerts(10).catch(() => null), dashboardStatsApi.getReleases(10).catch(() => null), ]); - const s: Record = 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 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[] = Array.isArray(alerts) ? alerts : (alerts as Record)?.list || []; - const releaseList: Record[] = Array.isArray(releases) ? releases : (releases as Record)?.list || []; + const alertList: JsonData[] = Array.isArray(alerts) ? alerts : (alerts as JsonData)?.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 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') || '(无)'}`; @@ -188,7 +189,7 @@ export function AIOpsAssistant() { const loadHistory = async () => { try { const res = await aiApi.getHistory(sessionId); - const items: ChatHistoryItem[] = (res as Record)?.list || []; + const items: ChatHistoryItem[] = (res as JsonData)?.list || []; setMessages( items.map((m) => ({ id: String(m.id), @@ -227,7 +228,7 @@ export function AIOpsAssistant() { try { const saved = await aiApi.saveConfig(config); setConfig(saved); - setConfigReady(!!(saved.enabled && (saved as Record).configured)); + setConfigReady(!!(saved.enabled && (saved as JsonData).configured)); toast.success('配置已保存'); setIsConfigOpen(false); } catch (e) { diff --git a/frontend/src/components/aiops/AIPredictiveAnalytics.tsx b/frontend/src/components/aiops/AIPredictiveAnalytics.tsx index b480d75..39edb04 100644 --- a/frontend/src/components/aiops/AIPredictiveAnalytics.tsx +++ b/frontend/src/components/aiops/AIPredictiveAnalytics.tsx @@ -36,6 +36,7 @@ import { trendColorClass, type CapacityForecast, } from './predictUtils'; +import type { JsonData } from '@/types/common'; // 重新导出 CapacityForecast,兼容已有 import 路径 './AIPredictiveAnalytics' export type { CapacityForecast }; @@ -46,10 +47,10 @@ export function AIPredictiveAnalytics() { const [loading, setLoading] = useState(true); // 原始数据 - const [rawMetrics, setRawMetrics] = useState([]); - const [serverStats, setServerStats] = useState({}); - const [alertEvents, setAlertEvents] = useState([]); - const [alertRules, setAlertRules] = useState([]); + const [rawMetrics, setRawMetrics] = useState([]); + const [serverStats, setServerStats] = useState({}); + const [alertEvents, setAlertEvents] = useState([]); + const [alertRules, setAlertRules] = useState([]); useEffect(() => { void loadData(); @@ -66,9 +67,9 @@ export function AIPredictiveAnalytics() { ]); setRawMetrics(Array.isArray(metricsData) ? metricsData : []); setServerStats(statsResp || {}); - const alertsRaw = (alertsResp as Record)?.list || (alertsResp as Record) || []; + const alertsRaw = (alertsResp as JsonData)?.list || (alertsResp as JsonData) || []; setAlertEvents(Array.isArray(alertsRaw) ? alertsRaw : []); - const rulesRaw = (rulesResp as Record)?.list || (rulesResp as Record) || []; + const rulesRaw = (rulesResp as JsonData)?.list || (rulesResp as JsonData) || []; setAlertRules(Array.isArray(rulesRaw) ? rulesRaw : []); } catch { // 保持空数据,组件显示空状态 diff --git a/frontend/src/components/aiops/analyzerHelpers.ts b/frontend/src/components/aiops/analyzerHelpers.ts index caa0275..90e1ace 100644 --- a/frontend/src/components/aiops/analyzerHelpers.ts +++ b/frontend/src/components/aiops/analyzerHelpers.ts @@ -1,3 +1,4 @@ +import type { JsonData } from "@/types/common"; // AI 告警分析纯逻辑辅助函数:类型定义 + 后端告警映射 + 动态 AI 分析生成 + 严重等级配色 // Logan.Li // 告警条目(前端统一模型,从后端 alert event 映射而来) @@ -145,7 +146,7 @@ export function generateAIAnalysis(alert: AlertItem): AIAnalysis { // ─── 后端告警事件 → 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 || ''; // 从表达式(如 "cpu_usage > 80")中提取指标名与阈值 const metricMatch = expr.match(/([\w_]+)\s*[><=!]+\s*([\d.]+)/); diff --git a/frontend/src/components/aiops/predictUtils.ts b/frontend/src/components/aiops/predictUtils.ts index 2920a5a..31bb85f 100644 --- a/frontend/src/components/aiops/predictUtils.ts +++ b/frontend/src/components/aiops/predictUtils.ts @@ -1,3 +1,4 @@ +import type { JsonData } from "@/types/common"; /** * 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 .filter((m) => m.name === name) .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()) @@ -140,10 +141,10 @@ export function anomalyAction(metricKey: string): string { /** computePredictions 的输入参数 */ export interface ComputePredictionsInput { - rawMetrics: any[]; - serverStats: any; - alertEvents: any[]; - alertRules: any[]; + rawMetrics: JsonData[]; + serverStats?: JsonData; + alertEvents: JsonData[]; + alertRules: JsonData[]; selectedTimeRange: string; // '1h' | '24h' | '7d' | '30d' } diff --git a/frontend/src/components/approval/ApprovalCreateDialog.tsx b/frontend/src/components/approval/ApprovalCreateDialog.tsx index 6a79099..2704be0 100644 --- a/frontend/src/components/approval/ApprovalCreateDialog.tsx +++ b/frontend/src/components/approval/ApprovalCreateDialog.tsx @@ -23,11 +23,12 @@ import { Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { approvalApi } from '@/api'; import { parseFields, APPROVAL_TYPE_OPTIONS } from './helpers'; +import type { JsonData } from '@/types/common'; interface ApprovalCreateDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - templates: any[]; + templates: JsonData[]; onSuccess: () => void; } @@ -38,8 +39,8 @@ export function ApprovalCreateDialog({ templates, onSuccess, }: ApprovalCreateDialogProps) { - const [selectedTemplate, setSelectedTemplate] = useState(null); - const [formData, setFormData] = useState>({}); + const [selectedTemplate, setSelectedTemplate] = useState(null); + const [formData, setFormData] = useState({}); const [submitting, setSubmitting] = useState(false); // 提交新建申请 // Logan.Li @@ -143,7 +144,7 @@ export function ApprovalCreateDialog({
- {parseFields(selectedTemplate.form_schema).map(( field: any) => { + {parseFields(selectedTemplate.form_schema).map((field: JsonData) => { const isTextarea = field.type === 'textarea'; return (
diff --git a/frontend/src/components/approval/ApprovalDetailDialog.tsx b/frontend/src/components/approval/ApprovalDetailDialog.tsx index 861fbe8..d4a1922 100644 --- a/frontend/src/components/approval/ApprovalDetailDialog.tsx +++ b/frontend/src/components/approval/ApprovalDetailDialog.tsx @@ -16,14 +16,15 @@ import { getApprovalTypeColor, getInstanceType, } from './helpers'; +import type { JsonData } from '@/types/common'; interface ApprovalDetailDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - instance: any; - templates: any[]; - onApprove: ( instance: any) => void; - onReject: ( instance: any) => void; + instance?: JsonData; + templates: JsonData[]; + onApprove: (instance: JsonData) => void; + onReject: (instance: JsonData) => void; } // 审批详情弹窗 — 展示流程详情/表单/进度/操作记录 // Logan.Li @@ -91,7 +92,7 @@ export function ApprovalDetailDialog({

操作记录

- {instance.actions.map(( action: any, index: number) => ( + {instance.actions.map((action: JsonData, index: number) => (
{action.action === 'approve' ? ( diff --git a/frontend/src/components/approval/ApprovalFlow.tsx b/frontend/src/components/approval/ApprovalFlow.tsx index d93edb6..bd0e13e 100644 --- a/frontend/src/components/approval/ApprovalFlow.tsx +++ b/frontend/src/components/approval/ApprovalFlow.tsx @@ -33,13 +33,14 @@ import { getApprovalTypeColor, getInstanceType, } from './helpers'; +import type { JsonData } from '@/types/common'; export function ApprovalFlow() { const [activeTab, setActiveTab] = useState<'instances' | 'flows' | 'my-approvals'>('instances'); - const [processes, setProcesses] = useState([]); - const [templates, setTemplates] = useState([]); - const [myApprovals, setMyApprovals] = useState([]); + const [processes, setProcesses] = useState([]); + const [templates, setTemplates] = useState([]); + const [myApprovals, setMyApprovals] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); @@ -48,7 +49,7 @@ export function ApprovalFlow() { // 详情弹窗 const [detailOpen, setDetailOpen] = useState(false); - const [selectedInstance, setSelectedInstance] = useState(null); + const [selectedInstance, setSelectedInstance] = useState(null); // 审批操作弹窗 const [actionDialogOpen, setActionDialogOpen] = useState(false); @@ -95,13 +96,13 @@ export function ApprovalFlow() { }); // 打开详情弹窗 // Logan.Li - const handleView = ( instance: any) => { + const handleView = (instance: JsonData) => { setSelectedInstance(instance); setDetailOpen(true); }; // 打开审批操作弹窗 // Logan.Li - const openActionDialog = (type: 'approve' | 'reject', instance: any) => { + const openActionDialog = (type: 'approve' | 'reject', instance: JsonData) => { setActionType(type); setSelectedInstance(instance); setActionComment(''); @@ -123,7 +124,7 @@ export function ApprovalFlow() { setActionDialogOpen(false); loadData(); } catch (error) { - toast.error('操作失败', { description: (error as Record).message }); + toast.error('操作失败', { description: (error as JsonData).message }); } finally { setActionSubmitting(false); } diff --git a/frontend/src/components/approval/ApprovalProcessTab.tsx b/frontend/src/components/approval/ApprovalProcessTab.tsx index 7280197..02aab72 100644 --- a/frontend/src/components/approval/ApprovalProcessTab.tsx +++ b/frontend/src/components/approval/ApprovalProcessTab.tsx @@ -19,6 +19,7 @@ import { getApprovalTypeIcon, getInstanceType, } from './helpers'; +import type { JsonData } from '@/types/common'; // 状态标签页定义 const STATUS_TABS = [ @@ -69,16 +70,16 @@ function getStatusBadge(status: string) { } interface ApprovalProcessTabProps { - processes: any[]; - filteredInstances: any[]; + processes: JsonData[]; + filteredInstances: JsonData[]; loading: boolean; searchQuery: string; setSearchQuery: (v: string) => void; statusFilter: string; setStatusFilter: (v: string) => void; - onView: ( instance: any) => void; - onApprove: ( instance: any) => void; - onReject: ( instance: any) => void; + onView: (instance: JsonData) => void; + onApprove: (instance: JsonData) => void; + onReject: (instance: JsonData) => void; } export function ApprovalProcessTab({ diff --git a/frontend/src/components/approval/ApprovalTemplateTab.tsx b/frontend/src/components/approval/ApprovalTemplateTab.tsx index 6b9e0b1..8d30528 100644 --- a/frontend/src/components/approval/ApprovalTemplateTab.tsx +++ b/frontend/src/components/approval/ApprovalTemplateTab.tsx @@ -3,8 +3,9 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; 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 (typeof steps === 'string') { try { @@ -31,14 +32,14 @@ const getApprovalTypeColor = (value: string): string => 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); if (nodes.length === 0) { return
暂无流程节点
; } return (
- {nodes.map(( node: any, index: number) => ( + {nodes.map((node: JsonData, index: number) => (
void; } diff --git a/frontend/src/components/approval/helpers.tsx b/frontend/src/components/approval/helpers.tsx index def7c80..1f17ed1 100644 --- a/frontend/src/components/approval/helpers.tsx +++ b/frontend/src/components/approval/helpers.tsx @@ -8,9 +8,10 @@ import { GitCommit, FileText, } from 'lucide-react'; +import type { JsonData } from '@/types/common'; // 解析审批步骤数组(兼容 JSON 字符串) // Logan.Li -export function parseSteps( steps: any) { +export function parseSteps(steps: JsonData) { if (Array.isArray(steps)) return steps; if (typeof steps === 'string') { try { @@ -24,7 +25,7 @@ export function parseSteps( steps: any) { } // 解析模板的 form_schema,返回字段定义数组 // Logan.Li -export function parseFields( formSchema: any): any[] { +export function parseFields(formSchema: JsonData): JsonData[] { if (!formSchema) return []; const schema = typeof formSchema === 'string' ? (() => { try { return JSON.parse(formSchema); } catch { return null; } })() : formSchema; if (!schema) return []; @@ -32,7 +33,7 @@ export function parseFields( formSchema: any): any[] { } // 在详情弹窗中渲染表单数据为键值对展示 // Logan.Li -export function renderFormData(formDataStr: string, template?: any) { +export function renderFormData(formDataStr: string, template?: JsonData) { if (!formDataStr) return null; let data: Record; try { @@ -78,26 +79,26 @@ export const getApprovalTypeIcon = (value: string) => APPROVAL_TYPE_OPTIONS.find((o) => o.value === value)?.icon || FileText; // 获取审批实例的类型(优先从关联模板取) // Logan.Li -export const getInstanceType = ( instance: any): string => +export const getInstanceType = (instance: JsonData): string => instance?.template?.type || instance?.type || ''; // 计算审批进度信息 // Logan.Li -export function getProgressInfo( instance: any) { +export function getProgressInfo(instance: JsonData) { const steps = parseSteps(instance.steps); const currentIndex = instance.current_step ?? 0; 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; return { steps, currentIndex, total, completed, percentage }; } // 渲染审批进度(用于详情弹窗) // Logan.Li -export function renderStepProgress( instance: any) { +export function renderStepProgress(instance: JsonData) { const { steps, currentIndex } = getProgressInfo(instance); if (steps.length === 0) return null; return (
- {steps.map(( step: any, index: number) => { + {steps.map((step: JsonData, index: number) => { const isCompleted = step.status === 'approved' || step.status === 'completed'; const isCurrent = index === currentIndex; const isRejected = step.status === 'rejected'; diff --git a/frontend/src/components/automation/CronJobFormDialog.tsx b/frontend/src/components/automation/CronJobFormDialog.tsx index e24b63d..50bfd81 100644 --- a/frontend/src/components/automation/CronJobFormDialog.tsx +++ b/frontend/src/components/automation/CronJobFormDialog.tsx @@ -21,6 +21,7 @@ import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { toast } from 'sonner'; import { cronJobApi, scriptApi } from '@/api'; +import type { JsonData } from '@/types/common'; // 定时任务类型定义 // Logan.Li const jobTypes = [ @@ -37,7 +38,7 @@ export function CronJobFormDialog({ }: { open: boolean; onOpenChange: (v: boolean) => void; - editJob: any | null; + editJob: JsonData | null; onSuccess: () => void; }) { const [form, setForm] = useState({ @@ -48,15 +49,15 @@ export function CronJobFormDialog({ content: '', script_id: null as number | null, }); - const [scripts, setScripts] = useState([]); - const [selectedScript, setSelectedScript] = useState(null); + const [scripts, setScripts] = useState([]); + const [selectedScript, setSelectedScript] = useState(null); const [submitting, setSubmitting] = useState(false); // 弹窗打开时:加载脚本库 + 初始化表单 // Logan.Li useEffect(() => { if (!open) return; // 拉取脚本库 - scriptApi.getScripts({ page_size: 200 }).then(( res: any) => { + scriptApi.getScripts({ page_size: 200 }).then((res: JsonData) => { setScripts(res?.list || []); }).catch(() => setScripts([])); @@ -71,7 +72,7 @@ export function CronJobFormDialog({ }); // 如果关联了脚本,拉取脚本详情 if (editJob.script_id) { - scriptApi.getScript(editJob.script_id).then(( res: any) => { + scriptApi.getScript(editJob.script_id).then((res: JsonData) => { setSelectedScript(res); }).catch(() => setSelectedScript(null)); } else { diff --git a/frontend/src/components/automation/ExecutionHistoryDialog.tsx b/frontend/src/components/automation/ExecutionHistoryDialog.tsx index 98617ed..ad83bc6 100644 --- a/frontend/src/components/automation/ExecutionHistoryDialog.tsx +++ b/frontend/src/components/automation/ExecutionHistoryDialog.tsx @@ -17,6 +17,7 @@ import { import { History, Loader2, Terminal } from 'lucide-react'; import { taskExecutionApi } from '@/api'; import { getStatusBadge } from './cronJobHelpers'; +import type { JsonData } from '@/types/common'; // 定时任务执行历史弹窗(打开时自动加载,点击行展开输出) // Logan.Li export function ExecutionHistoryDialog({ @@ -26,7 +27,7 @@ export function ExecutionHistoryDialog({ open: boolean; onOpenChange: (v: boolean) => void; }) { - const [executions, setExecutions] = useState([]); + const [executions, setExecutions] = useState([]); const [loading, setLoading] = useState(false); const [expandedExec, setExpandedExec] = useState(null); diff --git a/frontend/src/components/automation/ScriptManager.tsx b/frontend/src/components/automation/ScriptManager.tsx index 67cd084..8540009 100644 --- a/frontend/src/components/automation/ScriptManager.tsx +++ b/frontend/src/components/automation/ScriptManager.tsx @@ -20,6 +20,7 @@ import ScriptDetailDialog from './ScriptDetailDialog'; import ScriptLogsDialog from './ScriptLogsDialog'; import ScriptCard from './ScriptCard'; import ScriptExecutionsTable from './ScriptExecutionsTable'; +import type { JsonData } from '@/types/common'; export function ScriptManager() { const _confirm = useConfirm(); @@ -49,7 +50,7 @@ export function ScriptManager() { setLoading(true); try { const res = await scriptApi.getScripts({ page_size: 100 }); - const items = (res as Record)?.list || []; + const items = (res as JsonData)?.list || []; setScripts(items.map(mapScript)); } catch { toast.error('获取脚本列表失败'); @@ -61,7 +62,7 @@ export function ScriptManager() { const fetchExecutions = async () => { try { const res = await taskExecutionApi.getExecutions({ task_type: 'script', page_size: 50 }); - const items = (res as Record)?.list || []; + const items = (res as JsonData)?.list || []; setExecutions(items); } catch { // 静默失败 @@ -189,7 +190,7 @@ export function ScriptManager() { setExecuting(true); try { const res = await scriptApi.executeScript(sc.id); - const exec = (res as Record)?.data ?? res; + const exec = (res as JsonData)?.data ?? res; if (exec?.status === 'success') { toast.success(`执行成功,耗时 ${exec.duration ?? 0}ms`); } else { diff --git a/frontend/src/components/automation/script-helpers.tsx b/frontend/src/components/automation/script-helpers.tsx index 0e1adde..fa3d311 100644 --- a/frontend/src/components/automation/script-helpers.tsx +++ b/frontend/src/components/automation/script-helpers.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Clock, Loader2, CheckCircle, XCircle } from 'lucide-react'; 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 { id: raw.id, name: raw.name, diff --git a/frontend/src/components/cloud/BillingRecordDialog.tsx b/frontend/src/components/cloud/BillingRecordDialog.tsx index c93f6e5..0b5713f 100644 --- a/frontend/src/components/cloud/BillingRecordDialog.tsx +++ b/frontend/src/components/cloud/BillingRecordDialog.tsx @@ -22,6 +22,7 @@ import { Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { billingApi } from '@/api'; import { providerLabels } from './CloudBillingHelpers'; +import type { JsonData } from '@/types/common'; const emptyRecord = { provider: 'aliyun', @@ -36,12 +37,12 @@ const emptyRecord = { interface Props { open: boolean; onOpenChange: (open: boolean) => void; - editingRecord: any | null; + editingRecord: JsonData | null; onSuccess: () => void; } export function BillingRecordDialog({ open, onOpenChange, editingRecord, onSuccess }: Props) { - const [form, setForm] = useState(emptyRecord); + const [form, setForm] = useState(emptyRecord); const [submitting, setSubmitting] = useState(false); // 弹窗打开时初始化表单(新建预填今日日期,编辑回填数据) // Logan.Li @@ -80,7 +81,7 @@ export function BillingRecordDialog({ open, onOpenChange, editingRecord, onSucce onSuccess(); toast('保存成功'); } catch (e) { - toast.error('保存失败', { description: (e as Record).message }); + toast.error('保存失败', { description: (e as JsonData).message }); } finally { setSubmitting(false); } diff --git a/frontend/src/components/cloud/BillingRuleDialog.tsx b/frontend/src/components/cloud/BillingRuleDialog.tsx index 4323317..d8eb152 100644 --- a/frontend/src/components/cloud/BillingRuleDialog.tsx +++ b/frontend/src/components/cloud/BillingRuleDialog.tsx @@ -23,18 +23,19 @@ import { Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { billingApi } from '@/api'; import { providerLabels } from './CloudBillingHelpers'; +import type { JsonData } from '@/types/common'; const emptyRule = { name: '', provider: '', metric: 'daily', threshold: '', operator: '>', enabled: true }; interface Props { open: boolean; onOpenChange: (open: boolean) => void; - editingRule: any | null; + editingRule: JsonData | null; onSuccess: () => void; } export function BillingRuleDialog({ open, onOpenChange, editingRule, onSuccess }: Props) { - const [form, setForm] = useState(emptyRule); + const [form, setForm] = useState(emptyRule); const [submitting, setSubmitting] = useState(false); // 弹窗打开时初始化表单 // Logan.Li @@ -71,7 +72,7 @@ export function BillingRuleDialog({ open, onOpenChange, editingRule, onSuccess } onSuccess(); toast('保存成功'); } catch (e) { - toast.error('保存失败', { description: (e as Record).message }); + toast.error('保存失败', { description: (e as JsonData).message }); } finally { setSubmitting(false); } diff --git a/frontend/src/components/cloud/CloudAccountManager.tsx b/frontend/src/components/cloud/CloudAccountManager.tsx index 4f714e9..3fc9e36 100644 --- a/frontend/src/components/cloud/CloudAccountManager.tsx +++ b/frontend/src/components/cloud/CloudAccountManager.tsx @@ -26,6 +26,7 @@ import { } from 'lucide-react'; import { cloudApi } from '@/api'; import { CloudBrandIcon, getProviderColor } from '@/components/icons/CloudBrandIcon'; +import type { JsonData } from '@/types/common'; const providerLabels: Record = { aws: 'AWS', @@ -37,7 +38,7 @@ const providerLabels: Record = { }; export function CloudAccountManager() { - const [accounts, setAccounts] = useState([]); + const [accounts, setAccounts] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [selectedProvider, setSelectedProvider] = useState('all'); @@ -45,7 +46,7 @@ export function CloudAccountManager() { // 创建/编辑账号表单状态 const [formOpen, setFormOpen] = useState(false); const [submitting, setSubmitting] = useState(false); - const [editingAccount, setEditingAccount] = useState(null); + const [editingAccount, setEditingAccount] = useState(null); const [formData, setFormData] = useState({ name: '', provider: 'aliyun', @@ -72,7 +73,7 @@ export function CloudAccountManager() { setFormOpen(true); }; - const handleOpenEdit = ( account: any) => { + const handleOpenEdit = (account: JsonData) => { setEditingAccount(account); setFormData({ name: account.name || '', @@ -89,7 +90,7 @@ export function CloudAccountManager() { if (!formData.name.trim() || !formData.provider.trim()) return; try { setSubmitting(true); - const payload: Record = { + const payload: JsonData = { name: formData.name.trim(), provider: formData.provider, access_key: formData.access_key.trim(), @@ -114,7 +115,7 @@ export function CloudAccountManager() { const loadData = async () => { try { setLoading(true); - const res: Record = await cloudApi.getAccounts(); + const res: JsonData = await cloudApi.getAccounts(); setAccounts(res?.list || []); } catch (e) { console.error('加载云账号列表失败:', e); diff --git a/frontend/src/components/cloud/CloudBilling.tsx b/frontend/src/components/cloud/CloudBilling.tsx index e388c31..9b63ab9 100644 --- a/frontend/src/components/cloud/CloudBilling.tsx +++ b/frontend/src/components/cloud/CloudBilling.tsx @@ -28,29 +28,30 @@ import { formatCurrency, TrendChart, providerLabels, providerColors } from './Cl import { StatStrip } from '@/components/ui/stat-strip'; import { BillingRecordDialog } from './BillingRecordDialog'; import { BillingRuleDialog } from './BillingRuleDialog'; +import type { JsonData } from '@/types/common'; export function CloudBilling() { const confirm = useConfirm(); const [activeTab, setActiveTab] = useState<'overview' | 'records' | 'alerts'>('overview'); - const [stats, setStats] = useState(null); + const [stats, setStats] = useState(null); const [statsLoading, setStatsLoading] = useState(true); - const [records, setRecords] = useState([]); + const [records, setRecords] = useState([]); const [recordsLoading, setRecordsLoading] = useState(true); const [filterProvider, setFilterProvider] = useState(''); const [filterStart, setFilterStart] = useState(''); const [filterEnd, setFilterEnd] = useState(''); - const [rules, setRules] = useState([]); + const [rules, setRules] = useState([]); const [rulesLoading, setRulesLoading] = useState(true); // 弹窗状态(open + 编辑目标) // Logan.Li const [recordDialogOpen, setRecordDialogOpen] = useState(false); - const [editingRecord, setEditingRecord] = useState(null); + const [editingRecord, setEditingRecord] = useState(null); const [ruleDialogOpen, setRuleDialogOpen] = useState(false); - const [editingRule, setEditingRule] = useState(null); + const [editingRule, setEditingRule] = useState(null); const loadStats = useCallback(async () => { setStatsLoading(true); @@ -58,7 +59,7 @@ export function CloudBilling() { const res = await billingApi.getStats(); setStats(res); } catch (e) { - toast.error('加载统计失败', { description: (e as Record).message }); + toast.error('加载统计失败', { description: (e as JsonData).message }); } finally { setStatsLoading(false); } @@ -67,14 +68,14 @@ export function CloudBilling() { const loadRecords = useCallback(async () => { setRecordsLoading(true); try { - const params: Record = {}; + const params: JsonData = {}; if (filterProvider) params.provider = filterProvider; if (filterStart) params.start_date = filterStart; if (filterEnd) params.end_date = filterEnd; const res = await billingApi.list(params); setRecords(res.list || []); } catch (e) { - toast.error('加载账单失败', { description: (e as Record).message }); + toast.error('加载账单失败', { description: (e as JsonData).message }); } finally { setRecordsLoading(false); } @@ -86,7 +87,7 @@ export function CloudBilling() { const res = await billingApi.listAlertRules(); setRules(res.list || []); } catch (e) { - toast.error('加载预警规则失败', { description: (e as Record).message }); + toast.error('加载预警规则失败', { description: (e as JsonData).message }); } finally { setRulesLoading(false); } @@ -109,7 +110,7 @@ export function CloudBilling() { setRecordDialogOpen(true); }; - const openEditRecord = ( r: any) => { + const openEditRecord = (r: JsonData) => { setEditingRecord(r); setRecordDialogOpen(true); }; @@ -119,7 +120,7 @@ export function CloudBilling() { setRuleDialogOpen(true); }; - const openEditRule = ( r: any) => { + const openEditRule = (r: JsonData) => { setEditingRule(r); setRuleDialogOpen(true); }; @@ -133,7 +134,7 @@ export function CloudBilling() { loadStats(); toast('删除成功'); } catch (e) { - toast.error('删除失败', { description: (e as Record).message }); + toast.error('删除失败', { description: (e as JsonData).message }); } }; @@ -145,7 +146,7 @@ export function CloudBilling() { loadRules(); toast('删除成功'); } catch (e) { - toast.error('删除失败', { description: (e as Record).message }); + toast.error('删除失败', { description: (e as JsonData).message }); } }; @@ -155,7 +156,7 @@ export function CloudBilling() { loadRules(); loadStats(); } catch (e) { - toast.error('操作失败', { description: (e as Record).message }); + toast.error('操作失败', { description: (e as JsonData).message }); } }; @@ -222,7 +223,7 @@ export function CloudBilling() { 成本预警
- {stats.alerts.map(( a: any, i: number) => ( + {stats.alerts.map((a: JsonData, i: number) => (
{a.rule_name} @@ -256,7 +257,7 @@ export function CloudBilling() { ) : (
- {stats.provider_dist.map(( p: any, i: number) => ( + {stats.provider_dist.map((p: JsonData, i: number) => (
- {stats.provider_dist.map(( p: any, i: number) => ( + {stats.provider_dist.map((p: JsonData, i: number) => (
diff --git a/frontend/src/components/cloud/DomainManager.tsx b/frontend/src/components/cloud/DomainManager.tsx index 46ca0e4..2d01424 100644 --- a/frontend/src/components/cloud/DomainManager.tsx +++ b/frontend/src/components/cloud/DomainManager.tsx @@ -25,9 +25,10 @@ import { Loader2, } from 'lucide-react'; import { cloudApi } from '@/api'; +import type { JsonData } from '@/types/common'; export function DomainManager() { - const [domains, setDomains] = useState([]); + const [domains, setDomains] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [selectedStatus, setSelectedStatus] = useState('all'); @@ -35,7 +36,7 @@ export function DomainManager() { // 创建/编辑域名表单状态 const [formOpen, setFormOpen] = useState(false); const [submitting, setSubmitting] = useState(false); - const [editingDomain, setEditingDomain] = useState(null); + const [editingDomain, setEditingDomain] = useState(null); const [formData, setFormData] = useState({ name: '', registrar: '', @@ -66,7 +67,7 @@ export function DomainManager() { setFormOpen(true); }; - const handleOpenEdit = ( domain: any) => { + const handleOpenEdit = (domain: JsonData) => { setEditingDomain(domain); // 后端返回 RFC3339 时间,截取 YYYY-MM-DD 部分给 date input const toDateInput = (v: string) => (v ? v.substring(0, 10) : ''); @@ -87,7 +88,7 @@ export function DomainManager() { if (!formData.name.trim()) return; try { setSubmitting(true); - const payload: Record = { + const payload: JsonData = { name: formData.name.trim(), registrar: formData.registrar.trim(), status: formData.status, @@ -114,7 +115,7 @@ export function DomainManager() { const loadData = async () => { try { setLoading(true); - const res: Record = await cloudApi.getDomains(); + const res: JsonData = await cloudApi.getDomains(); setDomains(res?.list || []); } catch (e) { console.error('加载域名列表失败:', e); diff --git a/frontend/src/components/cloud/PlatformManager.tsx b/frontend/src/components/cloud/PlatformManager.tsx index facd41b..d78c7ba 100644 --- a/frontend/src/components/cloud/PlatformManager.tsx +++ b/frontend/src/components/cloud/PlatformManager.tsx @@ -30,6 +30,7 @@ import { Loader2, } from 'lucide-react'; import { platformApi } from '@/api'; +import type { JsonData } from '@/types/common'; const typeLabels: Record = { public: '公有云', @@ -44,12 +45,12 @@ const typeColors: Record = { }; export function PlatformManager() { - const [platforms, setPlatforms] = useState([]); + const [platforms, setPlatforms] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [selectedType, setSelectedType] = useState('all'); const [dialogOpen, setDialogOpen] = useState(false); - const [editingItem, setEditingItem] = useState(null); + const [editingItem, setEditingItem] = useState(null); const [submitting, setSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', code: '', type: 'public', status: 'active', @@ -60,7 +61,7 @@ export function PlatformManager() { setLoading(true); try { const res = await platformApi.list(); - const items = (res as Record)?.list || []; + const items = (res as JsonData)?.list || []; setPlatforms(items); } catch { setPlatforms([]); @@ -84,7 +85,7 @@ export function PlatformManager() { setDialogOpen(true); }; - const openEdit = ( item: any) => { + const openEdit = (item: JsonData) => { setEditingItem(item); setFormData({ name: item.name || '', code: item.code || '', type: item.type || 'public', diff --git a/frontend/src/components/cloud/RegionManager.tsx b/frontend/src/components/cloud/RegionManager.tsx index aaa0d42..8d198d7 100644 --- a/frontend/src/components/cloud/RegionManager.tsx +++ b/frontend/src/components/cloud/RegionManager.tsx @@ -40,6 +40,7 @@ import { import { regionApi } from '@/api'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { toast } from 'sonner'; +import type { JsonData } from '@/types/common'; const providerNames: Record = { aws: 'AWS', @@ -59,13 +60,13 @@ const providerColors: Record = { export function RegionManager() { const confirm = useConfirm(); - const [regions, setRegions] = useState([]); + const [regions, setRegions] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [selectedProvider, setSelectedProvider] = useState('all'); const [selectedIds, setSelectedIds] = useState([]); const [dialogOpen, setDialogOpen] = useState(false); - const [editingItem, setEditingItem] = useState(null); + const [editingItem, setEditingItem] = useState(null); const [submitting, setSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', code: '', provider: 'aliyun', location: '', @@ -76,7 +77,7 @@ export function RegionManager() { setLoading(true); try { const res = await regionApi.list(); - const items = (res as Record)?.list || []; + const items = (res as JsonData)?.list || []; setRegions(items); } catch { setRegions([]); @@ -113,7 +114,7 @@ export function RegionManager() { setDialogOpen(true); }; - const openEdit = ( item: any) => { + const openEdit = (item: JsonData) => { setEditingItem(item); setFormData({ name: item.name || '', code: item.code || '', provider: item.provider || 'aliyun', diff --git a/frontend/src/components/dashboard/Dashboard.tsx b/frontend/src/components/dashboard/Dashboard.tsx index bac82a7..4f01ca1 100644 --- a/frontend/src/components/dashboard/Dashboard.tsx +++ b/frontend/src/components/dashboard/Dashboard.tsx @@ -14,6 +14,7 @@ import { KubernetesClusterCard, } from './DashboardRecent'; import { SkeletonDashboard } from '@/components/layout/SkeletonCard'; +import type { JsonData } from '@/types/common'; /** * Dashboard - 优化后的仪表盘页面 @@ -30,13 +31,13 @@ export function Dashboard() { const navigate = useNavigate(); const [initialLoading, setInitialLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); - const [stats, setStats] = useState(null); - const [servers, setServers] = useState([]); - const [clusters, setClusters] = useState([]); - const [alerts, setAlerts] = useState([]); - const [releases, setReleases] = useState([]); - const [expCerts, setExpCerts] = useState([]); - const [metrics, setMetrics] = useState([]); + const [stats, setStats] = useState(null); + const [servers, setServers] = useState([]); + const [clusters, setClusters] = useState([]); + const [alerts, setAlerts] = useState([]); + const [releases, setReleases] = useState([]); + const [expCerts, setExpCerts] = useState([]); + const [metrics, setMetrics] = useState([]); const refreshData = useCallback(async () => { setRefreshing(true); diff --git a/frontend/src/components/dashboard/DashboardCharts.tsx b/frontend/src/components/dashboard/DashboardCharts.tsx index 58be820..1fa7db2 100644 --- a/frontend/src/components/dashboard/DashboardCharts.tsx +++ b/frontend/src/components/dashboard/DashboardCharts.tsx @@ -16,6 +16,7 @@ import { } from 'recharts'; import { Cpu, HardDrive, Activity, MemoryStick, Database } from 'lucide-react'; import { cn } from '@/lib/utils'; +import type { JsonData } from '@/types/common'; // Cloud platform color mapping (theme-aware) const CLOUD_COLORS: Record = { @@ -32,7 +33,7 @@ const CLOUD_COLORS: Record = { }; const FALLBACK_CLOUD_COLORS = ['#ff6a00', '#ff9900', '#006eff', '#ff0000', '#0078d4', '#10b981', '#a855f7', '#ec4899']; -export function computeMonthChange( monthTrend: any[]): { +export function computeMonthChange( monthTrend: JsonData[]): { pct: number; label: string; direction: 'up' | 'down' | 'neutral'; @@ -65,7 +66,7 @@ function generateMetricsData(avgCpu: number, avgMemory: number, avgDisk: number, return data; } -function buildMetricsFromReal( metrics: any[], avgDisk: number) { +function buildMetricsFromReal( metrics: JsonData[], avgDisk: number) { if (!metrics || metrics.length === 0) return []; const buckets: Record = {}; for (const m of metrics) { @@ -88,12 +89,12 @@ function buildMetricsFromReal( metrics: any[], avgDisk: number) { } // Custom tooltip for charts (theme-aware) -function CustomTooltip({ active, payload, label }: any) { +function CustomTooltip({ active, payload, label }: JsonData) { if (!active || !payload) return null; return (

{label}

- {payload.map(( entry: any, index: number) => ( + {payload.map((entry: JsonData, index: number) => (
{entry.name}: @@ -106,9 +107,9 @@ function CustomTooltip({ active, payload, label }: any) { ); } -export function CostOverviewCard({ costStats }: { costStats: Record }) { - const cloudCosts: Record[] = costStats?.cloud_costs ?? []; - const monthTrend: Record[] = costStats?.month_trend ?? []; +export function CostOverviewCard({ costStats }: { costStats: JsonData }) { + const cloudCosts: JsonData[] = costStats?.cloud_costs ?? []; + const monthTrend: JsonData[] = costStats?.month_trend ?? []; const costData = cloudCosts.map((c, i) => ({ name: c.cloud, @@ -201,12 +202,12 @@ export function CostOverviewCard({ costStats }: { costStats: Record ); } -export function TopResourceUsageCard({ topUsage }: { topUsage: any}) { +export function TopResourceUsageCard({ topUsage }: { topUsage: JsonData}) { const topCpuServers = (topUsage?.cpu ?? []).slice(0, 5); const topMemoryServers = (topUsage?.memory ?? []).slice(0, 5); const topDiskServers = (topUsage?.disk ?? []).slice(0, 5); - const renderList = ( items: any[], + const renderList = ( items: JsonData[], color: string, BarColor: string, icon: React.ReactNode, @@ -280,7 +281,7 @@ export function ResourceUsageCard({ avgCpu: number; avgMemory: number; avgDisk: number; - metrics?: any[]; + metrics?: JsonData[]; }) { const realData = buildMetricsFromReal(metrics ?? [], avgDisk); const data = realData.length > 0 ? realData : generateMetricsData(avgCpu, avgMemory, avgDisk, 24); diff --git a/frontend/src/components/deploy/CredentialFormDialog.tsx b/frontend/src/components/deploy/CredentialFormDialog.tsx index 853eb65..aa33e7b 100644 --- a/frontend/src/components/deploy/CredentialFormDialog.tsx +++ b/frontend/src/components/deploy/CredentialFormDialog.tsx @@ -9,6 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Plus, Key, Globe, Container } from 'lucide-react'; import { toast } from 'sonner'; import { credentialApi, deployApi } from '@/api'; +import type { JsonData } from '@/types/common'; interface Repo { 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 () => { try { - const res: Record = await credentialApi.list({ page_size: 100 }); + const res: JsonData = await credentialApi.list({ page_size: 100 }); setCredentials(res?.list || []); } catch { /* silent */ } }; diff --git a/frontend/src/components/deploy/DeployCredentials.tsx b/frontend/src/components/deploy/DeployCredentials.tsx index d455b06..9b5d97e 100644 --- a/frontend/src/components/deploy/DeployCredentials.tsx +++ b/frontend/src/components/deploy/DeployCredentials.tsx @@ -12,6 +12,7 @@ import { useConfirm } from '@/components/ui/confirm-dialog'; import { deployApi } from '@/api'; import CredentialFormDialog from './CredentialFormDialog'; import ToolConfigTab from './ToolConfigTab'; +import type { JsonData } from '@/types/common'; interface Repo { id: number; name: string; description: string; url: string; @@ -110,16 +111,16 @@ export default function DeployCredentials() { setLoading(true); try { const [rR, rReg, rEnv] = await Promise.all([deployApi.getRepos(), deployApi.getRegistries(), deployApi.getEnvironments()]); - setRepos((rR as Record)?.list || []); - setRegistries((rReg as Record)?.list || []); - setEnvironments((rEnv as Record)?.list || []); + setRepos((rR as JsonData)?.list || []); + setRegistries((rReg as JsonData)?.list || []); + setEnvironments((rEnv as JsonData)?.list || []); } catch { toast.error('获取数据失败'); } finally { setLoading(false); } }; 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' }); if (!ok) return; try { diff --git a/frontend/src/components/deploy/DeployRBAC.tsx b/frontend/src/components/deploy/DeployRBAC.tsx index d5cfdaa..3d6e9a1 100644 --- a/frontend/src/components/deploy/DeployRBAC.tsx +++ b/frontend/src/components/deploy/DeployRBAC.tsx @@ -9,6 +9,7 @@ import { Label } from '@/components/ui/label'; import { toast } from 'sonner'; import { deployApi, userApi, pipelineApi } from '@/api'; import { useConfirm } from '@/components/ui/confirm-dialog'; +import type { JsonData } from '@/types/common'; interface ItemRole { id: number; name: string; code: string; pattern: string; role: string; @@ -27,11 +28,11 @@ export default function DeployRBAC() { const [saving, setSaving] = useState(false); // Pipeline list for live preview - const [pipelines, setPipelines] = useState([]); + const [pipelines, setPipelines] = useState([]); const [previewRoleId, setPreviewRoleId] = useState(null); // User assignment - const [users, setUsers] = useState([]); + const [users, setUsers] = useState([]); const [assignments, setAssignments] = useState([]); const [selectedUserId, setSelectedUserId] = useState(''); const [assignRoleId, setAssignRoleId] = useState(''); @@ -60,9 +61,9 @@ export default function DeployRBAC() { pipelineApi.getPipelines({ page_size: 200 }), userApi.getUsers({ page_size: 500 }), ]); - setRoles(Array.isArray(rolesRes) ? rolesRes : (rolesRes as Record)?.list || (rolesRes as Record)?.data || []); - setPipelines(Array.isArray(pipesRes) ? pipesRes : (pipesRes as Record)?.list || (pipesRes as Record)?.data || []); - const uList = Array.isArray(usersRes) ? usersRes : (usersRes as Record)?.list || (usersRes as Record)?.data || []; + setRoles(Array.isArray(rolesRes) ? rolesRes : (rolesRes as JsonData)?.list || (rolesRes as JsonData)?.data || []); + setPipelines(Array.isArray(pipesRes) ? pipesRes : (pipesRes as JsonData)?.list || (pipesRes as JsonData)?.data || []); + const uList = Array.isArray(usersRes) ? usersRes : (usersRes as JsonData)?.list || (usersRes as JsonData)?.data || []; setUsers(uList); } catch { /* */ } finally { setLoading(false); } @@ -73,7 +74,7 @@ export default function DeployRBAC() { const loadAssignments = async () => { if (!selectedUserId) { setAssignments([]); return; } try { - const res: Record = await deployApi.getUserItemRoles(Number(selectedUserId)); + const res: JsonData = await deployApi.getUserItemRoles(Number(selectedUserId)); setAssignments(Array.isArray(res) ? res : res?.data || res?.list || []); } catch { setAssignments([]); } }; diff --git a/frontend/src/components/deploy/PipelineDetailDialog.tsx b/frontend/src/components/deploy/PipelineDetailDialog.tsx index 310edb6..d7abed7 100644 --- a/frontend/src/components/deploy/PipelineDetailDialog.tsx +++ b/frontend/src/components/deploy/PipelineDetailDialog.tsx @@ -16,6 +16,7 @@ import { import { GitBranch, GitCommit, Clock, ChevronDown, Loader2 } from 'lucide-react'; import type { PipelineStage } from './pipeline-helpers'; import { parseStages, getStatusBadge, getStageStatusIcon } from './pipeline-helpers'; +import type { JsonData } from '@/types/common'; // 流水线详情弹窗(阶段/配置/历史三 Tab) // Logan.Li export function PipelineDetailDialog({ @@ -29,8 +30,8 @@ export function PipelineDetailDialog({ }: { open: boolean; onOpenChange: (open: boolean) => void; - selectedPipeline: any; - pipelineRuns: any[]; + selectedPipeline?: JsonData; + pipelineRuns: JsonData[]; loadingRuns: boolean; activeStage: string | null; setActiveStage: (id: string | null) => void; @@ -131,7 +132,7 @@ export function PipelineDetailDialog({
暂无执行记录
) : (
- {pipelineRuns.map(( run: any) => ( + {pipelineRuns.map((run: JsonData) => (
= { source: '拉取代码', build: '构建', deploy: '部署', test: '测试', custom: '自定义' }; -const taskIcons: Record = { 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 fmtDuration(s: number | undefined): string { @@ -119,7 +120,7 @@ function TaskEditor({ task, onUpdate }: { task: Task | null; onUpdate: (t: Task) } // ====== 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 ( @@ -160,16 +161,16 @@ function LogDialog({ open, onClose, run, logs }: { open: boolean; onClose: () => export default function PipelineEditor() { const { code } = useParams<{ code: string }>(); const pipelineCode = code || ''; - const [pipeline, setPipeline] = useState(null); + const [pipeline, setPipeline] = useState(null); const [stages, setStages] = useState([]); const [selectedTaskId, setSelectedTaskId] = useState(null); const [expandedStages, setExpandedStages] = useState>({}); const [activeTab, setActiveTab] = useState('flow'); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); - const [runs, setRuns] = useState([]); - const [environments, setEnvironments] = useState([]); - const [members, setMembers] = useState([]); + const [runs, setRuns] = useState([]); + const [environments, setEnvironments] = useState([]); + const [members, setMembers] = useState([]); const [loadingMembers, setLoadingMembers] = useState(false); // trigger config @@ -186,7 +187,7 @@ export default function PipelineEditor() { // log dialog const [logOpen, setLogOpen] = useState(false); - const [logRun, setLogRun] = useState(null); + const [logRun, setLogRun] = useState(null); const [logs, setLogs] = useState([]); const _confirm = useConfirm(); @@ -194,7 +195,7 @@ export default function PipelineEditor() { const loadEnvironments = useCallback(async () => { try { const res = await deployApi.getEnvironments(); - setEnvironments(Array.isArray(res) ? res : (res as Record)?.list || []); + setEnvironments(Array.isArray(res) ? res : (res as JsonData)?.list || []); } catch { /* silent */ } }, []); @@ -202,7 +203,7 @@ export default function PipelineEditor() { setLoadingMembers(true); try { const res = await pipelineApi.getMembers(pipelineCode); - setMembers(Array.isArray(res) ? res : (res as Record)?.data || []); + setMembers(Array.isArray(res) ? res : (res as JsonData)?.data || []); } catch { setMembers([]); } finally { setLoadingMembers(false); } }, [pipelineCode]); @@ -210,7 +211,7 @@ export default function PipelineEditor() { const loadRuns = useCallback(async () => { try { const res = await pipelineApi.getRuns(pipelineCode); - setRuns(Array.isArray(res) ? res : (res as Record)?.list || []); + setRuns(Array.isArray(res) ? res : (res as JsonData)?.list || []); } catch { setRuns([]); } }, [pipelineCode]); @@ -218,7 +219,7 @@ export default function PipelineEditor() { (async () => { setLoading(true); try { - const p = await pipelineApi.getPipelineDetail(pipelineCode) as Record; + const p = await pipelineApi.getPipelineDetail(pipelineCode) as JsonData; // http.get 已自动解包 json.data,p 即为 pipeline 对象 if (!p || !p.id) { setPipeline(null); setLoading(false); return; } setPipeline(p); @@ -294,11 +295,11 @@ export default function PipelineEditor() { } catch { toast.error('停止失败'); } }; - const handleViewLog = async ( run: any) => { + const handleViewLog = async (run: JsonData) => { setLogRun(run); setLogOpen(true); try { - const data = await pipelineApi.getRunLog(pipelineCode, run.id) as Record; + const data = await pipelineApi.getRunLog(pipelineCode, run.id) as JsonData; setLogs(data?.logs || []); } catch { setLogs([]); } }; @@ -482,7 +483,7 @@ export default function PipelineEditor() { - {runs.map(( r: any) => ( + {runs.map((r: JsonData) => ( #{r.build_number || r.id} @@ -552,8 +553,8 @@ export default function PipelineEditor() { {/* Members list — split direct users vs group assignments */} {(() => { - const directUsers = members.filter(( m: any) => m.username); - const groupAssignments = members.filter(( m: any) => m.group_id); + const directUsers = members.filter((m: JsonData) => m.username); + const groupAssignments = members.filter((m: JsonData) => m.group_id); return ( <> {/* Direct users */} @@ -568,7 +569,7 @@ export default function PipelineEditor() {
暂无直接用户
) : (
- {directUsers.map(( m: any) => ( + {directUsers.map((m: JsonData) => (
@@ -597,7 +598,7 @@ export default function PipelineEditor() {
) : (
- {groupAssignments.map(( m: any) => ( + {groupAssignments.map((m: JsonData) => (
diff --git a/frontend/src/components/deploy/PipelineRunDetail.tsx b/frontend/src/components/deploy/PipelineRunDetail.tsx index e1331e9..6259bf5 100644 --- a/frontend/src/components/deploy/PipelineRunDetail.tsx +++ b/frontend/src/components/deploy/PipelineRunDetail.tsx @@ -12,6 +12,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { toast } from 'sonner'; import { pipelineApi } from '@/api'; import { FileBrowserDialog } from './FileBrowserDialog'; +import type { JsonData } from '@/types/common'; // ═══════════ 阶段定义 (与 PipelineManager 共用) ═══════════ const STAGE_DEFS = [ @@ -61,7 +62,7 @@ const STATUS_BADGES: Record = { cancelled: 'bg-amber-500/20 text-amber-400 border-amber-500/30', }; -const STATUS_ICONS: Record = { +const STATUS_ICONS: JsonData = { success: CheckCircle2, failed: XCircle, running: Loader2, @@ -101,8 +102,8 @@ function getRunStageStats(run: PipelineRun | null): Map { // 如果 run.stages 有数据,直接用 const statuses = parseStageStatuses(run.stages); statuses.forEach(s => { - if (s.name || (s as Record).type) { - const key = (s as Record).type || (STAGE_DEFS.find(d => d.label === s.name)?.key); + if (s.name || (s as JsonData).type) { + const key = (s as JsonData).type || (STAGE_DEFS.find(d => d.label === s.name)?.key); if (key) map.set(key, s.status); } }); @@ -128,7 +129,7 @@ function getStageLogSummary(logs: LogEntry[], stageKey: string): string { export default function PipelineRunDetail() { const { code: pipelineCode } = useParams<{ code: string }>(); - const [pipeline, setPipeline] = useState(null); + const [pipeline, setPipeline] = useState(null); const [runs, setRuns] = useState([]); const [selectedRun, setSelectedRun] = useState(null); const [logs, setLogs] = useState([]); @@ -147,7 +148,7 @@ export default function PipelineRunDetail() { // 加载流水线详情 const fetchPipeline = useCallback(async () => { try { - const data: Record = await pipelineApi.getByCode(pipelineCode); + const data: JsonData = await pipelineApi.getByCode(pipelineCode); setPipeline(data?.data || data); } catch { /* silent, pipeline info optional */ } }, [pipelineCode]); @@ -156,7 +157,7 @@ export default function PipelineRunDetail() { const fetchRuns = useCallback(async () => { setLoading(true); try { - const data: Record = await pipelineApi.getRunsByCode(pipelineCode, pageSize); + const data: JsonData = await pipelineApi.getRunsByCode(pipelineCode, pageSize); const list = data?.list || []; setRuns(list); if (!selectedRun && list.length > 0) { @@ -180,7 +181,7 @@ export default function PipelineRunDetail() { // 加载日志 const fetchLogs = useCallback(async (run: PipelineRun) => { try { - const data: Record = await pipelineApi.getRunLogStream(run.id); + const data: JsonData = await pipelineApi.getRunLogStream(run.id); if (data?.logs) { const parsed = typeof data.logs === 'string' ? JSON.parse(data.logs) : data.logs; setLogs(Array.isArray(parsed) ? parsed : []); diff --git a/frontend/src/components/deploy/PipelineRunDialog.tsx b/frontend/src/components/deploy/PipelineRunDialog.tsx index 65767b3..988bae1 100644 --- a/frontend/src/components/deploy/PipelineRunDialog.tsx +++ b/frontend/src/components/deploy/PipelineRunDialog.tsx @@ -11,6 +11,7 @@ import { import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { Play } from 'lucide-react'; +import type { JsonData } from '@/types/common'; // 执行流水线弹窗(分支/Commit/环境变量) // Logan.Li export function PipelineRunDialog({ @@ -21,7 +22,7 @@ export function PipelineRunDialog({ }: { open: boolean; onOpenChange: (open: boolean) => void; - selectedPipeline: any; + selectedPipeline?: JsonData; onRun: () => void; }) { return ( diff --git a/frontend/src/components/deploy/ReleaseApproveDialog.tsx b/frontend/src/components/deploy/ReleaseApproveDialog.tsx index 9cd2d13..fd13455 100644 --- a/frontend/src/components/deploy/ReleaseApproveDialog.tsx +++ b/frontend/src/components/deploy/ReleaseApproveDialog.tsx @@ -12,9 +12,10 @@ import { Button } from '@/components/ui/button'; import { AlertCircle, X, Check, Loader2 } from 'lucide-react'; import { releaseApi } from '@/api'; import { toast } from 'sonner'; +import type { JsonData } from '@/types/common'; interface ReleaseApproveDialogProps { - release: any | null; + release: JsonData | null; open: boolean; onOpenChange: (open: boolean) => void; onSuccess?: () => void; diff --git a/frontend/src/components/deploy/ReleaseDetailDialog.tsx b/frontend/src/components/deploy/ReleaseDetailDialog.tsx index f491ac6..2e4c2ec 100644 --- a/frontend/src/components/deploy/ReleaseDetailDialog.tsx +++ b/frontend/src/components/deploy/ReleaseDetailDialog.tsx @@ -15,9 +15,10 @@ import { Rocket, CheckCircle, Loader2, RotateCcw } from 'lucide-react'; import { releaseApi } from '@/api'; import { toast } from 'sonner'; import { PIPELINE_STEPS } from './releaseHelpers'; +import type { JsonData } from '@/types/common'; interface ReleaseDetailDialogProps { - release: any | null; + release: JsonData | null; open: boolean; onOpenChange: (open: boolean) => void; onSuccess?: () => void; diff --git a/frontend/src/components/deploy/ToolConfigTab.tsx b/frontend/src/components/deploy/ToolConfigTab.tsx index 5d93d61..7750aca 100644 --- a/frontend/src/components/deploy/ToolConfigTab.tsx +++ b/frontend/src/components/deploy/ToolConfigTab.tsx @@ -6,6 +6,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; import { toast } from 'sonner'; import { deployApi } from '@/api'; +import type { JsonData } from '@/types/common'; interface ToolConfig { id: number; name: string; display_name: string; description: string; @@ -55,8 +56,8 @@ export default function ToolConfigTab() { const fetchTools = async () => { setLoading(true); try { - const res: Record = await deployApi.getTools(); - setTools((res as Record)?.list || []); + const res: JsonData = await deployApi.getTools(); + setTools((res as JsonData)?.list || []); } catch { toast.error('获取工具配置失败'); } finally { setLoading(false); } }; @@ -65,8 +66,8 @@ export default function ToolConfigTab() { const openVersionDialog = async (tool: ToolConfig) => { try { - const res: Record = await deployApi.getToolVersions(tool.id); - const versions = (res as Record)?.versions || []; + const res: JsonData = await deployApi.getToolVersions(tool.id); + const versions = (res as JsonData)?.versions || []; if (versions.length === 0) { // No versions — install directly handleInstall(tool); @@ -99,7 +100,7 @@ export default function ToolConfigTab() { const handleCheckStatus = async (tool: ToolConfig) => { try { - const res: Record = await deployApi.checkTool(tool.id); + const res: JsonData = await deployApi.checkTool(tool.id); toast.success(res?.available ? `${tool.display_name} 可用` : `${tool.display_name} 不可用`); fetchTools(); } catch { toast.error('检测失败'); } @@ -119,9 +120,9 @@ export default function ToolConfigTab() { const start = Date.now(); try { // Simulate a quick reachability test by fetching the version URL - const res: Record = await deployApi.getToolVersions(tool.id); + const res: JsonData = await deployApi.getToolVersions(tool.id); const elapsed = ((Date.now() - start) / 1000).toFixed(1); - const available = (res as Record)?.versions?.length > 0; + const available = (res as JsonData)?.versions?.length > 0; if (available) { toast.success(`${tool.display_name} 镜像可访问 (${elapsed}s)`); } else { diff --git a/frontend/src/components/deploy/UserGroupsManager.tsx b/frontend/src/components/deploy/UserGroupsManager.tsx index 5d2dbe3..700bd67 100644 --- a/frontend/src/components/deploy/UserGroupsManager.tsx +++ b/frontend/src/components/deploy/UserGroupsManager.tsx @@ -8,6 +8,7 @@ import { Label } from '@/components/ui/label'; import { toast } from 'sonner'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { deployApi } from '@/api'; +import type { JsonData } from '@/types/common'; interface Group { id: number; name: string; description: string; @@ -35,7 +36,7 @@ export default function UserGroupsManager() { const loadGroups = async () => { setLoading(true); - try { const res = await deployApi.getGroups(); setGroups((res as Record)?.list || []); } + try { const res = await deployApi.getGroups(); setGroups((res as JsonData)?.list || []); } catch { toast.error('加载失败'); } finally { setLoading(false); } }; diff --git a/frontend/src/components/deploy/WorkspaceTermDialog.tsx b/frontend/src/components/deploy/WorkspaceTermDialog.tsx index efb11d4..79e691a 100644 --- a/frontend/src/components/deploy/WorkspaceTermDialog.tsx +++ b/frontend/src/components/deploy/WorkspaceTermDialog.tsx @@ -12,6 +12,7 @@ import { Maximize2, Minimize2, X, FolderOpen } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useUserStore } from '@/store'; import { pipelineApi } from '@/api'; +import type { JsonData } from '@/types/common'; interface WorkspaceTermDialogProps { open: boolean; @@ -41,7 +42,7 @@ export function WorkspaceTermDialog({ useEffect(() => { if (!open || !runId) return; pipelineApi.getWorkspace(runId) - .then(( data: any) => setWorkspace(data?.workspace_path || '')) + .then((data: JsonData) => setWorkspace(data?.workspace_path || '')) .catch(() => {}); }, [open, runId]); diff --git a/frontend/src/components/deploy/pipeline-helpers.tsx b/frontend/src/components/deploy/pipeline-helpers.tsx index 14650ac..d3da0bc 100644 --- a/frontend/src/components/deploy/pipeline-helpers.tsx +++ b/frontend/src/components/deploy/pipeline-helpers.tsx @@ -1,5 +1,6 @@ import { Badge } from '@/components/ui/badge'; import { CheckCircle, XCircle, Loader2 } from 'lucide-react'; +import type { JsonData } from '@/types/common'; // 流水线阶段定义 // Logan.Li export interface PipelineStage { @@ -19,7 +20,7 @@ export const pipelineTemplates = [ ]; // 解析 stages 字段(兼容 JSON 数组 / 逗号分隔字符串) // Logan.Li -export function parseStages( stages: any): PipelineStage[] { +export function parseStages( stages: string): PipelineStage[] { if (Array.isArray(stages)) return stages; if (typeof stages === 'string') { try { @@ -42,7 +43,7 @@ export function nameToCode(name: string): string { } // 状态徽章(使用后端 last_status 字段) // Logan.Li -export function getStatusBadge( status: any) { +export function getStatusBadge( status: string) { switch (status) { case 'success': return ( diff --git a/frontend/src/components/deploy/pipeline-manager/PipelineCard.tsx b/frontend/src/components/deploy/pipeline-manager/PipelineCard.tsx index a1aa1e0..356f85f 100644 --- a/frontend/src/components/deploy/pipeline-manager/PipelineCard.tsx +++ b/frontend/src/components/deploy/pipeline-manager/PipelineCard.tsx @@ -70,7 +70,7 @@ export function PipelineCard({ #{String(pipeline.run_count)} )} - {getStatusBadge(pipeline.last_status)} + {getStatusBadge(String(pipeline.last_status))} {environment && ( = { dev: 'border-slate-500/30 text-slate-400 bg-slate-500/10', test: 'border-blue-500/30 text-blue-400 bg-blue-500/10', diff --git a/frontend/src/components/deploy/releaseManagerHelpers.tsx b/frontend/src/components/deploy/releaseManagerHelpers.tsx index 7b6023a..246c65c 100644 --- a/frontend/src/components/deploy/releaseManagerHelpers.tsx +++ b/frontend/src/components/deploy/releaseManagerHelpers.tsx @@ -1,6 +1,7 @@ // 发布管理(审批流)共享工具函数与常量 // Logan.Li // 注意:本文件专用于 ReleaseManager(含审批流,状态标签与 AppRelease 的 releaseHelpers 不同) import { Badge } from '@/components/ui/badge'; +import type { JsonData } from '@/types/common'; import { Clock, Check, @@ -19,7 +20,7 @@ export const ENVIRONMENTS: Record = { }; // 发布状态徽章(审批流:待审批/已审批/已驳回/发布中/发布成功/发布失败/已回滚) // Logan.Li -export function getStatusBadge( status: any) { +export function getStatusBadge( status: string) { const map: Record = { 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' }, @@ -41,7 +42,7 @@ export function getStatusBadge( status: any) { } // 环境徽章 // Logan.Li -export function getEnvironmentBadge( env: any) { +export function getEnvironmentBadge( env: string) { const cfg = ENVIRONMENTS[env]; return ( diff --git a/frontend/src/components/kubernetes/CapacityPage.tsx b/frontend/src/components/kubernetes/CapacityPage.tsx index 5d2f36e..3fde48d 100644 --- a/frontend/src/components/kubernetes/CapacityPage.tsx +++ b/frontend/src/components/kubernetes/CapacityPage.tsx @@ -11,19 +11,20 @@ import { } from '@/components/ui/table'; import { RefreshCw, ChevronRight, BarChart3, Download, Cpu, HardDrive } from 'lucide-react'; import { kubernetesApi } from '@/api'; +import type { JsonData } from '@/types/common'; export default function CapacityPage() { const { clusterCode } = useParams<{ clusterCode: string }>(); const navigate = useNavigate(); const [loading, setLoading] = useState(false); - const [capacity, setCapacity] = useState([]); - const [clusters, setClusters] = useState([]); + const [capacity, setCapacity] = useState([]); + const [clusters, setClusters] = useState([]); const [namespaces, setNamespaces] = useState([]); const [selectedNs, setSelectedNs] = useState('all'); const [activeCluster, setActiveCluster] = useState(clusterCode || ''); useEffect(() => { - kubernetesApi.getClusters().then(( res: any) => { + kubernetesApi.getClusters().then((res: JsonData) => { const list = res?.list || []; setClusters(list); if (!activeCluster && list.length > 0) { @@ -36,7 +37,7 @@ export default function CapacityPage() { useEffect(() => { if (!activeCluster) return; - kubernetesApi.getNamespaces(activeCluster).then(( res: any) => { + kubernetesApi.getNamespaces(activeCluster).then((res: JsonData) => { setNamespaces(Array.isArray(res) ? res : (res?.list || res?.data || [])); }).catch(() => {}); }, [activeCluster]); @@ -46,7 +47,7 @@ export default function CapacityPage() { setLoading(true); try { const ns = selectedNs === 'all' ? undefined : selectedNs; - const res: Record = await kubernetesApi.getCapacity(activeCluster, ns); + const res: JsonData = await kubernetesApi.getCapacity(activeCluster, ns); setCapacity(Array.isArray(res) ? res : (res?.list || [])); } catch { setCapacity([]); } finally { setLoading(false); } }, [activeCluster, selectedNs]); @@ -54,8 +55,8 @@ export default function CapacityPage() { useEffect(() => { loadCapacity(); }, [loadCapacity]); const rows = useMemo(() => - capacity.flatMap(( d: any) => - (d.containers || []).map(( c: any) => ({ + capacity.flatMap((d: JsonData) => + (d.containers || []).map((c: JsonData) => ({ namespace: d.namespace, deployment: d.name, replicas: d.replicas, @@ -83,7 +84,7 @@ export default function CapacityPage() { { setActiveCluster(v); navigate(`/kubernetes/${v}/events`); }}> - {clusters.map((c: any) => ( + {clusters.map((c: JsonData) => ( {c.name || c.code} ))} @@ -260,7 +261,7 @@ export default function EventsPage() { 最新告警 ({summary.recent_warnings.length})
- {summary.recent_warnings.map((w: any, i: number) => ( + {summary.recent_warnings.map((w: JsonData, i: number) => (
{w.reason} diff --git a/frontend/src/components/kubernetes/K8sShortcutRedirect.tsx b/frontend/src/components/kubernetes/K8sShortcutRedirect.tsx index 4d4371c..5c9e276 100644 --- a/frontend/src/components/kubernetes/K8sShortcutRedirect.tsx +++ b/frontend/src/components/kubernetes/K8sShortcutRedirect.tsx @@ -3,6 +3,7 @@ import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { Loader2 } from 'lucide-react'; import { kubernetesApi } from '@/api'; +import type { JsonData } from '@/types/common'; interface Props { /** 集群下路径,如 events、namespace/default/deployments */ @@ -14,7 +15,7 @@ export default function K8sShortcutRedirect({ target }: Props) { useEffect(() => { kubernetesApi.getClusters() - .then((res: any) => { + .then((res: JsonData) => { const list = res?.list || []; if (list.length > 0) { const code = list[0].code || String(list[0].id); diff --git a/frontend/src/components/kubernetes/KubernetesClusterDetail.tsx b/frontend/src/components/kubernetes/KubernetesClusterDetail.tsx index e648e25..c48b91b 100644 --- a/frontend/src/components/kubernetes/KubernetesClusterDetail.tsx +++ b/frontend/src/components/kubernetes/KubernetesClusterDetail.tsx @@ -14,17 +14,18 @@ import { Tooltip, ResponsiveContainer, } from 'recharts'; +import type { JsonData } from '@/types/common'; // 资源使用趋势数据 // 由于当前无持久化时序存储,这里基于当前节点 metrics 构造一个简短的模拟趋势(最近几分钟变化) // 真正历史数据需要后端记录指标快照后才能实现 // Logan.Li -export function KubernetesClusterDetail({ cluster, onClose }: { cluster: any; onClose: () => void }) { - const [nodes, setNodes] = useState([]); - const [pods, setPods] = useState([]); - const [deployments, setDeployments] = useState([]); - const [services, setServices] = useState([]); - const [ingresses, setIngresses] = useState([]); +export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonData; onClose: () => void }) { + const [nodes, setNodes] = useState([]); + const [pods, setPods] = useState([]); + const [deployments, setDeployments] = useState([]); + const [services, setServices] = useState([]); + const [ingresses, setIngresses] = useState([]); // 从节点数据计算集群级 CPU/内存使用率(实时快照) // Logan.Li const clusterMetrics = useMemo(() => { @@ -76,7 +77,7 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster: any; on kubernetesApi.getServices(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)); setPods(toArray(podsRes)); setDeployments(toArray(depRes)); diff --git a/frontend/src/components/kubernetes/KubernetesClusterDialog.tsx b/frontend/src/components/kubernetes/KubernetesClusterDialog.tsx index 02cf67a..b334686 100644 --- a/frontend/src/components/kubernetes/KubernetesClusterDialog.tsx +++ b/frontend/src/components/kubernetes/KubernetesClusterDialog.tsx @@ -14,6 +14,7 @@ import { } from '@/components/ui/dialog'; import { kubernetesApi } from '@/api'; import { toast } from 'sonner'; +import type { JsonData } from '@/types/common'; // 名称即 code,只允许英文/数字/连字符 const NAME_RE = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/; @@ -36,7 +37,7 @@ export function KubernetesClusterDialog({ }: { open: boolean; onOpenChange: (open: boolean) => void; - editingCluster: any; + editingCluster?: JsonData; onSaved: () => void; }) { const [formData, setFormData] = useState({ ...emptyForm }); @@ -48,7 +49,7 @@ export function KubernetesClusterDialog({ useEffect(() => { if (!open) return; if (editingCluster && editingCluster.id) { - kubernetesApi.getClusterDetail(editingCluster.id).then(( res: any) => { + kubernetesApi.getClusterDetail(editingCluster.id).then((res: JsonData) => { const d = res || {}; setHasToken(!!d.has_token); setHasKubeConfig(!!d.has_kube_config); diff --git a/frontend/src/components/kubernetes/KubernetesClusters.tsx b/frontend/src/components/kubernetes/KubernetesClusters.tsx index b11e8bc..f368fed 100644 --- a/frontend/src/components/kubernetes/KubernetesClusters.tsx +++ b/frontend/src/components/kubernetes/KubernetesClusters.tsx @@ -34,29 +34,30 @@ import type { KubernetesCluster } from '@/types'; import { KubernetesClusterDetail } from './KubernetesClusterDetail'; import { KubernetesClusterDialog } from './KubernetesClusterDialog'; import { KubectlTerminal } from './KubectlTerminal'; +import type { JsonData } from '@/types/common'; export function KubernetesClusters() { - const [clusters, setClusters] = useState([]); + const [clusters, setClusters] = useState([]); const [loading, setLoading] = useState(true); const [selectedCluster, setSelectedCluster] = useState(null); const [searchQuery, setSearchQuery] = useState(''); // 创建/编辑集群弹窗 const [dialogOpen, setDialogOpen] = useState(false); - const [editingCluster, setEditingCluster] = useState(null); + const [editingCluster, setEditingCluster] = useState(null); // kubectl 终端 const [kubectlCluster, setKubectlCluster] = useState<{id: number; name: string} | null>(null); const [kubectlTerminalOpen, setKubectlTerminalOpen] = useState(false); // 导出资源弹窗 - const [exportCluster, setExportCluster] = useState(null); + const [exportCluster, setExportCluster] = useState(null); const [exporting, setExporting] = useState(false); const loadData = async () => { try { setLoading(true); - const res: Record = await kubernetesApi.getClusters(); + const res: JsonData = await kubernetesApi.getClusters(); setClusters(res?.list || []); } catch (e) { console.error('加载集群列表失败:', e); @@ -78,7 +79,7 @@ export function KubernetesClusters() { if (!editingCluster) { // 新建集群后,等列表刷新完再取最新一条同步 setTimeout(async () => { - const res: Record = await kubernetesApi.getClusters(); + const res: JsonData = await kubernetesApi.getClusters(); const list = res?.list || []; if (list.length > 0) { handleSync(list[list.length - 1].id, true); @@ -100,7 +101,7 @@ export function KubernetesClusters() { const handleSync = async (id: number | string, silent?: boolean) => { try { if (!silent) toast.info('正在同步集群资源...'); - const res: Record = await kubernetesApi.syncCluster(id); + const res: JsonData = await kubernetesApi.syncCluster(id); if (!silent) toast.success(res?.message || '同步完成'); loadData(); } catch (e) { diff --git a/frontend/src/components/kubernetes/KubernetesNodes.tsx b/frontend/src/components/kubernetes/KubernetesNodes.tsx index b8dc55d..602c132 100644 --- a/frontend/src/components/kubernetes/KubernetesNodes.tsx +++ b/frontend/src/components/kubernetes/KubernetesNodes.tsx @@ -11,18 +11,19 @@ import { } from '@/components/ui/table'; import { RefreshCw, Search, Server, Loader2 } from 'lucide-react'; import { kubernetesApi } from '@/api'; +import type { JsonData } from '@/types/common'; export function KubernetesNodes() { - const [clusters, setClusters] = useState([]); + const [clusters, setClusters] = useState([]); const [selectedCluster, setSelectedCluster] = useState(''); - const [nodes, setNodes] = useState([]); + const [nodes, setNodes] = useState([]); const [loading, setLoading] = useState(false); const [search, setSearch] = useState(''); const [roleFilter, setRoleFilter] = useState('all'); const loadClusters = useCallback(async () => { try { - const res: Record = await kubernetesApi.getClusters(); + const res: JsonData = await kubernetesApi.getClusters(); const list = res?.list || []; setClusters(list); if (list.length > 0 && !selectedCluster) setSelectedCluster(String(list[0].id)); @@ -35,7 +36,7 @@ export function KubernetesNodes() { if (!selectedCluster) return; setLoading(true); try { - const res: Record = await kubernetesApi.getNodes(selectedCluster); + const res: JsonData = await kubernetesApi.getNodes(selectedCluster); setNodes(Array.isArray(res) ? res : (res?.list || [])); } catch { setNodes([]); } finally { setLoading(false); } }, [selectedCluster]); diff --git a/frontend/src/components/kubernetes/KubernetesWorkloads.tsx b/frontend/src/components/kubernetes/KubernetesWorkloads.tsx index 5537b66..f0ae932 100644 --- a/frontend/src/components/kubernetes/KubernetesWorkloads.tsx +++ b/frontend/src/components/kubernetes/KubernetesWorkloads.tsx @@ -25,15 +25,16 @@ import { import { kubernetesApi } from '@/api'; import { useToast } from '@/hooks/use-toast'; import { DeploymentDetail } from './DeploymentDetail'; +import type { JsonData } from '@/types/common'; export function KubernetesWorkloads() { const { toast } = useToast(); - const [clusters, setClusters] = useState([]); + const [clusters, setClusters] = useState([]); const [selectedCluster, setSelectedCluster] = useState(''); const [namespaces, setNamespaces] = useState([]); const [selectedNamespace, setSelectedNamespace] = useState('all'); - const [deployments, setDeployments] = useState([]); - const [pods, setPods] = useState([]); + const [deployments, setDeployments] = useState([]); + const [pods, setPods] = useState([]); const [loading, setLoading] = useState(false); const [search, setSearch] = useState(''); const [activeTab, setActiveTab] = useState('deployments'); @@ -44,7 +45,7 @@ export function KubernetesWorkloads() { const loadClusters = useCallback(async () => { try { - const res: Record = await kubernetesApi.getClusters(); + const res: JsonData = await kubernetesApi.getClusters(); const list = res?.list || []; setClusters(list); if (list.length > 0 && !selectedCluster) setSelectedCluster(list[0].code || String(list[0].id)); @@ -57,7 +58,7 @@ export function KubernetesWorkloads() { const loadNamespaces = useCallback(async () => { if (!selectedCluster) return; try { - const res: Record = await kubernetesApi.getNamespaces(selectedCluster); + const res: JsonData = await kubernetesApi.getNamespaces(selectedCluster); const nsList: string[] = Array.isArray(res) ? res : (res?.list || res?.data || []); setNamespaces(nsList); } catch { setNamespaces([]); } @@ -75,7 +76,7 @@ export function KubernetesWorkloads() { kubernetesApi.getNamespaceDeployments(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)); setPods(toArray(podRes)); } else { @@ -83,7 +84,7 @@ export function KubernetesWorkloads() { kubernetesApi.getAllDeployments(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)); setPods(toArray(podRes)); } @@ -111,7 +112,7 @@ export function KubernetesWorkloads() { return {status || 'Unknown'}; }; - const handleQuickScale = async ( dep: any, delta: number) => { + const handleQuickScale = async (dep: JsonData, delta: number) => { const key = `scale-${dep.name}`; setActionLoading((prev) => ({ ...prev, [key]: 'loading' })); try { diff --git a/frontend/src/components/kubernetes/NamespaceOverviewPage.tsx b/frontend/src/components/kubernetes/NamespaceOverviewPage.tsx index a8b8070..e5c69fc 100644 --- a/frontend/src/components/kubernetes/NamespaceOverviewPage.tsx +++ b/frontend/src/components/kubernetes/NamespaceOverviewPage.tsx @@ -14,6 +14,7 @@ import { } from 'lucide-react'; import { kubernetesApi } from '@/api'; +import type { JsonData } from '@/types/common'; const RESOURCE_LINKS = [ { type: 'deployments', label: 'Deployments', icon: Boxes, desc: '工作负载副本管理' }, @@ -45,7 +46,7 @@ export default function NamespaceOverviewPage() { kubernetesApi.getHPAs(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({ deployments: toArr(results[0].status === 'fulfilled' ? results[0].value : []).length, pods: toArr(results[1].status === 'fulfilled' ? results[1].value : []).length, diff --git a/frontend/src/components/kubernetes/PodFileBrowser.tsx b/frontend/src/components/kubernetes/PodFileBrowser.tsx index 0a16b0e..9b82c69 100644 --- a/frontend/src/components/kubernetes/PodFileBrowser.tsx +++ b/frontend/src/components/kubernetes/PodFileBrowser.tsx @@ -17,6 +17,7 @@ import { ArrowLeft, HardDrive, } from 'lucide-react'; +import type { JsonData } from '@/types/common'; export interface PodFileBrowserProps { @@ -54,10 +55,10 @@ export function PodFileBrowser({ setError(''); setPreview(null); try { - const res: Record = await kubernetesApi.listPodFiles(clusterCode, namespace, podName, path, container || undefined); + const res: JsonData = await kubernetesApi.listPodFiles(clusterCode, namespace, podName, path, container || undefined); // 兼容后端 {files: [...]} 和 {entries: [...]} 两种返回格式 - const raw: Record[] = res?.entries || res?.files || res?.data?.entries || res?.data?.files || (Array.isArray(res) ? res : []); - const list: FileEntry[] = raw.map(( it: any) => ({ + const raw: JsonData[] = res?.entries || res?.files || res?.data?.entries || res?.data?.files || (Array.isArray(res) ? res : []); + const list: FileEntry[] = raw.map((it: JsonData) => ({ name: it.name ?? it.Name ?? '', 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), @@ -66,7 +67,7 @@ export function PodFileBrowser({ setEntries(list); setCurrentPath(path); } catch (e) { - setError((e as Record).message || '加载文件列表失败'); + setError((e as JsonData).message || '加载文件列表失败'); setEntries([]); } finally { setLoading(false); @@ -83,7 +84,7 @@ export function PodFileBrowser({ } setLoading(true); try { - const res: Record = 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 workdir = stdout && stdout.startsWith('/') ? stdout : '/'; workdirDetected.current = true; @@ -116,7 +117,7 @@ export function PodFileBrowser({ const text = await kubernetesApi.readPodFileText(clusterCode, namespace, podName, filePath, container || undefined); setPreview({ file: fileName, content: text }); } catch (e) { - setPreview({ file: fileName, content: `读取失败: ${(e as Record).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); loadFiles(currentPath); } catch (err) { - setError(`上传失败: ${(err as Record).message}`); + setError(`上传失败: ${(err as JsonData).message}`); } finally { setUploading(false); if (fileInputRef.current) fileInputRef.current.value = ''; diff --git a/frontend/src/components/kubernetes/PodLogsViewer.tsx b/frontend/src/components/kubernetes/PodLogsViewer.tsx index 4920853..dcadcc1 100644 --- a/frontend/src/components/kubernetes/PodLogsViewer.tsx +++ b/frontend/src/components/kubernetes/PodLogsViewer.tsx @@ -21,6 +21,7 @@ import { import { cn } from '@/lib/utils'; import { kubernetesApi } from '@/api'; import { useToast } from '@/hooks/use-toast'; +import type { JsonData } from '@/types/common'; /** * 增强版 Pod 日志查看器 — 增强 DeploymentDetail 的日志体验 @@ -153,7 +154,7 @@ export function PodLogsViewer({ linesRef.current = []; setLines([]); try { - const res: Record = await kubernetesApi.getPodLogsEnhanced(clusterCode, namespace, podName, { + const res: JsonData = await kubernetesApi.getPodLogsEnhanced(clusterCode, namespace, podName, { container: container || undefined, tailLines: tailLines, previous, diff --git a/frontend/src/components/kubernetes/PodMetricsPage.tsx b/frontend/src/components/kubernetes/PodMetricsPage.tsx index 6ef5165..66d3973 100644 --- a/frontend/src/components/kubernetes/PodMetricsPage.tsx +++ b/frontend/src/components/kubernetes/PodMetricsPage.tsx @@ -11,19 +11,20 @@ import { } from '@/components/ui/table'; import { Cpu, HardDrive, RefreshCw, ChevronRight, Activity } from 'lucide-react'; import { kubernetesApi } from '@/api'; +import type { JsonData } from '@/types/common'; export default function PodMetricsPage() { const { clusterCode } = useParams<{ clusterCode: string }>(); const navigate = useNavigate(); const [loading, setLoading] = useState(false); - const [metrics, setMetrics] = useState([]); - const [clusters, setClusters] = useState([]); + const [metrics, setMetrics] = useState([]); + const [clusters, setClusters] = useState([]); const [namespaces, setNamespaces] = useState([]); const [selectedNs, setSelectedNs] = useState('all'); const [activeCluster, setActiveCluster] = useState(clusterCode || ''); useEffect(() => { - kubernetesApi.getClusters().then(( res: any) => { + kubernetesApi.getClusters().then((res: JsonData) => { const list = res?.list || []; setClusters(list); if (!activeCluster && list.length > 0) { @@ -36,7 +37,7 @@ export default function PodMetricsPage() { useEffect(() => { if (!activeCluster) return; - kubernetesApi.getNamespaces(activeCluster).then(( res: any) => { + kubernetesApi.getNamespaces(activeCluster).then((res: JsonData) => { setNamespaces(Array.isArray(res) ? res : (res?.list || res?.data || [])); }).catch(() => {}); }, [activeCluster]); @@ -46,7 +47,7 @@ export default function PodMetricsPage() { setLoading(true); try { const ns = selectedNs === 'all' ? undefined : selectedNs; - const res: Record = await kubernetesApi.getPodMetrics(activeCluster, ns); + const res: JsonData = await kubernetesApi.getPodMetrics(activeCluster, ns); setMetrics(Array.isArray(res) ? res : (res?.list || [])); } catch { setMetrics([]); } finally { setLoading(false); } }, [activeCluster, selectedNs]); @@ -55,8 +56,8 @@ export default function PodMetricsPage() { // Flatten for table const rows = useMemo(() => - metrics.flatMap(( pm: any) => - (pm.containers || []).map(( c: any) => ({ + metrics.flatMap((pm: JsonData) => + (pm.containers || []).map((c: JsonData) => ({ pod: pm.name, namespace: pm.namespace, container: c.name, @@ -91,7 +92,7 @@ export default function PodMetricsPage() { void; - editingRole: any | null; + editingRole: JsonData | null; form: RoleFormData; setForm: React.Dispatch>; saving: boolean; diff --git a/frontend/src/components/system/SystemLogsTab.tsx b/frontend/src/components/system/SystemLogsTab.tsx index 7d5d2e5..af0b1dd 100644 --- a/frontend/src/components/system/SystemLogsTab.tsx +++ b/frontend/src/components/system/SystemLogsTab.tsx @@ -14,6 +14,7 @@ import { } from '@/components/ui/table'; import { auditApi } from '@/api'; import type { OperationLog } from '@/types'; +import type { JsonData } from '@/types/common'; interface SystemLogsTabProps { searchQuery: string; @@ -21,7 +22,7 @@ interface SystemLogsTabProps { } // 将后端审计日志规范化为前端 OperationLog 类型 // Logan.Li -function normalizeLog( raw: any): OperationLog { +function normalizeLog(raw: JsonData): OperationLog { return { id: String(raw.id ?? ''), userId: String(raw.user_id ?? raw.userId ?? ''), diff --git a/frontend/src/components/system/SystemSettingsTab.tsx b/frontend/src/components/system/SystemSettingsTab.tsx index a5df6a6..9ddbe71 100644 --- a/frontend/src/components/system/SystemSettingsTab.tsx +++ b/frontend/src/components/system/SystemSettingsTab.tsx @@ -11,6 +11,7 @@ import { } from '@/components/ui/select'; import { Lock, History, Key, Settings, Network, RefreshCw, Loader2 } from 'lucide-react'; import { LdapManagementSection } from './LdapManagementSection'; +import type { JsonData } from '@/types/common'; export interface LdapFormData { ldap_host: string; @@ -67,13 +68,13 @@ interface SystemSettingsTabProps { ldapUserTesting: boolean; ldapUserTestResult: unknown; handleLdapUserTest: () => void; - editLdapUser: any | null; - setEditLdapUser: (u: any | null) => void; + editLdapUser: JsonData | null; + setEditLdapUser: (u: JsonData | null) => void; editLdapForm: { cn: string; mail: string; phone: string; title: string }; setEditLdapForm: React.Dispatch>; handleUpdateLdapUser: () => void; handleDeleteLDAPEntry: (dn: string, silent?: boolean) => void; - openEditLdapUser: ( user: any) => void; + openEditLdapUser: (user: JsonData) => void; } export function SystemSettingsTab(props: SystemSettingsTabProps) { diff --git a/frontend/src/components/system/UserFormDialog.tsx b/frontend/src/components/system/UserFormDialog.tsx index 7dcb2fe..97a61c6 100644 --- a/frontend/src/components/system/UserFormDialog.tsx +++ b/frontend/src/components/system/UserFormDialog.tsx @@ -18,6 +18,7 @@ import { } from '@/components/ui/select'; import { User } from 'lucide-react'; import { userApi } from '@/api'; +import type { JsonData } from '@/types/common'; export interface UserFormData { username: string; @@ -32,7 +33,7 @@ export interface UserFormData { interface UserFormDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - editingUser: any | null; + editingUser: JsonData | null; form: UserFormData; setForm: React.Dispatch>; saving: boolean; @@ -60,7 +61,7 @@ export function UserFormDialog({ if (cancelled) return; const list = resp?.list ?? []; setRoles( - list.map(( r: any) => ({ + list.map((r: JsonData) => ({ id: String(r.id ?? ''), name: r.name ?? r.code ?? '', })) diff --git a/frontend/src/components/system/useLdapManagement.ts b/frontend/src/components/system/useLdapManagement.ts index 0ee3341..11801ea 100644 --- a/frontend/src/components/system/useLdapManagement.ts +++ b/frontend/src/components/system/useLdapManagement.ts @@ -3,6 +3,7 @@ import { toast } from 'sonner'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { ldapApi } from '@/api'; import type { LdapFormData } from './SystemSettingsTab'; +import type { JsonData } from '@/types/common'; /** * useLdapManagement — 封装 SystemManager 中所有 LDAP 相关状态与业务逻辑 @@ -21,14 +22,14 @@ export function useLdapManagement(onUsersChanged: () => Promise | void) { const confirm = useConfirm(); // ===== LDAP 用户导入(用户管理 Tab 的导入弹窗用) ===== - const [ldapUsers, setLdapUsers] = useState([]); + const [ldapUsers, setLdapUsers] = useState([]); const [ldapSelected, setLdapSelected] = useState>(new Set()); const [ldapLoading, setLdapLoading] = useState(false); - const [ldapImportOUs, setLdapImportOUs] = useState([]); + const [ldapImportOUs, setLdapImportOUs] = useState([]); const [ldapImportOUDN, setLdapImportOUDN] = useState(''); // ===== LDAP 配置 ===== - const [ldapConfig, setLdapConfig] = useState(null); + const [ldapConfig, setLdapConfig] = useState(null); const [ldapForm, setLdapForm] = useState({ ldap_host: '', ldap_port: 389, use_ldaps: false, admin_dn: '', admin_password: '', base_dn: '', search_filter: '', @@ -41,7 +42,7 @@ export function useLdapManagement(onUsersChanged: () => Promise | void) { const [importResultOpen, setImportResultOpen] = useState(false); // ===== LDAP 账号管理 ===== - const [ldapOUs, setLdapOUs] = useState([]); + const [ldapOUs, setLdapOUs] = useState([]); const [selectedOUDN, setSelectedOUDN] = useState(''); const [selectedLDAPUsers, setSelectedLDAPUsers] = useState([]); const [createOUOpen, setCreateOUOpen] = useState(false); @@ -52,8 +53,8 @@ export function useLdapManagement(onUsersChanged: () => Promise | void) { const [ldapTestUser, setLdapTestUser] = useState(''); const [ldapTestPass, setLdapTestPass] = useState(''); const [ldapUserTesting, setLdapUserTesting] = useState(false); - const [ldapUserTestResult, setLdapUserTestResult] = useState(null); - const [editLdapUser, setEditLdapUser] = useState(null); + const [ldapUserTestResult, setLdapUserTestResult] = useState(null); + const [editLdapUser, setEditLdapUser] = useState(null); const [editLdapForm, setEditLdapForm] = useState({ cn: '', mail: '', phone: '', title: '' }); // 导入进行中状态(CSV 与 LDAP 导入共用) @@ -176,7 +177,7 @@ export function useLdapManagement(onUsersChanged: () => Promise | void) { }; /** 打开 LDAP 用户编辑 // Logan.Li */ - const openEditLdapUser = ( user: any) => { + const openEditLdapUser = (user: JsonData) => { setEditLdapUser(user); setEditLdapForm({ cn: user.cn || '', mail: user.mail || '', phone: user.phone || '', title: user.title || '' }); }; diff --git a/frontend/src/components/ui/chart.tsx b/frontend/src/components/ui/chart.tsx index 329ff2a..7b670a2 100644 --- a/frontend/src/components/ui/chart.tsx +++ b/frontend/src/components/ui/chart.tsx @@ -4,6 +4,7 @@ import * as React from "react" import * as RechartsPrimitive from "recharts" import { cn } from "@/lib/utils" +import type { JsonData } from '@/types/common'; // Format: { THEME_NAME: CSS_SELECTOR } const THEMES = { light: "", dark: ".dark" } as const @@ -310,8 +311,7 @@ function ChartLegendContent({ // Helper to extract item config from a payload. function getPayloadConfigFromPayload( - config: ChartConfig, payload: any, - key: string + config: ChartConfig, payload: JsonData, key: string ) { if (typeof payload !== "object" || payload === null) { return undefined diff --git a/frontend/src/lib/agent-utils.ts b/frontend/src/lib/agent-utils.ts index 6817221..1a94537 100644 --- a/frontend/src/lib/agent-utils.ts +++ b/frontend/src/lib/agent-utils.ts @@ -1,3 +1,4 @@ +import type { JsonData } from "@/types/common"; // Agent 监控相关工具函数(供组件与 API 层共享) // Logan.Li /** 在线判定:3 分钟内有上报视为在线 */ @@ -22,7 +23,7 @@ export function timeAgo(ts: unknown): string { /** 兼容多种列表响应包装:数组 / {list} / {items} / {data} */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function asList(data: any): any[] { +export function asList(data: JsonData): JsonData[] { if (!data) return []; if (Array.isArray(data)) return data; if (Array.isArray(data.list)) return data.list; diff --git a/frontend/src/pages/ApprovalCategoryPage.tsx b/frontend/src/pages/ApprovalCategoryPage.tsx index e21093f..12bd3fb 100644 --- a/frontend/src/pages/ApprovalCategoryPage.tsx +++ b/frontend/src/pages/ApprovalCategoryPage.tsx @@ -15,6 +15,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Plus, Search, RefreshCw, Clock, CheckCircle, XCircle, FileText, Loader2, Eye, Check, XIcon, User, ChevronRight, Play } from 'lucide-react'; import { approvalApi } from '@/api'; import { toast } from 'sonner'; +import type { JsonData } from '@/types/common'; export interface FormField { key: string; label: string; type: 'text' | 'number' | 'select' | 'textarea'; @@ -50,7 +51,7 @@ function parseFormData(data: string): Record { try { return JSON.parse(data) || {}; } catch { return {}; } } -function getApplicant(p: any): string { +function getApplicant(p: JsonData): string { const u = p?.applicant_user; return u ? (u.real_name || u.username || `#${p.applicant}`) : (p?.applicant ? `#${p.applicant}` : '--'); } @@ -80,10 +81,10 @@ export function ApprovalCategoryPage({ category, title, description, fields, tem const fetchProcesses = useCallback(async () => { setLoading(true); try { - const params: any = { page_size: '100' }; + const params: JsonData = { page_size: '100' }; if (category) params.category = category; if (statusFilter) params.status = statusFilter; - const resp = (await approvalApi.getProcesses(params)) as any; + const resp = (await approvalApi.getProcesses(params)) as JsonData; setProcesses(resp?.list ?? resp?.data?.list ?? []); } catch { setProcesses([]); } finally { setLoading(false); } @@ -91,7 +92,7 @@ export function ApprovalCategoryPage({ category, title, description, fields, tem const fetchTemplateId = useCallback(async () => { try { - const resp = (await approvalApi.getTemplates({ type: templateCode })) as any; + const resp = (await approvalApi.getTemplates({ type: templateCode })) as JsonData; const list = resp?.list ?? resp?.data?.list ?? []; if (list.length > 0) setTemplateId(list[0].id); } catch {} @@ -141,9 +142,9 @@ export function ApprovalCategoryPage({ category, title, description, fields, tem const openDetail = async (p: ProcessItem) => { // Load full detail including steps try { - const resp: any = await approvalApi.getProcesses({ page_size: 200 } as any); + const resp: JsonData = await approvalApi.getProcesses({ page_size: 200 } as JsonData); const list = resp?.list ?? resp?.data?.list ?? []; - const full = list.find((x: any) => x.id === p.id); + const full = list.find((x: JsonData) => x.id === p.id); setDetailItem(full || p); } catch { setDetailItem(p); } }; diff --git a/frontend/src/pages/HostMonitor.tsx b/frontend/src/pages/HostMonitor.tsx index aadaf26..3a30da6 100644 --- a/frontend/src/pages/HostMonitor.tsx +++ b/frontend/src/pages/HostMonitor.tsx @@ -25,6 +25,7 @@ import { DualLineChart } from './host-monitor/DualLineChart'; import { HealthProbeSection } from './host-monitor/HealthProbeSection'; import { PortPolicyDialog } from './host-monitor/PortPolicyDialog'; import { SystemInfoSection } from './host-monitor/SystemInfoSection'; +import type { JsonData } from '@/types/common'; export function HostMonitor() { const qc = useQueryClient(); @@ -83,10 +84,10 @@ export function HostMonitor() { const metrics = useMemo(() => buildMetricsFromHistory(history, timeConfig), [history, timeConfig]); const netInfo = parseJSON(latest?.net_info); - const portInfo: Record[] = Array.isArray(latest?.port_info) ? latest.port_info : (Array.isArray(parseJSON(latest?.port_info)) ? parseJSON(latest?.port_info) : []); + const portInfo: JsonData = Array.isArray(latest?.port_info) ? latest.port_info : (Array.isArray(parseJSON(latest?.port_info)) ? parseJSON(latest?.port_info) : []); const diskIOInfo = parseJSON(latest?.disk_io_info); const swapInfo = parseJSON(latest?.swap_info); - const processTop: Record[] = Array.isArray(parseJSON(latest?.process_top_info)) ? parseJSON(latest?.process_top_info) : []; + const processTop: JsonData = Array.isArray(parseJSON(latest?.process_top_info)) ? parseJSON(latest?.process_top_info) : []; const fdInfo = parseJSON(latest?.fd_info); const csInfo = parseJSON(latest?.cs_info); const heartbeatInfo = parseJSON(latest?.heartbeat_info); @@ -108,7 +109,7 @@ export function HostMonitor() { - {hosts.map((h: any) => ( + {hosts.map((h: JsonData) => ( {h.hostname} ))} @@ -177,7 +178,7 @@ export function HostMonitor() { data={metrics.disk} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} /> { const dArr = Array.isArray(parseJSON(latest?.disk_info)) ? parseJSON(latest?.disk_info) : []; - const root = dArr.find((d: any) => d.mount === '/') || dArr[0]; + const root = dArr.find((d: JsonData) => d.mount === '/') || dArr[0]; return root?.inode_usage_pct != null ? `${root.inode_usage_pct.toFixed(1)}%` : '--'; })()} icon={} data={metrics.inode} color="#8b5cf6" unit="%" domain={[0, 100]} aggType={aggType} /> @@ -254,7 +255,7 @@ export function HostMonitor() { - {portInfo.map((p: any, i: number) => ( + {portInfo.map((p: JsonData, i: number) => ( {p.port} @@ -301,7 +302,7 @@ export function HostMonitor() { - {processTop.map((p: any, i: number) => { + {processTop.map((p: JsonData, i: number) => { const cmd = p.cmdline || p.name || '?'; const cpup = p.cpu_pct != null ? Number(p.cpu_pct) : null; const memp = p.mem_pct != null ? Number(p.mem_pct) : null; diff --git a/frontend/src/pages/LoginLogsTab.tsx b/frontend/src/pages/LoginLogsTab.tsx index 717bb16..c4f1d82 100644 --- a/frontend/src/pages/LoginLogsTab.tsx +++ b/frontend/src/pages/LoginLogsTab.tsx @@ -14,6 +14,7 @@ import { } from '@/components/ui/table'; import { Search, RefreshCw, LogIn, Monitor, Globe } from 'lucide-react'; import { loginLogApi } from '@/api'; +import type { JsonData } from '@/types/common'; interface LoginLog { id: number; @@ -30,7 +31,7 @@ interface LoginLog { created_at: string; } -function normalizeLoginLog(raw: any): LoginLog { +function normalizeLoginLog(raw: JsonData): LoginLog { return { id: raw.id ?? 0, username: raw.username ?? '', @@ -56,9 +57,9 @@ export function LoginLogsTab() { const fetchLogs = useCallback(async () => { setLoading(true); try { - const params: any = { page: 1, page_size: 100 }; + const params: JsonData = { page: 1, page_size: 100 }; if (statusFilter) params.status = statusFilter === 'fail' ? 'failed' : statusFilter; - const resp = (await loginLogApi.list(params)) as any; + const resp = (await loginLogApi.list(params)) as JsonData; const list = resp?.list ?? []; setLogs(list.map(normalizeLoginLog)); } catch { diff --git a/frontend/src/pages/MetricsExplorer.tsx b/frontend/src/pages/MetricsExplorer.tsx index 27b71c2..20b209a 100644 --- a/frontend/src/pages/MetricsExplorer.tsx +++ b/frontend/src/pages/MetricsExplorer.tsx @@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Activity, Search, Filter, BarChart3, Cpu, HardDrive, Network, Database, Plus } from 'lucide-react'; import { metricsApi } from '@/api'; +import type { JsonData } from '@/types/common'; interface _MetricName { name: string; @@ -51,7 +52,7 @@ export function MetricsExplorer() { // Load targets const loadTargets = useCallback(async () => { try { - const res: Record = await metricsApi.getTargets({ page_size: 200 }); + const res: JsonData = await metricsApi.getTargets({ page_size: 200 }); setTargets(res?.list || []); } catch { /* empty */ } finally { setLoading(false); } @@ -60,7 +61,7 @@ export function MetricsExplorer() { // Load metric names const loadMetricNames = useCallback(async () => { try { - const names: any = await metricsApi.getNames(); + const names: string[] = await metricsApi.getNames(); setMetricNames(names || []); } catch { /* empty */ } }, []); @@ -79,7 +80,7 @@ export function MetricsExplorer() { Promise.all( selectedMetrics.map(mn => metricsApi.query({ target_id: selectedTarget, metric_name: mn, from: String(from), to: String(to), limit: 200 }) - .then(( data: any) => (Array.isArray(data) ? data : [])) + .then((data: JsonData) => (Array.isArray(data) ? data : [])) .catch(() => []) ) ).then(results => { diff --git a/frontend/src/pages/MiddlewareMonitor.tsx b/frontend/src/pages/MiddlewareMonitor.tsx index 8d0c694..0c02fe1 100644 --- a/frontend/src/pages/MiddlewareMonitor.tsx +++ b/frontend/src/pages/MiddlewareMonitor.tsx @@ -4,6 +4,7 @@ import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Database, Server, Activity, Loader2, Circle } from 'lucide-react'; import { metricsApi } from '@/api'; +import type { JsonData } from '@/types/common'; interface TargetInfo { id: number; @@ -71,7 +72,7 @@ export function MiddlewareMonitor() { const data = await metricsApi.getTargets({ type: 'middleware', page_size: 200 }) as TargetsResponse; setTargets(data?.list || []); } catch (err) { - setError((err as Record).message || '加载失败'); + setError((err as JsonData).message || '加载失败'); setTargets([]); } finally { setLoading(false); diff --git a/frontend/src/pages/SiteMonitor.tsx b/frontend/src/pages/SiteMonitor.tsx index 1bb8f94..27907d8 100644 --- a/frontend/src/pages/SiteMonitor.tsx +++ b/frontend/src/pages/SiteMonitor.tsx @@ -45,6 +45,7 @@ import { import { toast } from 'sonner'; import { siteApi } from '@/api'; import { useConfirm } from '@/components/ui/confirm-dialog'; +import type { JsonData } from '@/types/common'; // 拨测协议类型(与后端 model 对齐:http, https, tcp, dns, ping) const probeTypes = [ @@ -161,7 +162,7 @@ export function SiteMonitor() { setRefreshing(true); try { const data = await siteApi.getTasks(); - setTasks((data as Record)?.list || []); + setTasks((data as JsonData)?.list || []); } catch { // 后端不可用时保留空状态 } finally { @@ -206,7 +207,7 @@ export function SiteMonitor() { loadTasks(); loadStats(); } catch (e) { - toast.error((e as Record).message || '创建失败'); + toast.error((e as JsonData).message || '创建失败'); } finally { setSaving(false); } @@ -225,7 +226,7 @@ export function SiteMonitor() { loadTasks(); loadStats(); } catch (e) { - toast.error((e as Record).message || '删除失败'); + toast.error((e as JsonData).message || '删除失败'); } }; @@ -236,7 +237,7 @@ export function SiteMonitor() { loadTasks(); loadStats(); } catch (e) { - toast.error((e as Record).message || '操作失败'); + toast.error((e as JsonData).message || '操作失败'); } }; diff --git a/frontend/src/pages/host-monitor/SystemInfoSection.tsx b/frontend/src/pages/host-monitor/SystemInfoSection.tsx index 9e5d107..e53d9fe 100644 --- a/frontend/src/pages/host-monitor/SystemInfoSection.tsx +++ b/frontend/src/pages/host-monitor/SystemInfoSection.tsx @@ -3,12 +3,13 @@ import { Activity, HardDrive } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; import { TabsContent } from '@/components/ui/tabs'; import { parseJSON } from './utils'; +import type { JsonData } from '@/types/common'; interface SystemInfoSectionProps { selectedHost: string; - serverInfo: any; - latest: any; - heartbeatInfo: any; + serverInfo?: JsonData; + latest?: JsonData; + heartbeatInfo?: JsonData; portInfoCount: number; } @@ -67,7 +68,7 @@ export function SystemInfoSection({

磁盘分区

- {parts.map((p: any, i: number) => ( + {parts.map((p: JsonData, i: number) => (
{p.mount || p.mountpoint || p.device || `disk${i}`}
diff --git a/frontend/src/pages/host-monitor/utils.ts b/frontend/src/pages/host-monitor/utils.ts index 8b46415..b71a44c 100644 --- a/frontend/src/pages/host-monitor/utils.ts +++ b/frontend/src/pages/host-monitor/utils.ts @@ -1,5 +1,6 @@ // 主机监控工具函数 // Logan.Li import type { BucketPoint } from './types'; +import type { JsonData } from '@/types/common'; export function bucketWithStats(raw: number[], timestamps: number[], targetBuckets: number): BucketPoint[] { if (!raw || raw.length === 0) return []; @@ -26,7 +27,7 @@ export function bucketWithStats(raw: number[], timestamps: number[], targetBucke } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function parseJSON(raw: unknown): any { +export function parseJSON(raw: unknown): JsonData { if (!raw) return {}; if (typeof raw === 'object') return raw; try { return JSON.parse(raw as string); } catch { return {}; } @@ -41,7 +42,7 @@ export function formatBPS(bps: number): string { interface TimeConfig { buckets: number; } -export function buildMetricsFromHistory(history: any[], timeConfig: TimeConfig) { +export function buildMetricsFromHistory(history: JsonData[], timeConfig: TimeConfig) { const rawCpu: number[] = []; const rawMem: number[] = []; const rawLoad1: number[] = []; const rawDisk: number[] = []; const rawTcp: number[] = []; const rawTw: number[] = []; const rawCw: number[] = []; const rawRx: number[] = []; const rawTx: number[] = []; @@ -65,7 +66,7 @@ export function buildMetricsFromHistory(history: any[], timeConfig: TimeConfig) const diskInfoArr = parseJSON(r.disk_info); if (Array.isArray(diskInfoArr) && diskInfoArr.length > 0) { - const rootDisk = diskInfoArr.find((d: any) => d.mount === '/') || diskInfoArr[0]; + const rootDisk = diskInfoArr.find((d: JsonData) => d.mount === '/') || diskInfoArr[0]; rawInode.push(rootDisk?.inode_usage_pct || 0); } else { rawInode.push(0); @@ -117,8 +118,7 @@ export function buildMetricsFromHistory(history: any[], timeConfig: TimeConfig) }; } -export function getCurrentVal( - latest: any, netInfo: any, diskIOInfo: any, swapInfo: any, fdInfo: any, +export function getCurrentVal(latest: JsonData, netInfo: JsonData, diskIOInfo: JsonData, swapInfo: JsonData, fdInfo: JsonData, key: string, fallback = '--', ): string { if (!latest) return fallback; diff --git a/frontend/src/types/common.ts b/frontend/src/types/common.ts index e08e1c6..71a44fb 100644 --- a/frontend/src/types/common.ts +++ b/frontend/src/types/common.ts @@ -55,3 +55,11 @@ export interface SortParams { sort_by?: string; sort_order?: 'asc' | 'desc'; } + +// ===== 动态 JSON 数据类型 ===== +// 后端 API 返回的动态 JSON 结构,字段高度不确定(运行时从 DB/API 构建) +// 集中声明 any 以便 ESLint no-explicit-any 规则在全局仅此一处豁免 +// 后续应逐步替换为 types/ 中的具体接口定义 +// eslint-disable-next-line @typescript-eslint/no-explicit-any +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type JsonData = Record; diff --git a/frontend/src/types/monitor.ts b/frontend/src/types/monitor.ts index e1f0afb..928b923 100644 --- a/frontend/src/types/monitor.ts +++ b/frontend/src/types/monitor.ts @@ -1,3 +1,4 @@ +import type { JsonData } from "@/types/common"; // 监控告警模块类型定义 // 字段与后端 alert_engine.go / agent.go JSON tag 对齐(snake_case) @@ -355,7 +356,7 @@ export interface DashboardPanel { x: number; y: number; // eslint-disable-next-line @typescript-eslint/no-explicit-any - options: any; + options?: JsonData; } export interface GrafanaDashboard {