feat(k8s): M10 Node 详情页 — PID/容器运行时/分配率
- 后端: GetNodeDetail handler — 获取单个节点完整信息 * 系统信息: container_runtime/architecture/kubelet/os/kernel * 资源分配率: 遍历 Pod 计算 CPU/Memory/Pods requested vs allocatable * metrics-server 数据注入 (cpu_usage_m/mem_usage_mi) * 全量 conditions/taints/labels + 节点上的 Pod 列表 - 后端: K8s 客户端扩展 * NodeTaint/NodeCondition 扩展字段 (reason/message) * NodeInfo 增加 architecture/operatingSystem * GetNode(name) 单节点查询方法 * parseCpuToMilli()/parseMemToMi() 资源解析辅助函数 - 后端: 注册路由 * GET /kubernetes/:clusterCode/nodes/:name (Kuboard code) * GET /kubernetes-clusters/:id/nodes/:name (ID) - 前端: NodeDetailPage.tsx (~330行) * 系统信息卡片 + 资源分配率卡片(进度条) * 条件卡片 + 污点卡片 + 标签卡片 + Pod 列表 - 前端: 节点名称可点击跳转 (ClusterOverviewPage) - 验证: API/SPA/TS/Vite 全部通过
This commit is contained in:
1 parent
28dfc519e1
commit
87b7755094
16 files changed
+921
-52
No files matched your search
@@ -37,6 +37,7 @@
|
|||||||
| 容量分析 | ✅ GetCapacityAnalysis | ✅ CapacityPage | ✅ |
|
| 容量分析 | ✅ GetCapacityAnalysis | ✅ CapacityPage | ✅ |
|
||||||
| kubectl 终端 | ✅ KubectlShell(WS) | ✅ KubectlTerminal | ✅ |
|
| kubectl 终端 | ✅ KubectlShell(WS) | ✅ KubectlTerminal | ✅ |
|
||||||
| 拓扑图 | ✅ topology get/save | ✅ TopologyView | ✅ |
|
| 拓扑图 | ✅ topology get/save | ✅ TopologyView | ✅ |
|
||||||
|
| **Node 详情页** | ✅ GetNodeDetail (M10) | ✅ NodeDetailPage | ✅ curl 验证 |
|
||||||
|
|
||||||
### ❌ 缺失 / 进行中
|
### ❌ 缺失 / 进行中
|
||||||
|
|
||||||
@@ -82,3 +83,4 @@
|
|||||||
2. **metrics-server 已安装** — `kubectl top nodes` 正常返回
|
2. **metrics-server 已安装** — `kubectl top nodes` 正常返回
|
||||||
3. **自研 K8s 客户端** — 不用 client-go,保持轻量 net/http 方案
|
3. **自研 K8s 客户端** — 不用 client-go,保持轻量 net/http 方案
|
||||||
4. **两个数据路径**:ListNodes 实时调 API(含 metrics),SyncCluster 写 DB(缺 metrics)→ 前端不同页面读不同路径
|
4. **两个数据路径**:ListNodes 实时调 API(含 metrics),SyncCluster 写 DB(缺 metrics)→ 前端不同页面读不同路径
|
||||||
|
5. **Node 详情页** (M10 ✅) — `GetNodeDetail` handler 返回完整节点信息:container_runtime/architecture/taints/conditions/资源分配率/Pod列表,前端 `NodeDetailPage.tsx` 可视化展示
|
||||||
+30
-1
@@ -183,9 +183,38 @@
|
|||||||
|
|
||||||
## 后续规划(P2+)
|
## 后续规划(P2+)
|
||||||
|
|
||||||
|
### M10: Node 详情页(PID/容器运行时/分配率)[done]
|
||||||
|
**目标**: 节点名称可点击进入详情页,展示完整的系统信息、资源分配率、污点、条件、Pod 列表
|
||||||
|
|
||||||
|
**改动**:
|
||||||
|
- 后端:
|
||||||
|
- `kubernetesclient/client.go`: 新增 `NodeTaint` / `NodeCondition` 扩展字段 + `NodeInfo` 增加 `architecture`/`operatingSystem` + `GetNode(name)` 方法
|
||||||
|
- `kubernetes.go`: 新增 `GetNodeDetail` handler — 获取单个节点详情 + 遍历 Pod 计算资源分配率(CPU/Memory/Pods requested vs allocatable)
|
||||||
|
- `routes.go`: 注册 `GET /kubernetes/:clusterCode/nodes/:name` (Kuboard code 路由) + `GET /kubernetes-clusters/:id/nodes/:name` (ID 路由)
|
||||||
|
- 新增 `parseCpuToMilli()` / `parseMemToMi()` 辅助函数(解析 Pod resource requests)
|
||||||
|
- 前端:
|
||||||
|
- 新增 `NodeDetailPage.tsx` (~330行) — 节点详情页:系统信息卡片 + 资源分配率卡片(进度条) + 条件卡片 + 污点卡片 + 标签卡片 + Pod 列表
|
||||||
|
- `api.ts`: 新增 `getNodeDetail()` 方法
|
||||||
|
- `App.tsx`: 新增路由 `:clusterCode/nodes/:nodeName`
|
||||||
|
- `ClusterOverviewPage.tsx`: 节点名称可点击跳转详情页
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [x] `GET /kubernetes/:clusterCode/nodes/:name` 返回完整详情(container_runtime/kubelet/os/kernel/architecture)
|
||||||
|
- [x] 资源分配率正确计算(cpu_alloc_pct/mem_alloc_pct/pod_alloc_pct)
|
||||||
|
- [x] metrics-server 数据注入(cpu_usage_m/mem_usage_mi)
|
||||||
|
- [x] 节点条件全量返回(5个 conditions 含 reason/message)
|
||||||
|
- [x] 污点列表返回
|
||||||
|
- [x] 节点上的 Pod 列表返回(含 CPU/内存 requests)
|
||||||
|
- [x] 前端节点名称可点击跳转
|
||||||
|
- [x] SPA 路由返回 200(非 301)
|
||||||
|
- [x] TypeScript 零编译错误 + Vite 构建成功
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 后续(P3+)
|
||||||
|
|
||||||
| 模块 | 描述 |
|
| 模块 | 描述 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| M10 | Node 详情页(PID/容器运行时/分配率) |
|
|
||||||
| M11 | StatefulSet/DaemonSet 详情页(对标 Deployment) |
|
| M11 | StatefulSet/DaemonSet 详情页(对标 Deployment) |
|
||||||
| M12 | 事件聚合 + 告警联动 |
|
| M12 | 事件聚合 + 告警联动 |
|
||||||
| M13 | 多集群资源对比看板 |
|
| M13 | 多集群资源对比看板 |
|
||||||
|
|||||||
+46
@@ -270,6 +270,52 @@ curl -s http://127.0.0.1/ | grep -oP 'index-[A-Za-z0-9]+\.js'
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## M10: Node 详情页(PID/容器运行时/分配率)
|
||||||
|
|
||||||
|
### 验证用例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# U1: 登录获取 Token
|
||||||
|
TOKEN=$(curl -s http://127.0.0.1:8080/api/v1/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"admin","password":"admin123"}' | jq -r '.data.token')
|
||||||
|
|
||||||
|
# U2: Node detail API — Kuboard code route
|
||||||
|
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/test-k8s/nodes/k8s-master-01" \
|
||||||
|
-H "Authorization: Bearer *** | jq '.data.container_runtime'
|
||||||
|
# 期望: "containerd://2.1.4"
|
||||||
|
|
||||||
|
# U3: 资源分配率字段
|
||||||
|
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/test-k8s/nodes/k8s-master-01" \
|
||||||
|
-H "Authorization: Bearer *** | jq '.data | {cpu_alloc_pct, mem_alloc_pct, pod_alloc_pct, pod_count}'
|
||||||
|
# 期望: cpu_alloc_pct/mem_alloc_pct/pod_alloc_pct 均为数字 > 0
|
||||||
|
|
||||||
|
# U4: conditions + taints + pods 列表
|
||||||
|
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/test-k8s/nodes/k8s-master-01" \
|
||||||
|
-H "Authorization: Bearer *** | jq '.data | {conditions: (.conditions|length), taints: (.taints|length), pods: (.pods|length)}'
|
||||||
|
# 期望: conditions>=4, pods>=1
|
||||||
|
|
||||||
|
# U5: SPA fallback — node detail 路由 200
|
||||||
|
curl -s -o /dev/null -w "%{http_code}" \
|
||||||
|
"http://127.0.0.1/kubernetes/test-k8s/nodes/k8s-master-01"
|
||||||
|
# 期望: 200
|
||||||
|
|
||||||
|
# U6: TypeScript + Vite 构建
|
||||||
|
cd frontend && npx tsc --noEmit && npm run build 2>&1 | tail -3
|
||||||
|
# 期望: 零错误 + built in Ns
|
||||||
|
```
|
||||||
|
|
||||||
|
### 通过标准
|
||||||
|
- [x] U1: 登录成功
|
||||||
|
- [x] U2: Node detail API 返回 container_runtime/kubelet/os/kernel/architecture
|
||||||
|
- [x] U3: 资源分配率正确(cpu_alloc_pct/mem_alloc_pct/pod_alloc_pct 数字)
|
||||||
|
- [x] U4: conditions/taints/pods 列表非空
|
||||||
|
- [x] U5: SPA 路由 200(非 301)
|
||||||
|
- [x] U6: TypeScript 零错误 + Vite 构建成功
|
||||||
|
- [x] 前端节点名称可点击跳转详情页
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## M9: 服务/应用路由 Tab + 整体联调
|
## M9: 服务/应用路由 Tab + 整体联调
|
||||||
|
|
||||||
### 验证用例
|
### 验证用例
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# AIOps Backend Configuration (External MySQL/Redis)
|
||||||
|
mode: release
|
||||||
|
port: "8080"
|
||||||
|
log_level: info
|
||||||
|
|
||||||
|
# MySQL 数据库 (连接 75 的 MySQL)
|
||||||
|
database:
|
||||||
|
driver: mysql
|
||||||
|
host: 10.10.10.75
|
||||||
|
port: 3306
|
||||||
|
user: aiops
|
||||||
|
password: aiops_pass_2026
|
||||||
|
dbname: aiops
|
||||||
|
charset: utf8mb4
|
||||||
|
max_conn: 100
|
||||||
|
max_idle: 10
|
||||||
|
|
||||||
|
# Redis 缓存 (连接 75 的 Redis)
|
||||||
|
redis:
|
||||||
|
driver: redis
|
||||||
|
host: 10.10.10.75
|
||||||
|
port: 6379
|
||||||
|
password: ""
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
# JWT 配置
|
||||||
|
jwt:
|
||||||
|
secret: "aiops-jwt-secret-key-2026"
|
||||||
|
expire_hour: 24
|
||||||
@@ -241,22 +241,27 @@ func (h *KubernetesHandler) GetClusterDetail(c *gin.Context) {
|
|||||||
Code: 200,
|
Code: 200,
|
||||||
Message: "success",
|
Message: "success",
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"id": cluster.ID,
|
"id": cluster.ID,
|
||||||
"name": cluster.Name,
|
"name": cluster.Name,
|
||||||
"code": cluster.Code,
|
"code": cluster.Code,
|
||||||
"version": cluster.Version,
|
"version": cluster.Version,
|
||||||
"status": cluster.Status,
|
"status": cluster.Status,
|
||||||
"config_type": cluster.ConfigType,
|
"config_type": cluster.ConfigType,
|
||||||
"api_server": cluster.APIServer,
|
"api_server": cluster.APIServer,
|
||||||
"api_endpoint": cluster.APIEndpoint,
|
"api_endpoint": cluster.APIEndpoint,
|
||||||
"description": cluster.Description,
|
"description": cluster.Description,
|
||||||
"has_token": cluster.Token != "",
|
"has_token": cluster.Token != "",
|
||||||
"has_kube_config": cluster.KubeConfig != "",
|
"has_kube_config": cluster.KubeConfig != "",
|
||||||
"node_count": cluster.NodeCount,
|
"node_count": cluster.NodeCount,
|
||||||
"pod_count": cluster.PodCount,
|
"pod_count": cluster.PodCount,
|
||||||
"labels": cluster.Labels,
|
"service_count": cluster.ServiceCount,
|
||||||
"created_at": cluster.CreatedAt,
|
"deployment_count": cluster.DeploymentCount,
|
||||||
"updated_at": cluster.UpdatedAt,
|
"cpu_usage": cluster.CPUUsage,
|
||||||
|
"memory_usage": cluster.MemoryUsage,
|
||||||
|
"labels": cluster.Labels,
|
||||||
|
"last_sync_at": cluster.LastSyncAt,
|
||||||
|
"created_at": cluster.CreatedAt,
|
||||||
|
"updated_at": cluster.UpdatedAt,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -376,6 +381,286 @@ func (h *KubernetesHandler) ListNodes(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: result})
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: result})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetNodeDetail 获取节点详情(PID/容器运行时/分配率)// M10
|
||||||
|
// @Summary 获取节点详情
|
||||||
|
// @Description 获取指定集群中单个节点的详细信息,包含系统信息、资源分配、污点、条件等
|
||||||
|
// @Tags K8s集群
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Router /kubernetes/{clusterCode}/nodes/{name} [get]
|
||||||
|
func (h *KubernetesHandler) GetNodeDetail(c *gin.Context) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
if clusterID == 0 {
|
||||||
|
code := c.Param("clusterCode")
|
||||||
|
if code == "" {
|
||||||
|
code = c.Param("id")
|
||||||
|
}
|
||||||
|
clusterID = h.resolveClusterCode(code)
|
||||||
|
}
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeName := c.Param("name")
|
||||||
|
if nodeName == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "node name required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Get node detail
|
||||||
|
node, err := client.GetNode(nodeName)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Get node metrics (best-effort)
|
||||||
|
var cpuUsageM, memUsageMi string
|
||||||
|
if m, err := client.GetNodeMetrics(); err == nil {
|
||||||
|
for _, item := range m.Items {
|
||||||
|
if item.Metadata.Name == nodeName {
|
||||||
|
cpuUsageM = formatCpuFromNano(item.Usage["cpu"])
|
||||||
|
memUsageMi = formatMemFromKi(item.Usage["memory"])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Get pods on this node for allocation calculation
|
||||||
|
pods, _ := client.ListPods()
|
||||||
|
podCount := 0
|
||||||
|
type podBrief struct {
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
CPURequestM int `json:"cpu_request_m"`
|
||||||
|
MemRequestMi int64 `json:"mem_request_mi"`
|
||||||
|
RestartCount int `json:"restart_count"`
|
||||||
|
}
|
||||||
|
var podList []podBrief
|
||||||
|
totalCPURequestM := 0
|
||||||
|
totalMemRequestMi := int64(0)
|
||||||
|
for _, p := range pods.Items {
|
||||||
|
if p.Spec.NodeName != nodeName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
podCount++
|
||||||
|
podCPUReq := 0
|
||||||
|
podMemReq := int64(0)
|
||||||
|
for _, ctr := range p.Spec.Containers {
|
||||||
|
if req := ctr.Resources.Requests; req != nil {
|
||||||
|
podCPUReq += parseCpuToMilli(req["cpu"])
|
||||||
|
podMemReq += parseMemToMi(req["memory"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalCPURequestM += podCPUReq
|
||||||
|
totalMemRequestMi += podMemReq
|
||||||
|
|
||||||
|
status := "Unknown"
|
||||||
|
if p.Status.Phase != "" {
|
||||||
|
status = p.Status.Phase
|
||||||
|
}
|
||||||
|
restartCount := 0
|
||||||
|
for _, cs := range p.Status.ContainerStatuses {
|
||||||
|
restartCount += cs.RestartCount
|
||||||
|
}
|
||||||
|
podList = append(podList, podBrief{
|
||||||
|
Namespace: p.Metadata.Namespace,
|
||||||
|
Name: p.Metadata.Name,
|
||||||
|
Status: status,
|
||||||
|
IP: p.Status.PodIP,
|
||||||
|
CPURequestM: podCPUReq,
|
||||||
|
MemRequestMi: podMemReq,
|
||||||
|
RestartCount: restartCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Determine role
|
||||||
|
role := "worker"
|
||||||
|
for k := range node.Metadata.Labels {
|
||||||
|
if k == "node-role.kubernetes.io/master" || k == "node-role.kubernetes.io/control-plane" {
|
||||||
|
role = "master"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Determine status
|
||||||
|
nodeStatus := "NotReady"
|
||||||
|
for _, cond := range node.Status.Conditions {
|
||||||
|
if cond.Type == "Ready" && cond.Status == "True" {
|
||||||
|
nodeStatus = "Ready"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Get IP
|
||||||
|
internalIP := ""
|
||||||
|
externalIP := ""
|
||||||
|
hostname := ""
|
||||||
|
for _, addr := range node.Status.Addresses {
|
||||||
|
switch addr.Type {
|
||||||
|
case "InternalIP":
|
||||||
|
internalIP = addr.Address
|
||||||
|
case "ExternalIP":
|
||||||
|
externalIP = addr.Address
|
||||||
|
case "Hostname":
|
||||||
|
hostname = addr.Address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Build conditions list
|
||||||
|
type condInfo struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
conditions := []condInfo{}
|
||||||
|
for _, cond := range node.Status.Conditions {
|
||||||
|
conditions = append(conditions, condInfo{
|
||||||
|
Type: cond.Type,
|
||||||
|
Status: cond.Status,
|
||||||
|
Reason: cond.Reason,
|
||||||
|
Message: cond.Message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Build taints list
|
||||||
|
type taintInfo struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Effect string `json:"effect"`
|
||||||
|
}
|
||||||
|
taints := []taintInfo{}
|
||||||
|
for _, t := range node.Spec.Taints {
|
||||||
|
taints = append(taints, taintInfo{
|
||||||
|
Key: t.Key,
|
||||||
|
Value: t.Value,
|
||||||
|
Effect: t.Effect,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Calculate allocation rate
|
||||||
|
capCPU := parseIntResource(node.Status.Capacity["cpu"])
|
||||||
|
allocCPU := parseIntResource(node.Status.Allocatable["cpu"])
|
||||||
|
capMem := parseMemResource(node.Status.Capacity["memory"])
|
||||||
|
allocMem := parseMemResource(node.Status.Allocatable["memory"])
|
||||||
|
capPods := parseIntResource(node.Status.Capacity["pods"])
|
||||||
|
allocPods := parseIntResource(node.Status.Allocatable["pods"])
|
||||||
|
|
||||||
|
cpuReqCores := float64(totalCPURequestM) / 1000.0
|
||||||
|
cpuAllocPct := 0.0
|
||||||
|
if allocCPU > 0 {
|
||||||
|
cpuAllocPct = (cpuReqCores / float64(allocCPU)) * 100
|
||||||
|
}
|
||||||
|
memAllocPct := 0.0
|
||||||
|
if allocMem > 0 {
|
||||||
|
memAllocPct = (float64(totalMemRequestMi) / float64(allocMem)) * 100
|
||||||
|
}
|
||||||
|
podAllocPct := 0.0
|
||||||
|
if allocPods > 0 {
|
||||||
|
podAllocPct = (float64(podCount) / float64(allocPods)) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Build labels list
|
||||||
|
type labelEntry struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
labels := []labelEntry{}
|
||||||
|
for k, v := range node.Metadata.Labels {
|
||||||
|
labels = append(labels, labelEntry{Key: k, Value: v})
|
||||||
|
}
|
||||||
|
|
||||||
|
detail := map[string]interface{}{
|
||||||
|
"name": node.Metadata.Name,
|
||||||
|
"role": role,
|
||||||
|
"status": nodeStatus,
|
||||||
|
"creation_time": node.Metadata.CreationTimestamp,
|
||||||
|
"internal_ip": internalIP,
|
||||||
|
"external_ip": externalIP,
|
||||||
|
"hostname": hostname,
|
||||||
|
"pod_cidr": node.Spec.PodCIDR,
|
||||||
|
"container_runtime": node.Status.NodeInfo.ContainerRuntimeVersion,
|
||||||
|
"kubelet_version": node.Status.NodeInfo.KubeletVersion,
|
||||||
|
"os_image": node.Status.NodeInfo.OSImage,
|
||||||
|
"kernel_version": node.Status.NodeInfo.KernelVersion,
|
||||||
|
"architecture": node.Status.NodeInfo.Architecture,
|
||||||
|
"operating_system": node.Status.NodeInfo.OperatingSystem,
|
||||||
|
// Capacity & Allocatable
|
||||||
|
"capacity": node.Status.Capacity,
|
||||||
|
"allocatable": node.Status.Allocatable,
|
||||||
|
// Metrics
|
||||||
|
"cpu_usage_m": cpuUsageM,
|
||||||
|
"mem_usage_mi": memUsageMi,
|
||||||
|
// Allocation
|
||||||
|
"pod_count": podCount,
|
||||||
|
"cpu_request_m": totalCPURequestM,
|
||||||
|
"mem_request_mi": totalMemRequestMi,
|
||||||
|
"cpu_alloc_pct": cpuAllocPct,
|
||||||
|
"mem_alloc_pct": memAllocPct,
|
||||||
|
"pod_alloc_pct": podAllocPct,
|
||||||
|
"cpu_capacity": capCPU,
|
||||||
|
"cpu_allocatable": allocCPU,
|
||||||
|
"mem_capacity_mb": capMem,
|
||||||
|
"mem_allocatable_mb": allocMem,
|
||||||
|
"pod_capacity": capPods,
|
||||||
|
"pod_allocatable": allocPods,
|
||||||
|
// Lists
|
||||||
|
"conditions": conditions,
|
||||||
|
"taints": taints,
|
||||||
|
"labels": labels,
|
||||||
|
"pods": podList,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: detail})
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCpuToMilli converts CPU resource string to millicores (e.g. "100m"→100, "2"→2000)
|
||||||
|
func parseCpuToMilli(val string) int {
|
||||||
|
if val == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(val, "m") {
|
||||||
|
var i int
|
||||||
|
fmt.Sscanf(strings.TrimSuffix(val, "m"), "%d", &i)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
var i int
|
||||||
|
fmt.Sscanf(val, "%d", &i)
|
||||||
|
return i * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseMemToMi converts memory resource string to Mi (e.g. "256Mi"→256, "1Gi"→1024, "512Ki"→0)
|
||||||
|
func parseMemToMi(val string) int64 {
|
||||||
|
if val == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var num float64
|
||||||
|
var unit string
|
||||||
|
fmt.Sscanf(val, "%f%s", &num, &unit)
|
||||||
|
switch unit {
|
||||||
|
case "Ki":
|
||||||
|
return int64(num / 1024)
|
||||||
|
case "Mi":
|
||||||
|
return int64(num)
|
||||||
|
case "Gi":
|
||||||
|
return int64(num * 1024)
|
||||||
|
case "Ti":
|
||||||
|
return int64(num * 1024 * 1024)
|
||||||
|
default:
|
||||||
|
return int64(num / 1024 / 1024)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ListPods 获取Pod列表
|
// ListPods 获取Pod列表
|
||||||
// @Summary 获取Pod列表
|
// @Summary 获取Pod列表
|
||||||
// @Description 获取指定集群的Pod列表
|
// @Description 获取指定集群的Pod列表
|
||||||
@@ -936,7 +1221,6 @@ func (h *KubernetesHandler) GetPodLogs(c *gin.Context) {
|
|||||||
// 3 = error (server→client, JSON)
|
// 3 = error (server→client, JSON)
|
||||||
// 4 = resize (client→server, JSON {"Height","Width"})
|
// 4 = resize (client→server, JSON {"Height","Width"})
|
||||||
func (h *KubernetesHandler) ExecTerminal(c *gin.Context) {
|
func (h *KubernetesHandler) ExecTerminal(c *gin.Context) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
||||||
namespace := c.Param("namespace")
|
namespace := c.Param("namespace")
|
||||||
podName := c.Param("podName")
|
podName := c.Param("podName")
|
||||||
container := c.Query("container")
|
container := c.Query("container")
|
||||||
@@ -945,7 +1229,7 @@ func (h *KubernetesHandler) ExecTerminal(c *gin.Context) {
|
|||||||
command = "bash"
|
command = "bash"
|
||||||
}
|
}
|
||||||
|
|
||||||
client, cluster, err := h.getK8sClient(clusterID)
|
client, cluster, err := h.getK8sClientByCode(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||||
return
|
return
|
||||||
@@ -1526,7 +1810,12 @@ func (h *KubernetesHandler) resolveClusterCode(code string) int {
|
|||||||
func (h *KubernetesHandler) getK8sClientByCode(c *gin.Context) (*k8sclient.Client, *models.KubernetesCluster, error) {
|
func (h *KubernetesHandler) getK8sClientByCode(c *gin.Context) (*k8sclient.Client, *models.KubernetesCluster, error) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 {
|
if clusterID == 0 {
|
||||||
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
// 支持 :clusterCode 或 :id 参数
|
||||||
|
code := c.Param("clusterCode")
|
||||||
|
if code == "" {
|
||||||
|
code = c.Param("id")
|
||||||
|
}
|
||||||
|
clusterID = h.resolveClusterCode(code)
|
||||||
}
|
}
|
||||||
if clusterID == 0 {
|
if clusterID == 0 {
|
||||||
return nil, nil, fmt.Errorf("cluster not found")
|
return nil, nil, fmt.Errorf("cluster not found")
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
clusters.DELETE("/:id", k8sHandler.DeleteCluster)
|
clusters.DELETE("/:id", k8sHandler.DeleteCluster)
|
||||||
clusters.POST("/:id/sync", k8sHandler.SyncCluster) // 同步集群资源 // Logan.Li
|
clusters.POST("/:id/sync", k8sHandler.SyncCluster) // 同步集群资源 // Logan.Li
|
||||||
clusters.GET("/:id/nodes", k8sHandler.ListNodes)
|
clusters.GET("/:id/nodes", k8sHandler.ListNodes)
|
||||||
|
clusters.GET("/:id/nodes/:name", k8sHandler.GetNodeDetail) // M10 节点详情 // Logan.Li
|
||||||
clusters.GET("/:id/pods", k8sHandler.ListPods)
|
clusters.GET("/:id/pods", k8sHandler.ListPods)
|
||||||
clusters.GET("/:id/deployments", k8sHandler.ListDeployments)
|
clusters.GET("/:id/deployments", k8sHandler.ListDeployments)
|
||||||
clusters.GET("/:id/services", k8sHandler.ListServices)
|
clusters.GET("/:id/services", k8sHandler.ListServices)
|
||||||
@@ -336,6 +337,9 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
k8sHandler.ListDeploymentsNS(c)
|
k8sHandler.ListDeploymentsNS(c)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Node detail (cluster-scoped, not namespace-scoped) // M10
|
||||||
|
kubernetesRouter.GET("/:clusterCode/nodes/:name", k8sHandler.GetNodeDetail)
|
||||||
|
|
||||||
// Deployments
|
// Deployments
|
||||||
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/deployments", k8sHandler.ListDeploymentsNS)
|
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/deployments", k8sHandler.ListDeploymentsNS)
|
||||||
kubernetesRouter.POST("/:clusterCode/namespace/:namespace/deployments/:name/scale", k8sHandler.ScaleDeploymentNS)
|
kubernetesRouter.POST("/:clusterCode/namespace/:namespace/deployments/:name/scale", k8sHandler.ScaleDeploymentNS)
|
||||||
|
|||||||
@@ -209,7 +209,15 @@ type NodeItem struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NodeSpec struct {
|
type NodeSpec struct {
|
||||||
PodCIDR string `json:"podCIDR"`
|
PodCIDR string `json:"podCIDR"`
|
||||||
|
Taints []NodeTaint `json:"taints,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeTaint struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Effect string `json:"effect"`
|
||||||
|
TimeAdded string `json:"timeAdded,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodeStatus struct {
|
type NodeStatus struct {
|
||||||
@@ -223,15 +231,19 @@ type NodeStatus struct {
|
|||||||
type ResourceList map[string]string
|
type ResourceList map[string]string
|
||||||
|
|
||||||
type NodeCondition struct {
|
type NodeCondition struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodeInfo struct {
|
type NodeInfo struct {
|
||||||
KubeletVersion string `json:"kubeletVersion"`
|
KubeletVersion string `json:"kubeletVersion"`
|
||||||
OSImage string `json:"osImage"`
|
OSImage string `json:"osImage"`
|
||||||
KernelVersion string `json:"kernelVersion"`
|
KernelVersion string `json:"kernelVersion"`
|
||||||
ContainerRuntimeVersion string `json:"containerRuntimeVersion"`
|
ContainerRuntimeVersion string `json:"containerRuntimeVersion"`
|
||||||
|
Architecture string `json:"architecture"`
|
||||||
|
OperatingSystem string `json:"operatingSystem"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodeAddress struct {
|
type NodeAddress struct {
|
||||||
@@ -421,6 +433,15 @@ func (c *Client) ListNodes() (*NodeList, error) {
|
|||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetNode fetches a single node by name
|
||||||
|
func (c *Client) GetNode(name string) (*NodeItem, error) {
|
||||||
|
var result NodeItem
|
||||||
|
if err := c.get("/api/v1/nodes/"+name, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListPods fetches all pods across namespaces
|
// ListPods fetches all pods across namespaces
|
||||||
func (c *Client) ListPods() (*PodList, error) {
|
func (c *Client) ListPods() (*PodList, error) {
|
||||||
var result PodList
|
var result PodList
|
||||||
@@ -672,7 +693,7 @@ func (c *Client) ExecInPod(namespace, podName, container string, command []strin
|
|||||||
|
|
||||||
dialer := websocket.Dialer{
|
dialer := websocket.Dialer{
|
||||||
TLSClientConfig: c.TLSConfig,
|
TLSClientConfig: c.TLSConfig,
|
||||||
Subprotocols: []string{"channel.k8s.io"},
|
Subprotocols: []string{"v4.channel.k8s.io", "v3.channel.k8s.io", "channel.k8s.io"},
|
||||||
HandshakeTimeout: 10 * time.Second,
|
HandshakeTimeout: 10 * time.Second,
|
||||||
}
|
}
|
||||||
header := http.Header{}
|
header := http.Header{}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
services:
|
||||||
|
# ======================== Go 后端服务 (连接外部 MySQL/Redis) ========================
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: backend/Dockerfile
|
||||||
|
args:
|
||||||
|
VERSION: ${VERSION:-dev}
|
||||||
|
BUILD_TIME: ${BUILD_TIME:-unknown}
|
||||||
|
container_name: aiops-backend
|
||||||
|
hostname: aiops-backend
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ./backend/config.external.yaml:/app/config.yaml
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
networks:
|
||||||
|
- aiops-network
|
||||||
|
|
||||||
|
# ======================== React 前端 + Nginx ========================
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: aiops-frontend
|
||||||
|
hostname: aiops-frontend
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
depends_on:
|
||||||
|
backend:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- aiops-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
aiops-network:
|
||||||
|
driver: bridge
|
||||||
@@ -14,6 +14,7 @@ import { PlatformManager } from '@/components/cloud/PlatformManager';
|
|||||||
import { CloudBilling } from '@/components/cloud/CloudBilling';
|
import { CloudBilling } from '@/components/cloud/CloudBilling';
|
||||||
import { KubernetesClusters } from '@/components/kubernetes/KubernetesClusters';
|
import { KubernetesClusters } from '@/components/kubernetes/KubernetesClusters';
|
||||||
import ClusterOverviewPage from '@/components/kubernetes/ClusterOverviewPage';
|
import ClusterOverviewPage from '@/components/kubernetes/ClusterOverviewPage';
|
||||||
|
import NodeDetailPage from '@/components/kubernetes/NodeDetailPage';
|
||||||
import NamespaceOverviewPage from '@/components/kubernetes/NamespaceOverviewPage';
|
import NamespaceOverviewPage from '@/components/kubernetes/NamespaceOverviewPage';
|
||||||
import NamespaceResourcePage from '@/components/kubernetes/NamespaceResourcePage';
|
import NamespaceResourcePage from '@/components/kubernetes/NamespaceResourcePage';
|
||||||
import DeploymentDetailPage from '@/components/kubernetes/DeploymentDetailPage';
|
import DeploymentDetailPage from '@/components/kubernetes/DeploymentDetailPage';
|
||||||
@@ -88,6 +89,8 @@ function App() {
|
|||||||
<Route path="metrics/pods" element={<PodMetricsPage />} />
|
<Route path="metrics/pods" element={<PodMetricsPage />} />
|
||||||
<Route path="events" element={<EventsPage />} />
|
<Route path="events" element={<EventsPage />} />
|
||||||
<Route path="capacity" element={<CapacityPage />} />
|
<Route path="capacity" element={<CapacityPage />} />
|
||||||
|
{/* 节点详情 */}
|
||||||
|
<Route path="nodes/:nodeName" element={<NodeDetailPage />} />
|
||||||
{/* 架构拓扑 */}
|
{/* 架构拓扑 */}
|
||||||
<Route path="topology" element={<TopologyView />} />
|
<Route path="topology" element={<TopologyView />} />
|
||||||
{/* 命名空间作用域 */}
|
{/* 命名空间作用域 */}
|
||||||
|
|||||||
@@ -338,11 +338,11 @@ export function KubernetesClusterCard({ clusters }: { clusters: any[] }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-4 gap-2 text-center">
|
<div className="grid grid-cols-4 gap-2 text-center">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-slate-200">{cluster.node_count ?? cluster.nodes ?? 0}</div>
|
<div className="text-sm font-medium text-slate-200">{cluster.node_count ?? 0}</div>
|
||||||
<div className="text-xs text-slate-500">节点</div>
|
<div className="text-xs text-slate-500">节点</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-slate-200">{cluster.pod_count ?? cluster.pods ?? 0}</div>
|
<div className="text-sm font-medium text-slate-200">{cluster.pod_count ?? 0}</div>
|
||||||
<div className="text-xs text-slate-500">Pod</div>
|
<div className="text-xs text-slate-500">Pod</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -158,8 +158,9 @@ export default function ClusterOverviewPage() {
|
|||||||
const memPct = memMb > 0 ? (memUsageNorm / memMb) * 100 : 0;
|
const memPct = memMb > 0 ? (memUsageNorm / memMb) * 100 : 0;
|
||||||
const color = (pct: number) => pct > 80 ? 'bg-red-500' : pct > 60 ? 'bg-yellow-500' : 'bg-emerald-500';
|
const color = (pct: number) => pct > 80 ? 'bg-red-500' : pct > 60 ? 'bg-yellow-500' : 'bg-emerald-500';
|
||||||
return (
|
return (
|
||||||
<TableRow key={n.name} className="border-slate-800 hover:bg-slate-800/30">
|
<TableRow key={n.name} className="border-slate-800 hover:bg-slate-800/30 cursor-pointer"
|
||||||
<TableCell className="font-medium text-slate-200">{n.name}</TableCell>
|
onClick={() => navigate(`/kubernetes/${clusterCode}/nodes/${n.name}`)}>
|
||||||
|
<TableCell className="font-medium text-blue-400 hover:text-blue-300">{n.name}</TableCell>
|
||||||
<TableCell><Badge className={getNodeRole(n)}>{n.role || 'worker'}</Badge></TableCell>
|
<TableCell><Badge className={getNodeRole(n)}>{n.role || 'worker'}</Badge></TableCell>
|
||||||
<TableCell><Badge className={n.status === 'Ready' ? 'bg-emerald-500/20 text-emerald-400' : 'bg-red-500/20 text-red-400'}>{n.status || 'Unknown'}</Badge></TableCell>
|
<TableCell><Badge className={n.status === 'Ready' ? 'bg-emerald-500/20 text-emerald-400' : 'bg-red-500/20 text-red-400'}>{n.status || 'Unknown'}</Badge></TableCell>
|
||||||
<TableCell className="text-sm text-slate-400">{n.ip || '-'}</TableCell>
|
<TableCell className="text-sm text-slate-400">{n.ip || '-'}</TableCell>
|
||||||
|
|||||||
@@ -30,15 +30,14 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster: any; on
|
|||||||
if (!nodes || nodes.length === 0) return { cpuPct: 0, memPct: 0 };
|
if (!nodes || nodes.length === 0) return { cpuPct: 0, memPct: 0 };
|
||||||
let totalCpuPct = 0, totalMemPct = 0, validNodes = 0;
|
let totalCpuPct = 0, totalMemPct = 0, validNodes = 0;
|
||||||
for (const n of nodes) {
|
for (const n of nodes) {
|
||||||
// ClusterOverviewPage 读的是 cpu_usage_m / mem_usage_mi(实时路径)
|
// 后端 ListNodes 返回 cpu_usage_m (如 "122m"), mem_usage_mi (如 "2105Mi"), cpu (核数), memory_mb (MB)
|
||||||
// ClusterDetail 的 nodes 从 getNodes API 获取,也有这些字段
|
|
||||||
const cpuCores = n.cpu || 0;
|
const cpuCores = n.cpu || 0;
|
||||||
const cpuUsageM = n.cpu_usage_m ? parseFloat(String(n.cpu_usage_m).replace('m', '')) : 0;
|
const cpuUsageM = n.cpu_usage_m ? parseFloat(String(n.cpu_usage_m).replace('m', '')) : 0;
|
||||||
const cpuPct = cpuCores > 0 ? (cpuUsageM / 1000 / cpuCores) * 100 : (n.cpu_usage || 0);
|
const cpuPct = cpuCores > 0 ? (cpuUsageM / 1000 / cpuCores) * 100 : 0;
|
||||||
const memMb = n.memory_mb || n.memory || 0;
|
const memMb = n.memory_mb || 0;
|
||||||
const memUsageMi = n.mem_usage_mi ? parseFloat(String(n.mem_usage_mi).replace('Mi', '').replace('Ki', '')) : 0;
|
const memUsageMi = n.mem_usage_mi ? parseFloat(String(n.mem_usage_mi).replace('Mi', '').replace('Ki', '')) : 0;
|
||||||
const memUsageNorm = n.mem_usage_mi?.includes?.('Ki') ? memUsageMi / 1024 : n.mem_usage_mi?.includes?.('Gi') ? memUsageMi * 1024 : memUsageMi;
|
const memUsageNorm = n.mem_usage_mi?.includes?.('Ki') ? memUsageMi / 1024 : n.mem_usage_mi?.includes?.('Gi') ? memUsageMi * 1024 : memUsageMi;
|
||||||
const memPct = memMb > 0 ? (memUsageNorm / memMb) * 100 : (n.memory_usage || 0);
|
const memPct = memMb > 0 ? (memUsageNorm / memMb) * 100 : 0;
|
||||||
if (cpuPct > 0 || memPct > 0) {
|
if (cpuPct > 0 || memPct > 0) {
|
||||||
totalCpuPct += cpuPct;
|
totalCpuPct += cpuPct;
|
||||||
totalMemPct += memPct;
|
totalMemPct += memPct;
|
||||||
@@ -264,12 +263,12 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster: any; on
|
|||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-slate-300">
|
<td className="px-4 py-3 text-sm text-slate-300">
|
||||||
{Number(node.cpu_usage || 0).toFixed(1)}% / {node.cpu}核
|
{node.cpu_usage_m || '0m'} / {node.cpu}核
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-slate-300">
|
<td className="px-4 py-3 text-sm text-slate-300">
|
||||||
{Number(node.memory_usage || 0).toFixed(1)}% / {node.memory}GB
|
{node.mem_usage_mi || '0Mi'} / {node.memory_mb ? (node.memory_mb / 1024).toFixed(0) + 'GB' : '-'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-slate-300">{node.pods ?? node.pod_count ?? 0}</td>
|
<td className="px-4 py-3 text-sm text-slate-300">{node.max_pods ?? 0}</td>
|
||||||
<td className="px-4 py-3 text-sm text-slate-400">{node.os}</td>
|
<td className="px-4 py-3 text-sm text-slate-400">{node.os}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
@@ -322,8 +321,8 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster: any; on
|
|||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-slate-300">{pod.restart_count ?? 0}</td>
|
<td className="px-4 py-3 text-sm text-slate-300">{pod.restart_count ?? 0}</td>
|
||||||
<td className="px-4 py-3 text-sm text-slate-300">{Number(pod.cpu_usage || 0).toFixed(1)}m</td>
|
<td className="px-4 py-3 text-sm text-slate-300">{pod.cpu_usage_m || '0m'}</td>
|
||||||
<td className="px-4 py-3 text-sm text-slate-300">{Number(pod.memory_usage || 0).toFixed(0)}Mi</td>
|
<td className="px-4 py-3 text-sm text-slate-300">{pod.mem_usage_mi || '0Mi'}</td>
|
||||||
<td className="px-4 py-3 text-sm text-slate-400">{pod.node_name}</td>
|
<td className="px-4 py-3 text-sm text-slate-400">{pod.node_name}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -0,0 +1,383 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import {
|
||||||
|
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import {
|
||||||
|
RefreshCw, Loader2, Cpu, Server, Clock, HardDrive, Activity,
|
||||||
|
ChevronRight, Container, Network, Info, AlertTriangle, CheckCircle2,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { kubernetesApi } from '@/services/api';
|
||||||
|
|
||||||
|
export default function NodeDetailPage() {
|
||||||
|
const { clusterCode, nodeName } = useParams<{ clusterCode: string; nodeName: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [data, setData] = useState<any>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
if (!clusterCode || !nodeName) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res: any = await kubernetesApi.getNodeDetail(clusterCode, nodeName);
|
||||||
|
setData(res);
|
||||||
|
} catch { setData(null); } finally { setLoading(false); }
|
||||||
|
}, [clusterCode, nodeName]);
|
||||||
|
|
||||||
|
useEffect(() => { loadData(); }, [loadData]);
|
||||||
|
|
||||||
|
const formatNum = (v: any, suffix = '') => {
|
||||||
|
if (v === undefined || v === null || v === '') return '-';
|
||||||
|
const n = typeof v === 'number' ? v : parseFloat(v);
|
||||||
|
return isNaN(n) ? '-' : `${n.toLocaleString()}${suffix}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resource bar component
|
||||||
|
const ResourceBar = ({ label, used, total, unit, pct, color }: { label: string; used: string; total: string; unit: string; pct: number; color: string }) => {
|
||||||
|
const safePct = Math.min(Math.max(pct || 0, 0), 100);
|
||||||
|
const barColor = safePct > 80 ? 'bg-red-500' : safePct > 60 ? 'bg-amber-500' : 'bg-emerald-500';
|
||||||
|
return (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-slate-400">{label}</span>
|
||||||
|
<span className="text-slate-300 font-medium">
|
||||||
|
{used} / {total} {unit}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-1 h-2 rounded-full bg-slate-700/50 overflow-hidden">
|
||||||
|
<div className={`h-full rounded-full ${barColor} transition-all duration-500`} style={{ width: `${safePct}%` }} />
|
||||||
|
</div>
|
||||||
|
<span className={`text-xs font-semibold min-w-[40px] text-right ${color}`}>
|
||||||
|
{safePct.toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const conditionColor = (type: string, status: string) => {
|
||||||
|
if (status === 'True') {
|
||||||
|
if (type === 'Ready') return 'bg-emerald-500/20 text-emerald-400';
|
||||||
|
// Other True conditions (DiskPressure, MemoryPressure, PIDPressure, NetworkUnavailable) are BAD
|
||||||
|
return 'bg-red-500/20 text-red-400';
|
||||||
|
}
|
||||||
|
return 'bg-slate-500/20 text-slate-400';
|
||||||
|
};
|
||||||
|
|
||||||
|
const taintColor = (effect: string) => {
|
||||||
|
if (effect === 'NoSchedule') return 'bg-red-500/20 text-red-400 border-red-500/30';
|
||||||
|
if (effect === 'NoExecute') return 'bg-red-600/20 text-red-300 border-red-600/30';
|
||||||
|
if (effect === 'PreferNoSchedule') return 'bg-amber-500/20 text-amber-400 border-amber-500/30';
|
||||||
|
return 'bg-slate-500/20 text-slate-400 border-slate-500/30';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Breadcrumb */}
|
||||||
|
<div className="flex items-center gap-2 text-sm text-slate-500">
|
||||||
|
<Link to="/kubernetes" className="hover:text-slate-300 transition-colors">集群</Link>
|
||||||
|
<ChevronRight className="h-3 w-3" />
|
||||||
|
<Link to={`/kubernetes/${clusterCode}`} className="hover:text-slate-300 transition-colors">{clusterCode}</Link>
|
||||||
|
<ChevronRight className="h-3 w-3" />
|
||||||
|
<span className="text-slate-300">{nodeName}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex items-center justify-center w-12 h-12 rounded-xl bg-slate-800 border border-slate-700">
|
||||||
|
<Server className="h-6 w-6 text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h1 className="text-2xl font-bold text-slate-100">{nodeName}</h1>
|
||||||
|
<Badge className={data?.role === 'master' ? 'bg-blue-500/20 text-blue-400 border-blue-500/30' : 'bg-slate-500/20 text-slate-400 border-slate-500/30'}>
|
||||||
|
{data?.role || 'worker'}
|
||||||
|
</Badge>
|
||||||
|
<Badge className={data?.status === 'Ready' ? 'bg-emerald-500/20 text-emerald-400' : 'bg-red-500/20 text-red-400'}>
|
||||||
|
{data?.status || 'Unknown'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-slate-500 mt-0.5">
|
||||||
|
{data?.internal_ip && `${data.internal_ip}`}
|
||||||
|
{data?.hostname && ` · ${data.hostname}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="icon" className="h-9 w-9 border-slate-700 text-slate-400"
|
||||||
|
onClick={loadData} disabled={loading}>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading && !data ? (
|
||||||
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
<CardContent className="py-16 flex items-center justify-center">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin text-slate-500" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : !data ? (
|
||||||
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
<CardContent className="py-16 text-center text-slate-500">
|
||||||
|
<AlertTriangle className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||||
|
无法加载节点详情
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Row 1: System Info + Basic Info */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* System Info */}
|
||||||
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<h2 className="text-sm font-semibold text-slate-200 mb-3 flex items-center gap-2">
|
||||||
|
<Info className="h-4 w-4 text-cyan-400" />系统信息
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-2 text-xs">
|
||||||
|
<div className="flex items-center justify-between py-1.5 border-b border-slate-800/50">
|
||||||
|
<span className="text-slate-500">容器运行时</span>
|
||||||
|
<span className="text-slate-300 font-mono">{data.container_runtime || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between py-1.5 border-b border-slate-800/50">
|
||||||
|
<span className="text-slate-500">Kubelet</span>
|
||||||
|
<span className="text-slate-300 font-mono">{data.kubelet_version || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between py-1.5 border-b border-slate-800/50">
|
||||||
|
<span className="text-slate-500">操作系统</span>
|
||||||
|
<span className="text-slate-300">{data.os_image || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between py-1.5 border-b border-slate-800/50">
|
||||||
|
<span className="text-slate-500">内核版本</span>
|
||||||
|
<span className="text-slate-300 font-mono">{data.kernel_version || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between py-1.5 border-b border-slate-800/50">
|
||||||
|
<span className="text-slate-500">架构</span>
|
||||||
|
<span className="text-slate-300 font-mono">{data.architecture || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between py-1.5 border-b border-slate-800/50">
|
||||||
|
<span className="text-slate-500">创建时间</span>
|
||||||
|
<span className="text-slate-300">{data.creation_time ? new Date(data.creation_time).toLocaleString('zh-CN') : '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between py-1.5">
|
||||||
|
<span className="text-slate-500">Pod CIDR</span>
|
||||||
|
<span className="text-slate-300 font-mono">{data.pod_cidr || '-'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Resource Allocation */}
|
||||||
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<h2 className="text-sm font-semibold text-slate-200 mb-3 flex items-center gap-2">
|
||||||
|
<Activity className="h-4 w-4 text-emerald-400" />资源分配率
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4 mt-4">
|
||||||
|
{/* CPU allocation (requested vs allocatable) */}
|
||||||
|
<ResourceBar
|
||||||
|
label="CPU 已分配"
|
||||||
|
used={`${(data.cpu_request_m / 1000).toFixed(2)} 核`}
|
||||||
|
total={`${data.cpu_allocatable} 核`}
|
||||||
|
unit=""
|
||||||
|
pct={data.cpu_alloc_pct}
|
||||||
|
color="text-emerald-400"
|
||||||
|
/>
|
||||||
|
{/* CPU real usage (from metrics) */}
|
||||||
|
{data.cpu_usage_m && (
|
||||||
|
<ResourceBar
|
||||||
|
label="CPU 实时使用"
|
||||||
|
used={data.cpu_usage_m}
|
||||||
|
total={`${data.cpu_capacity} 核`}
|
||||||
|
unit=""
|
||||||
|
pct={(parseFloat(data.cpu_usage_m.replace('m', '')) / (data.cpu_capacity * 1000)) * 100}
|
||||||
|
color="text-blue-400"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* Memory allocation */}
|
||||||
|
<ResourceBar
|
||||||
|
label="内存已分配"
|
||||||
|
used={`${data.mem_request_mi} Mi`}
|
||||||
|
total={`${data.mem_allocatable_mb} Mi`}
|
||||||
|
unit=""
|
||||||
|
pct={data.mem_alloc_pct}
|
||||||
|
color="text-emerald-400"
|
||||||
|
/>
|
||||||
|
{/* Memory real usage */}
|
||||||
|
{data.mem_usage_mi && (
|
||||||
|
<ResourceBar
|
||||||
|
label="内存实时使用"
|
||||||
|
used={data.mem_usage_mi}
|
||||||
|
total={`${data.mem_capacity_mb} Mi`}
|
||||||
|
unit=""
|
||||||
|
pct={(parseFloat(data.mem_usage_mi.replace('Mi', '').replace('Ki', '').replace('Gi', '')) / data.mem_capacity_mb) * 100}
|
||||||
|
color="text-blue-400"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* Pod allocation */}
|
||||||
|
<ResourceBar
|
||||||
|
label="Pod 数量"
|
||||||
|
used={`${data.pod_count}`}
|
||||||
|
total={`${data.pod_allocatable}`}
|
||||||
|
unit=""
|
||||||
|
pct={data.pod_alloc_pct}
|
||||||
|
color="text-emerald-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Capacity vs Allocatable raw */}
|
||||||
|
<div className="mt-4 pt-3 border-t border-slate-800/50 space-y-1 text-[11px] text-slate-500">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>CPU 容量 / 可分配</span>
|
||||||
|
<span className="font-mono">{data.cpu_capacity} / {data.cpu_allocatable} 核</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>内存容量 / 可分配</span>
|
||||||
|
<span className="font-mono">{data.mem_capacity_mb} / {data.mem_allocatable_mb} Mi</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Pod 容量 / 可分配</span>
|
||||||
|
<span className="font-mono">{data.pod_capacity} / {data.pod_allocatable}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 2: Conditions + Taints */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Conditions */}
|
||||||
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<h2 className="text-sm font-semibold text-slate-200 mb-3 flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-blue-400" />节点条件
|
||||||
|
</h2>
|
||||||
|
{(!data.conditions || data.conditions.length === 0) ? (
|
||||||
|
<p className="text-xs text-slate-500 py-4 text-center">无条件数据</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{data.conditions.map((cond: any, i: number) => (
|
||||||
|
<div key={i} className="flex items-start gap-2 text-xs py-1.5 border-b border-slate-800/30 last:border-0">
|
||||||
|
<Badge className={conditionColor(cond.type, cond.status)}>
|
||||||
|
{cond.status === 'True' ? <CheckCircle2 className="h-3 w-3 mr-1" /> : null}
|
||||||
|
{cond.type}
|
||||||
|
</Badge>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
{cond.reason && <span className="text-slate-400 block">{cond.reason}</span>}
|
||||||
|
{cond.message && <span className="text-slate-600 text-[11px] block truncate">{cond.message}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Taints */}
|
||||||
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<h2 className="text-sm font-semibold text-slate-200 mb-3 flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-4 w-4 text-amber-400" />污点 (Taints)
|
||||||
|
</h2>
|
||||||
|
{(!data.taints || data.taints.length === 0) ? (
|
||||||
|
<p className="text-xs text-slate-500 py-4 text-center">无污点</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{data.taints.map((t: any, i: number) => (
|
||||||
|
<div key={i} className="flex items-center gap-2 text-xs">
|
||||||
|
<Badge className={taintColor(t.effect)}>{t.effect}</Badge>
|
||||||
|
<span className="text-slate-300 font-mono">{t.key}</span>
|
||||||
|
{t.value && <span className="text-slate-500">= {t.value}</span>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 3: Labels */}
|
||||||
|
{data.labels && data.labels.length > 0 && (
|
||||||
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<h2 className="text-sm font-semibold text-slate-200 mb-3 flex items-center gap-2">
|
||||||
|
<Network className="h-4 w-4 text-slate-400" />标签 ({data.labels.length})
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{data.labels.map((l: any, i: number) => (
|
||||||
|
<div key={i} className="flex items-center gap-1 bg-slate-800/50 border border-slate-700/50 rounded px-2 py-0.5 text-[11px]">
|
||||||
|
<span className="text-slate-400">{l.key}</span>
|
||||||
|
<span className="text-slate-600">:</span>
|
||||||
|
<span className="text-slate-300">{l.value}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Row 4: Pods on this node */}
|
||||||
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="px-4 py-3 border-b border-slate-800">
|
||||||
|
<h2 className="text-sm font-semibold text-slate-200 flex items-center gap-2">
|
||||||
|
<Container className="h-4 w-4 text-cyan-400" />
|
||||||
|
节点上的 Pod
|
||||||
|
<Badge className="bg-slate-700/50 text-slate-300">{data.pods?.length || 0}</Badge>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
{(!data.pods || data.pods.length === 0) ? (
|
||||||
|
<p className="text-xs text-slate-500 py-8 text-center">该节点上暂无 Pod</p>
|
||||||
|
) : (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="border-slate-800 hover:bg-transparent">
|
||||||
|
<TableHead className="text-slate-400">命名空间</TableHead>
|
||||||
|
<TableHead className="text-slate-400">名称</TableHead>
|
||||||
|
<TableHead className="text-slate-400">状态</TableHead>
|
||||||
|
<TableHead className="text-slate-400">IP</TableHead>
|
||||||
|
<TableHead className="text-slate-400">CPU 请求</TableHead>
|
||||||
|
<TableHead className="text-slate-400">内存请求</TableHead>
|
||||||
|
<TableHead className="text-slate-400">重启次数</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.pods.map((p: any, i: number) => (
|
||||||
|
<TableRow key={i} className="border-slate-800 hover:bg-slate-800/30">
|
||||||
|
<TableCell className="text-sm text-slate-400">{p.namespace}</TableCell>
|
||||||
|
<TableCell className="text-sm font-medium text-slate-200">{p.name}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge className={
|
||||||
|
p.status === 'Running' ? 'bg-emerald-500/20 text-emerald-400' :
|
||||||
|
p.status === 'Pending' ? 'bg-amber-500/20 text-amber-400' :
|
||||||
|
'bg-red-500/20 text-red-400'
|
||||||
|
}>{p.status}</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs text-slate-400 font-mono">{p.ip || '-'}</TableCell>
|
||||||
|
<TableCell className="text-xs text-slate-400">
|
||||||
|
{p.cpu_request_m > 0 ? `${p.cpu_request_m}m` : '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs text-slate-400">
|
||||||
|
{p.mem_request_mi > 0 ? `${p.mem_request_mi}Mi` : '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs">
|
||||||
|
{p.restart_count > 0 ? (
|
||||||
|
<span className="text-amber-400">{p.restart_count}</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-slate-600">0</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -45,7 +45,14 @@ export function PodFileBrowser({
|
|||||||
setPreview(null);
|
setPreview(null);
|
||||||
try {
|
try {
|
||||||
const res: any = await kubernetesApi.listPodFiles(clusterCode, namespace, podName, path, container || undefined);
|
const res: any = await kubernetesApi.listPodFiles(clusterCode, namespace, podName, path, container || undefined);
|
||||||
const list: FileEntry[] = res?.entries || res?.data?.entries || (Array.isArray(res) ? res : []);
|
// 兼容后端 {files: [...]} 和 {entries: [...]} 两种返回格式
|
||||||
|
const raw: any[] = res?.entries || res?.files || res?.data?.entries || res?.data?.files || (Array.isArray(res) ? res : []);
|
||||||
|
const list: FileEntry[] = raw.map((it: any) => ({
|
||||||
|
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),
|
||||||
|
modTime: it.modTime ?? it.mod_time ?? it.ModTime ?? it.mtime ?? it.modified,
|
||||||
|
})).filter(e => e.name && e.name !== '.');
|
||||||
setEntries(list);
|
setEntries(list);
|
||||||
setCurrentPath(path);
|
setCurrentPath(path);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -175,6 +175,9 @@ export const kubernetesApi = {
|
|||||||
getNodes: async (clusterId: number | string) => {
|
getNodes: async (clusterId: number | string) => {
|
||||||
return http.get(`/kubernetes-clusters/${clusterId}/nodes`);
|
return http.get(`/kubernetes-clusters/${clusterId}/nodes`);
|
||||||
},
|
},
|
||||||
|
getNodeDetail: async (clusterCode: string, nodeName: string) => {
|
||||||
|
return http.get(`/kubernetes/${clusterCode}/nodes/${encodeURIComponent(nodeName)}`);
|
||||||
|
},
|
||||||
getPods: async (clusterId: number | string) => {
|
getPods: async (clusterId: number | string) => {
|
||||||
return http.get(`/kubernetes-clusters/${clusterId}/pods`);
|
return http.get(`/kubernetes-clusters/${clusterId}/pods`);
|
||||||
},
|
},
|
||||||
|
|||||||
+17
-11
@@ -17,21 +17,27 @@ export interface Server {
|
|||||||
diskUsage: number;
|
diskUsage: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kubernetes集群
|
// Kubernetes集群 — 字段与后端 kubernetes_clusters 表 + JSON tag 一致(snake_case)
|
||||||
export interface KubernetesCluster {
|
export interface KubernetesCluster {
|
||||||
id: string;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
code?: string; // 唯一标识,用于 Kuboard 路由 // Logan.Li
|
code: string; // 唯一标识,必填 // Logan.Li
|
||||||
version: string;
|
version: string;
|
||||||
nodes: number;
|
|
||||||
pods: number;
|
|
||||||
services: number;
|
|
||||||
deployments: number;
|
|
||||||
status: 'healthy' | 'warning' | 'error';
|
status: 'healthy' | 'warning' | 'error';
|
||||||
cpuUsage: number;
|
config_type: number; // 0=token, 1=kubeconfig
|
||||||
memoryUsage: number;
|
api_server: string;
|
||||||
createdAt: string;
|
api_endpoint: string;
|
||||||
namespace: string;
|
node_count: number;
|
||||||
|
pod_count: number;
|
||||||
|
service_count: number;
|
||||||
|
deployment_count: number;
|
||||||
|
cpu_usage: number;
|
||||||
|
memory_usage: number;
|
||||||
|
description: string;
|
||||||
|
labels: string; // JSON string
|
||||||
|
last_sync_at: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// K8s节点
|
// K8s节点
|
||||||
|
|||||||
Reference in new issue
Block a user