diff --git a/backend/internal/api/handlers/k8s.go b/backend/internal/api/handlers/k8s.go index e467622..d2ff797 100644 --- a/backend/internal/api/handlers/k8s.go +++ b/backend/internal/api/handlers/k8s.go @@ -145,7 +145,8 @@ func (h *K8sHandler) CreateCluster(c *gin.Context) { func (h *K8sHandler) UpdateCluster(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) - var req models.CreateK8sClusterRequest + // 用 map 接收,避免 binding:"required" 校验 // Logan.Li + var req map[string]interface{} if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.Response{ Code: 400, @@ -163,15 +164,39 @@ func (h *K8sHandler) UpdateCluster(c *gin.Context) { return } - updates := map[string]interface{}{ - "name": req.Name, - "version": req.Version, - "config_type": req.ConfigType, - "api_server": req.APIServer, - "api_endpoint": req.APIServer, - "token": req.Token, - "kube_config": req.KubeConfig, - "description": req.Description, + // 逐字段更新:空值不覆盖原值(防止凭据被清空) // Logan.Li + updates := map[string]interface{}{} + if v, ok := req["name"]; ok && v != nil && v != "" { + updates["name"] = v + } + if v, ok := req["version"]; ok { + updates["version"] = v + } + if v, ok := req["description"]; ok { + updates["description"] = v + } + if v, ok := req["config_type"]; ok { + updates["config_type"] = v + } + if v, ok := req["api_server"]; ok && v != nil && v != "" { + updates["api_server"] = v + updates["api_endpoint"] = v + } + // Token / KubeConfig:空值不覆盖(前端编辑时可能拿不到原值) + if v, ok := req["token"]; ok && v != nil && v != "" { + updates["token"] = v + } + if v, ok := req["kube_config"]; ok && v != nil && v != "" { + updates["kube_config"] = v + } + + if len(updates) == 0 { + c.JSON(http.StatusOK, models.Response{ + Code: 200, + Message: "无更新内容", + Data: cluster, + }) + return } if err := h.db.Model(&cluster).Updates(updates).Error; err != nil { @@ -191,6 +216,44 @@ func (h *K8sHandler) UpdateCluster(c *gin.Context) { }) } +// GetClusterDetail 返回集群完整信息(含 token/kube_config,供编辑回填) // Logan.Li +func (h *K8sHandler) GetClusterDetail(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + + var cluster models.K8sCluster + if err := h.db.First(&cluster, id).Error; err != nil { + c.JSON(http.StatusNotFound, models.Response{ + Code: 404, + Message: "集群不存在", + }) + return + } + + // 返回时手动脱敏:token/kube_config 只返回是否存在标记,不返回原文 + c.JSON(http.StatusOK, models.Response{ + Code: 200, + Message: "success", + Data: map[string]interface{}{ + "id": cluster.ID, + "name": cluster.Name, + "code": cluster.Code, + "version": cluster.Version, + "status": cluster.Status, + "config_type": cluster.ConfigType, + "api_server": cluster.APIServer, + "api_endpoint": cluster.APIEndpoint, + "description": cluster.Description, + "has_token": cluster.Token != "", + "has_kube_config": cluster.KubeConfig != "", + "node_count": cluster.NodeCount, + "pod_count": cluster.PodCount, + "labels": cluster.Labels, + "created_at": cluster.CreatedAt, + "updated_at": cluster.UpdatedAt, + }, + }) +} + // DeleteCluster 删除集群 // @Summary 删除集群 // @Description 删除指定K8s集群 diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index 1efce9b..b3b72f4 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -236,6 +236,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c { clusters.GET("", k8sHandler.ListClusters) clusters.GET("/:id", k8sHandler.GetCluster) + clusters.GET("/:id/detail", k8sHandler.GetClusterDetail) // 编辑用详情(含 has_token 标记) // Logan.Li clusters.POST("", k8sHandler.CreateCluster) clusters.PUT("/:id", k8sHandler.UpdateCluster) clusters.DELETE("/:id", k8sHandler.DeleteCluster) diff --git a/frontend/src/components/k8s/K8sClusterDialog.tsx b/frontend/src/components/k8s/K8sClusterDialog.tsx index 9218602..6b16dd3 100644 --- a/frontend/src/components/k8s/K8sClusterDialog.tsx +++ b/frontend/src/components/k8s/K8sClusterDialog.tsx @@ -37,21 +37,42 @@ export function K8sClusterDialog({ }) { const [formData, setFormData] = useState({ ...emptyForm }); const [submitting, setSubmitting] = useState(false); + const [hasToken, setHasToken] = useState(false); // 已配置 Token 标记 // Logan.Li + const [hasKubeConfig, setHasKubeConfig] = useState(false); // 已配置 Kubeconfig 标记 - // 打开时同步表单(编辑时填充,新建时重置) + // 打开时同步表单(编辑时拉取详情,新建时重置) useEffect(() => { if (!open) return; - if (editingCluster) { - setFormData({ - name: editingCluster.name || '', - version: editingCluster.version || '', - description: editingCluster.description || '', - config_type: Number(editingCluster.config_type ?? 1), - api_server: editingCluster.api_server || '', - token: editingCluster.token || '', - kube_config: editingCluster.kube_config || '', + if (editingCluster && editingCluster.id) { + // 编辑模式:拉取详情获取 has_token/has_kube_config 标记 // Logan.Li + k8sApi.getClusterDetail(editingCluster.id).then((res: any) => { + const d = res || {}; + setHasToken(!!d.has_token); + setHasKubeConfig(!!d.has_kube_config); + setFormData({ + name: d.name || '', + version: d.version || '', + description: d.description || '', + config_type: Number(d.config_type ?? 1), + api_server: d.api_server || '', + token: '', // 不回填原文,留空不修改 + kube_config: '', // 不回填原文,留空不修改 + }); + }).catch(() => { + // 降级:用列表数据 + setFormData({ + name: editingCluster.name || '', + version: editingCluster.version || '', + description: editingCluster.description || '', + config_type: Number(editingCluster.config_type ?? 1), + api_server: editingCluster.api_server || '', + token: '', + kube_config: '', + }); }); } else { + setHasToken(false); + setHasKubeConfig(false); setFormData({ ...emptyForm }); } }, [open, editingCluster]); @@ -171,28 +192,34 @@ export function K8sClusterDialog({ {formData.config_type === 0 ? (