feat(k8s): M6 编辑器基础设施 + 基本信息 Tab + YAML编辑器

- DeploymentWorkload: 三模式切换(查看/编辑/YAML编辑)+ EditModeContainer Tab 导航系统
- DeploymentEditBasic: 新增基本信息 Tab — replicas/labels/annotations/selector 编辑
- YamlViewer: 升级为 CodeMirror 6(语法高亮+行号+折叠+括号匹配),新增 inline 内联模式
- DeploymentDetailPage: mode 类型扩展为 view|edit|yaml
- 路由 :mode 参数自动支持 yaml 值
- M7-M9 Tab 预留占位(容器信息/存储/高级/服务)
This commit is contained in:
attacker committed 2026-06-23 05:15:42 +08:00
1 parent 2e66908e20
commit 68a954c1c5
9 files changed
+855 -42

No files matched your search

+6 -5
View File
@@ -44,8 +44,8 @@
|------|:------:|------| |------|:------:|------|
| Pod 文件上传 | ✅ | P2 | ✅ 已完成 M3 | | Pod 文件上传 | ✅ | P2 | ✅ 已完成 M3 |
| ConfigMap/Secret 编辑 | ✅ | P2 | ✅ 已完成 M4 | | ConfigMap/Secret 编辑 | ✅ | P2 | ✅ 已完成 M4 |
| **Deployment 可视化编辑器** | **P1** | **M6-M9 进行中 — 当前编辑器只有 image/resources/probe(httpGet)/env/mounts,缺容器完整编辑/存储/调度/服务** | | **Deployment 可视化编辑器** | **P1** | **M6已完成(三模式切换+基本信息Tab+YAML编辑器CodeMirror),M7-M9 进行中** |
| **YAML 编辑器增强** | **P1** | **M6 — 当前是纯 textarea,需 Monaco/CodeMirror 语法高亮** | | **YAML 编辑器增强** | **P1** | **✅ 已完成 M6 — CodeMirror 6 语法高亮+行号+折叠+内联模式** |
| CI/CD 集成菜单项 | ✅ | P3 | ✅ 已完成 M5 — 跳转流水线页面 | | CI/CD 集成菜单项 | ✅ | P3 | ✅ 已完成 M5 — 跳转流水线页面 |
| Deployment 暂停更新 | ✅ | P3 | ✅ 已完成 M5 — pause/resume 端点 + 暂停 Badge | | Deployment 暂停更新 | ✅ | P3 | ✅ 已完成 M5 — pause/resume 端点 + 暂停 Badge |
@@ -53,10 +53,11 @@
| 组件 | 行数 | 状态 | 说明 | | 组件 | 行数 | 状态 | 说明 |
|------|------|:----:|------| |------|------|:----:|------|
| `DeploymentWorkload.tsx` | 569 | ✅ 可用 | Shell 框架(header + 模式切换 + 操作下拉 + 模态框系统) | | `DeploymentWorkload.tsx` | 680 | ✅ 可用 | Shell 框架(header + 三模式切换 + EditModeContainer Tab 系统) |
| `DeploymentView.tsx` | 395 | ✅ 完整 | 查看模式(状态卡片/容器模板/Pod实例/版本历史/RS) | | `DeploymentView.tsx` | 395 | ✅ 完整 | 查看模式(状态卡片/容器模板/Pod实例/版本历史/RS) |
| `DeploymentEdit.tsx` | 374 | ⚠️ 浅层 | **只覆盖 image/resources/probe(httpGet)/env/mounts,将在 M9 删除** | | `DeploymentEditBasic.tsx` | 340 | ✅ 新增 M6 | 基本信息 Tab — replicas/labels/annotations/selector 编辑 |
| `YamlViewer.tsx` | 139 | ⚠️ 基础 | **纯 textarea,无语法高亮/行号/校验,将在 M6 升级** | | `DeploymentEdit.tsx` | 374 | ⚠️ 过渡 | M6 临时放在容器Tab下方,M7 将被完整容器编辑器取代 |
| `YamlViewer.tsx` | 220 | ✅ 升级 M6 | CodeMirror 6 YAML 语法高亮+行号+折叠,弹窗+内联双模式 |
| `DeploymentDetail.tsx` | 719 | 💀 遗留 | **旧版 tab 式详情页,仅 KubernetesWorkloads.tsx 引用,待清理** | | `DeploymentDetail.tsx` | 719 | 💀 遗留 | **旧版 tab 式详情页,仅 KubernetesWorkloads.tsx 引用,待清理** |
| 后端 GetYAML/UpdateYAML | — | ✅ 完善 | 支持 12 种资源,PUT 直通 K8s API,**纯前端工程无需改后端** | | 后端 GetYAML/UpdateYAML | — | ✅ 完善 | 支持 12 种资源,PUT 直通 K8s API,**纯前端工程无需改后端** |
+12 -7
View File
@@ -88,19 +88,24 @@
## 当前冲刺:P1 — Deployment 可视化编辑器(Kuboard 风格) ## 当前冲刺:P1 — Deployment 可视化编辑器(Kuboard 风格)
### M6: 编辑器基础设施 + 基本信息 Tab + YAML编辑器 [pending] ### M6: 编辑器基础设施 + 基本信息 Tab + YAML编辑器 [done]
**目标**: 三模式切换(查看/可视化编辑/YAML编辑)+ 基本信息 Tab **目标**: 三模式切换(查看/可视化编辑/YAML编辑)+ 基本信息 Tab
**改动**: **改动**:
- `DeploymentWorkload.tsx`: 模式切换从 2 tab → 3 tab(查看模式 / 编辑模式 / YAML编辑) - `DeploymentWorkload.tsx`: 模式切换从 2 tab → 3 tab(查看模式 / 编辑模式 / YAML编辑)
- 新增 `DeploymentEditBasic.tsx`: 基本信息 Tab — name/namespace/labels/annotations/replicas - 新增 `DeploymentEditBasic.tsx`: 基本信息 Tab — name/namespace(只读) + replicas/labels/annotations/selector(可编辑)
- `YamlViewer.tsx` 升级: 引入 Monaco Editor(或 CodeMirror),支持 YAML 语法高亮 + 行号 + 基本校验 - `YamlViewer.tsx` 升级: 引入 CodeMirror 6(`@uiw/react-codemirror` + `@codemirror/lang-yaml`),支持 YAML 语法高亮 + 行号 + 折叠 + 括号匹配
- 路由扩展: `:mode` 增加 `yaml` 值 - `YamlViewer.tsx` 新增 `inline` 模式 — 非弹窗内联展示,适合 YAML 编辑 Tab
- `DeploymentDetailPage.tsx`: `mode` 类型扩展为 `'view' | 'edit' | 'yaml'`
- `DeploymentWorkload.tsx` 新增 `EditModeContainer` — 编辑模式 Tab 导航(基本信息✅ / 容器信息M7 / 存储M8 / 高级M8 / 服务M9)
- 路由 `:mode` 参数自动支持 `yaml` 值(无需改 App.tsx 路由表)
**验收标准**: **验收标准**:
- [ ] 三模式 tab 切换正常,YAML 模式打开 Monaco 编辑器 - [x] 三模式 tab 切换正常,YAML 模式打开 CodeMirror 编辑器
- [ ] 基本信息 Tab 可编辑 labels/annotations/replicas 并保存 - [x] 基本信息 Tab 可编辑 labels/annotations/replicas 并保存
- [ ] YAML 编辑器有语法高亮和行号 - [x] YAML 编辑器有语法高亮和行号
- [x] YAML 编辑器支持内联模式(inline prop)
- [x] 编辑模式 Tab 导航系统就绪(M7-M9 预留占位)
--- ---
+40
View File
@@ -136,3 +136,43 @@ curl -s "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespace/devops/d
- [x] ID 路由 + Kuboard code 路由均可用 - [x] ID 路由 + Kuboard code 路由均可用
- [x] 前端操作菜单显示「暂停更新/恢复更新」动态切换 - [x] 前端操作菜单显示「暂停更新/恢复更新」动态切换
- [x] CI/CD 发布按钮可跳转到流水线页面 - [x] CI/CD 发布按钮可跳转到流水线页面
---
## M6: 编辑器基础设施 + 基本信息 Tab + YAML编辑器
### 验证用例
```bash
# U1: 三模式路由 SPA fallback 200(非 301/404)
curl -s -o /dev/null -w "%{http_code}" \
"http://127.0.0.1/kubernetes/prod-cluster/namespace/devops/workload/yaml/Deployment/redis"
# 期望: 200
# U2: YAML GET API — CodeMirror 编辑器数据源
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespace/devops/deployments/redis/yaml" \
-H "Authorization: Bearer *** | head -5
# 期望: apiVersion: apps/v1 / kind: Deployment / metadata:
# U3: 前端构建含 CodeMirror 依赖
curl -s http://127.0.0.1/ | grep -oP 'index-[A-Za-z0-9]+\.js'
# 期望: 新 hash(每次构建变化)
# U4: TypeScript 编译无错误
cd frontend && npx tsc --noEmit
# 期望: 退出码 0
# U5: Vite 生产构建成功
cd frontend && npm run build 2>&1 | tail -3
# 期望: built in Ns
```
### 通过标准
- [x] U1: yaml 路由 SPA fallback 返回 200
- [x] U2: YAML API 返回有效 K8s manifest(apiVersion/kind/metadata 齐全)
- [x] U3: 前端 bundle 含 CodeMirror(codemirror 关键字在 JS chunk 中)
- [x] U4: TypeScript 零编译错误
- [x] U5: Vite 生产构建成功
- [x] YamlViewer 支持弹窗模式 + 内联模式(inline prop)
- [x] DeploymentEditBasic 可编辑 replicas/labels/annotations/selector
- [x] 编辑模式 Tab 导航系统就绪(基本信息可用 + M7-M9 预留)
+264 -2
View File
@@ -8,6 +8,10 @@
"name": "my-app", "name": "my-app",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@codemirror/lang-yaml": "^6.1.3",
"@codemirror/state": "^6.6.0",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.43.1",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-alert-dialog": "^1.1.15",
@@ -38,6 +42,7 @@
"@tanstack/react-query": "^5.95.2", "@tanstack/react-query": "^5.95.2",
"@tanstack/react-query-devtools": "^5.95.2", "@tanstack/react-query-devtools": "^5.95.2",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@uiw/react-codemirror": "^4.25.10",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
@@ -127,6 +132,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@@ -1847,6 +1853,115 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@codemirror/autocomplete": {
"version": "6.20.3",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz",
"integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz",
"integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.6.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/lang-yaml": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.3.tgz",
"integrity": "sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.2.0",
"@lezer/lr": "^1.0.0",
"@lezer/yaml": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.12.3",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz",
"integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz",
"integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.42.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz",
"integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.37.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz",
"integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/theme-one-dark": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.43.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.1.tgz",
"integrity": "sha512-+BIjw/AG3tDQ4pJgTLPYdAW25eDE66YsvM4LKyVPgGzVgZ4a9Wj1SRX8kPVKgBDdPt8oHtZ15F0qx7p0oOHdHw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/state": "^6.6.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@date-fns/tz": { "node_modules/@date-fns/tz": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz", "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz",
@@ -2604,6 +2719,47 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@lezer/common": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
"license": "MIT"
},
"node_modules/@lezer/highlight": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.3.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/yaml": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz",
"integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.4.0"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -4639,6 +4795,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.95.2.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.95.2.tgz",
"integrity": "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA==", "integrity": "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@tanstack/query-core": "5.95.2" "@tanstack/query-core": "5.95.2"
}, },
@@ -4801,6 +4958,7 @@
"integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
@@ -4810,6 +4968,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@@ -4820,6 +4979,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
@@ -4890,6 +5050,7 @@
"integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==", "integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/scope-manager": "8.52.0",
"@typescript-eslint/types": "8.52.0", "@typescript-eslint/types": "8.52.0",
@@ -5114,6 +5275,59 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
} }
}, },
"node_modules/@uiw/codemirror-extensions-basic-setup": {
"version": "4.25.10",
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.10.tgz",
"integrity": "sha512-P3vytLlpE62KYSWrMUnwDCv2lvaQDuDZzyj03mHntuHo5bSl34fRZpjTY3kQTPGuXHxkGSYpoPFFj+hMTqaaMQ==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@codemirror/autocomplete": ">=6.0.0",
"@codemirror/commands": ">=6.0.0",
"@codemirror/language": ">=6.0.0",
"@codemirror/lint": ">=6.0.0",
"@codemirror/search": ">=6.0.0",
"@codemirror/state": ">=6.0.0",
"@codemirror/view": ">=6.0.0"
}
},
"node_modules/@uiw/react-codemirror": {
"version": "4.25.10",
"resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.10.tgz",
"integrity": "sha512-DzgSMwM5qzB7v1FIb4gEeriYt67iiay756/HIOM9mAbeOVK0MO7rqefHf0O5c0269pJKMW7AH9FjclExD23V9w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.6",
"@codemirror/commands": "^6.1.0",
"@codemirror/state": "^6.1.1",
"@codemirror/theme-one-dark": "^6.0.0",
"@uiw/codemirror-extensions-basic-setup": "4.25.10",
"codemirror": "^6.0.0"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@babel/runtime": ">=7.11.0",
"@codemirror/state": ">=6.0.0",
"@codemirror/theme-one-dark": ">=6.0.0",
"@codemirror/view": ">=6.0.0",
"codemirror": ">=6.0.0",
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
}
},
"node_modules/@vitejs/plugin-react": { "node_modules/@vitejs/plugin-react": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz",
@@ -5141,6 +5355,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -5404,6 +5619,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -5551,6 +5767,21 @@
"react-dom": "^18 || ^19 || ^19.0.0-rc" "react-dom": "^18 || ^19 || ^19.0.0-rc"
} }
}, },
"node_modules/codemirror": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -5622,6 +5853,12 @@
"url": "https://opencollective.com/core-js" "url": "https://opencollective.com/core-js"
} }
}, },
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -5865,7 +6102,8 @@
"version": "8.6.0", "version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/embla-carousel-react": { "node_modules/embla-carousel-react": {
"version": "8.6.0", "version": "8.6.0",
@@ -5960,6 +6198,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -6551,6 +6790,7 @@
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
@@ -7005,6 +7245,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -7052,6 +7293,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -7258,6 +7500,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -7288,6 +7531,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@@ -7300,6 +7544,7 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.70.0.tgz", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.70.0.tgz",
"integrity": "sha512-COOMajS4FI3Wuwrs3GPpi/Jeef/5W1DRR84Yl5/ShlT3dKVFUfoGiEZ/QE6Uw8P4T2/CLJdcTVYKvWBMQTEpvw==", "integrity": "sha512-COOMajS4FI3Wuwrs3GPpi/Jeef/5W1DRR84Yl5/ShlT3dKVFUfoGiEZ/QE6Uw8P4T2/CLJdcTVYKvWBMQTEpvw==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@@ -7790,6 +8035,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/style-mod": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
"license": "MIT"
},
"node_modules/sucrase": { "node_modules/sucrase": {
"version": "3.35.1", "version": "3.35.1",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
@@ -7855,6 +8106,7 @@
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2", "arg": "^5.0.2",
@@ -8011,6 +8263,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -8235,6 +8488,7 @@
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -8304,6 +8558,12 @@
} }
} }
}, },
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -8335,7 +8595,8 @@
"resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
"integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==",
"deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/xterm-addon-fit": { "node_modules/xterm-addon-fit": {
"version": "0.8.0", "version": "0.8.0",
@@ -8382,6 +8643,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
+5
View File
@@ -10,6 +10,10 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@codemirror/lang-yaml": "^6.1.3",
"@codemirror/state": "^6.6.0",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.43.1",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-alert-dialog": "^1.1.15",
@@ -40,6 +44,7 @@
"@tanstack/react-query": "^5.95.2", "@tanstack/react-query": "^5.95.2",
"@tanstack/react-query-devtools": "^5.95.2", "@tanstack/react-query-devtools": "^5.95.2",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@uiw/react-codemirror": "^4.25.10",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
@@ -1,19 +1,21 @@
// DeploymentDetailPage.tsx — 路由包装:解析 cluster/namespace/mode/name // DeploymentDetailPage.tsx — 路由包装:解析 cluster/namespace/mode/name
// 支持 Kuboard 风格双模式路由: // 支持 Kuboard 风格三模式路由:
// view: /kubernetes/:c/namespace/:n/workload/view/Deployment/:name // view: /kubernetes/:c/namespace/:n/workload/view/Deployment/:name
// edit: /kubernetes/:c/namespace/:n/workload/edit/Deployment/:name // edit: /kubernetes/:c/namespace/:n/workload/edit/Deployment/:name
// yaml: /kubernetes/:c/namespace/:n/workload/yaml/Deployment/:name
// 兼容旧路由: /kubernetes/:c/namespace/:n/deployments/:name(默认 view) // 兼容旧路由: /kubernetes/:c/namespace/:n/deployments/:name(默认 view)
// 作者: Logan <admin@attacker.club> // 作者: Logan <admin@attacker.club>
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { DeploymentWorkload } from './DeploymentWorkload'; import { DeploymentWorkload } from './DeploymentWorkload';
export type WorkloadMode = 'view' | 'edit' | 'yaml';
export default function DeploymentDetailPage() { export default function DeploymentDetailPage() {
// 兼容两种路由参数命名:旧 :name 与新 :name(路径段不同)
const params = useParams<{ const params = useParams<{
clusterCode: string; clusterCode: string;
namespace: string; namespace: string;
name: string; name: string;
mode?: string; // 'view' | 'edit'(新路由) mode?: string; // 'view' | 'edit' | 'yaml'(新路由)
workloadKind?: string; // 'Deployment'(新路由,预留扩展) workloadKind?: string; // 'Deployment'(新路由,预留扩展)
}>(); }>();
@@ -21,10 +23,10 @@ export default function DeploymentDetailPage() {
const clusterCode = params.clusterCode || ''; const clusterCode = params.clusterCode || '';
const namespace = params.namespace || ''; const namespace = params.namespace || '';
const deploymentName = params.name || ''; const deploymentName = params.name || '';
const currentMode: 'view' | 'edit' = params.mode === 'edit' ? 'edit' : 'view'; const currentMode: WorkloadMode = (params.mode === 'edit' || params.mode === 'yaml') ? params.mode : 'view';
// 模式切换:在 view/edit 路由间跳转 // 模式切换:在 view/edit/yaml 路由间跳转
const handleModeChange = (m: 'view' | 'edit') => { const handleModeChange = (m: WorkloadMode) => {
if (m === currentMode) return; if (m === currentMode) return;
navigate( navigate(
`/kubernetes/${clusterCode}/namespace/${namespace}/workload/${m}/Deployment/${deploymentName}`, `/kubernetes/${clusterCode}/namespace/${namespace}/workload/${m}/Deployment/${deploymentName}`,
@@ -0,0 +1,298 @@
// DeploymentEditBasic.tsx — 编辑器「基本信息」Tab
// 编辑内容:replicas / labels / annotations(name/namespace 只读)
// 作者: Logan <admin@attacker.club>
import { useState, useEffect, useCallback } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Plus, Trash2, Save, Loader2, Tags, FileText, Hash, Lock } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import { kubernetesApi } from '@/services/api';
import { cleanYAML } from './k8s-yaml-utils';
interface DeploymentEditBasicProps {
clusterCode: string;
namespace: string;
deploymentName: string;
/** 从后端拿到的 manifest 对象 */
manifest: any | null;
onSaved: () => void;
onCancel: () => void;
}
/** 将 metadata.labels 对象转为 key-value 数组 */
function objToKv(obj: Record<string, string> | undefined): Array<{ key: string; value: string }> {
if (!obj || typeof obj !== 'object') return [];
return Object.entries(obj).map(([key, value]) => ({ key, value: String(value) }));
}
/** 将 key-value 数组转为对象(去重,空 key 跳过) */
function kvToObj(arr: Array<{ key: string; value: string }>): Record<string, string> {
const out: Record<string, string> = {};
for (const { key, value } of arr) {
const k = key.trim();
if (k) out[k] = value;
}
return out;
}
export function DeploymentEditBasic({ clusterCode, namespace, deploymentName, manifest, onSaved, onCancel }: DeploymentEditBasicProps) {
const { toast } = useToast();
const [saving, setSaving] = useState(false);
const [loading, setLoading] = useState(!manifest);
const [rawManifest, setRawManifest] = useState<any>(manifest || {});
const [replicas, setReplicas] = useState(1);
const [labels, setLabels] = useState<Array<{ key: string; value: string }>>([]);
const [annotations, setAnnotations] = useState<Array<{ key: string; value: string }>>([]);
const [selectorLabels, setSelectorLabels] = useState<Array<{ key: string; value: string }>>([]);
// 初始化数据
useEffect(() => {
const m = manifest;
if (!m) { setLoading(false); return; }
setRawManifest(m);
setReplicas(m?.spec?.replicas ?? 1);
setLabels(objToKv(m?.metadata?.labels));
setAnnotations(objToKv(m?.metadata?.annotations));
setSelectorLabels(objToKv(m?.spec?.selector?.matchLabels));
setLoading(false);
}, [manifest]);
// ============ Labels ============
const updateLabel = (i: number, patch: Partial<{ key: string; value: string }>) => {
setLabels(prev => prev.map((l, idx) => (idx === i ? { ...l, ...patch } : l)));
};
const addLabel = () => setLabels(prev => [...prev, { key: '', value: '' }]);
const removeLabel = (i: number) => setLabels(prev => prev.filter((_, idx) => idx !== i));
// ============ Annotations ============
const updateAnnotation = (i: number, patch: Partial<{ key: string; value: string }>) => {
setAnnotations(prev => prev.map((a, idx) => (idx === i ? { ...a, ...patch } : a)));
};
const addAnnotation = () => setAnnotations(prev => [...prev, { key: '', value: '' }]);
const removeAnnotation = (i: number) => setAnnotations(prev => prev.filter((_, idx) => idx !== i));
// ============ Selector (只读显示) ============
const updateSelector = (i: number, patch: Partial<{ key: string; value: string }>) => {
setSelectorLabels(prev => prev.map((s, idx) => (idx === i ? { ...s, ...patch } : s)));
};
const addSelector = () => setSelectorLabels(prev => [...prev, { key: '', value: '' }]);
const removeSelector = (i: number) => setSelectorLabels(prev => prev.filter((_, idx) => idx !== i));
// ============ 序列化并保存 ============
const save = async () => {
setSaving(true);
try {
const base = JSON.parse(JSON.stringify(rawManifest));
// 副本数
base.spec = base.spec || {};
base.spec.replicas = replicas;
// labels
const labelObj = kvToObj(labels);
base.metadata = base.metadata || {};
base.metadata.labels = Object.keys(labelObj).length > 0 ? labelObj : undefined;
// annotations(空则删除 key)
const annObj = kvToObj(annotations);
base.metadata.annotations = Object.keys(annObj).length > 0 ? annObj : undefined;
// selector — 同步更新 spec.selector.matchLabels 和 spec.template.metadata.labels
// ⚠️ selector 必须和 template labels 一致,否则 Deployment 会拒绝
const selectorObj = kvToObj(selectorLabels);
if (Object.keys(selectorObj).length > 0) {
base.spec.selector = base.spec.selector || {};
base.spec.selector.matchLabels = selectorObj;
base.spec.template = base.spec.template || {};
base.spec.template.metadata = base.spec.template.metadata || {};
base.spec.template.metadata.labels = { ...base.spec.template.metadata.labels, ...selectorObj };
}
const json = JSON.stringify(base, null, 2);
await kubernetesApi.updateResourceYaml(clusterCode, namespace, 'deployments', deploymentName, json);
toast({ title: '基本信息已保存', description: `${deploymentName} 更新已提交` });
onSaved();
} catch (e: any) {
toast({ title: '保存失败', description: e?.message || String(e), variant: 'destructive' });
} finally { setSaving(false); }
};
if (loading) {
return (
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-12 text-center">
<Loader2 className="h-6 w-6 animate-spin mx-auto text-slate-500" />
<p className="text-slate-500 mt-3 text-sm">加载中...</p>
</CardContent>
</Card>
);
}
if (!manifest) {
return (
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-8 text-center text-slate-500">
未获取到 manifest 数据,请使用 YAML 编辑模式直接修改。
</CardContent>
</Card>
);
}
return (
<div className="space-y-4">
{/* 副本数 */}
<Card className="bg-slate-900 border-slate-800">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
<Hash className="h-4 w-4 text-blue-400" />
副本数 (Replicas)
</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<div className="flex items-center gap-3">
<Input
type="number"
min={0}
value={replicas}
onChange={e => setReplicas(Math.max(0, Number(e.target.value) || 0))}
className="w-32 bg-slate-800 border-slate-700 text-slate-200"
/>
<span className="text-xs text-slate-500">期望运行的 Pod 实例数量</span>
</div>
</CardContent>
</Card>
{/* 基本标识(只读) */}
<Card className="bg-slate-900 border-slate-800">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
<Lock className="h-4 w-4 text-slate-500" />
基本标识(只读)
</CardTitle>
</CardHeader>
<CardContent className="pt-0 grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-400 text-xs">名称 (name)</Label>
<Input value={deploymentName} readOnly
className="mt-1 bg-slate-950 border-slate-800 text-slate-500 font-mono text-sm cursor-not-allowed" />
</div>
<div>
<Label className="text-slate-400 text-xs">命名空间 (namespace)</Label>
<Input value={namespace} readOnly
className="mt-1 bg-slate-950 border-slate-800 text-slate-500 font-mono text-sm cursor-not-allowed" />
</div>
</CardContent>
</Card>
{/* Labels */}
<Card className="bg-slate-900 border-slate-800">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
<Tags className="h-4 w-4 text-emerald-400" />
标签 (Labels)
<Badge variant="outline" className="border-slate-700 text-slate-500 text-[10px]">{labels.length}</Badge>
</CardTitle>
<Button size="sm" variant="outline" className="h-7 border-slate-700 text-slate-300 text-xs" onClick={addLabel}>
<Plus className="h-3 w-3 mr-1" />新增
</Button>
</div>
</CardHeader>
<CardContent className="pt-0 space-y-1.5">
{labels.length === 0 && <p className="text-[11px] text-slate-600">无标签</p>}
{labels.map((l, i) => (
<div key={i} className="grid grid-cols-[1fr_1.4fr_auto] gap-2">
<Input value={l.key} placeholder="app"
onChange={e => updateLabel(i, { key: e.target.value })}
className="bg-slate-800 border-slate-700 text-slate-200 font-mono text-xs h-8" />
<Input value={l.value} placeholder="nginx"
onChange={e => updateLabel(i, { value: e.target.value })}
className="bg-slate-800 border-slate-700 text-slate-200 font-mono text-xs h-8" />
<Button variant="ghost" size="icon" className="h-8 w-8 text-red-400 hover:text-red-300"
onClick={() => removeLabel(i)}><Trash2 className="h-3.5 w-3.5" /></Button>
</div>
))}
</CardContent>
</Card>
{/* Selector */}
<Card className="bg-slate-900 border-slate-800">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
<Tags className="h-4 w-4 text-amber-400" />
选择器 (Selector)
<Badge variant="outline" className="border-amber-700/50 text-amber-500 text-[10px]">⚠️ 需与模板标签一致</Badge>
</CardTitle>
<Button size="sm" variant="outline" className="h-7 border-slate-700 text-slate-300 text-xs" onClick={addSelector}>
<Plus className="h-3 w-3 mr-1" />新增
</Button>
</div>
</CardHeader>
<CardContent className="pt-0 space-y-1.5">
<p className="text-[11px] text-amber-500/70 mb-1">Selector 决定哪些 Pod 归属此 Deployment。修改时必须确保与 Pod Template Labels 匹配,否则 Deployment 将无法管理 Pod。</p>
{selectorLabels.length === 0 && <p className="text-[11px] text-slate-600">无选择器</p>}
{selectorLabels.map((s, i) => (
<div key={i} className="grid grid-cols-[1fr_1.4fr_auto] gap-2">
<Input value={s.key} placeholder="app"
onChange={e => updateSelector(i, { key: e.target.value })}
className="bg-slate-800 border-slate-700 text-slate-200 font-mono text-xs h-8" />
<Input value={s.value} placeholder="nginx"
onChange={e => updateSelector(i, { value: e.target.value })}
className="bg-slate-800 border-slate-700 text-slate-200 font-mono text-xs h-8" />
<Button variant="ghost" size="icon" className="h-8 w-8 text-red-400 hover:text-red-300"
onClick={() => removeSelector(i)}><Trash2 className="h-3.5 w-3.5" /></Button>
</div>
))}
</CardContent>
</Card>
{/* Annotations */}
<Card className="bg-slate-900 border-slate-800">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
<FileText className="h-4 w-4 text-purple-400" />
注解 (Annotations)
<Badge variant="outline" className="border-slate-700 text-slate-500 text-[10px]">{annotations.length}</Badge>
</CardTitle>
<Button size="sm" variant="outline" className="h-7 border-slate-700 text-slate-300 text-xs" onClick={addAnnotation}>
<Plus className="h-3 w-3 mr-1" />新增
</Button>
</div>
</CardHeader>
<CardContent className="pt-0 space-y-1.5">
{annotations.length === 0 && <p className="text-[11px] text-slate-600">无注解</p>}
{annotations.map((a, i) => (
<div key={i} className="grid grid-cols-[1fr_1.4fr_auto] gap-2">
<Input value={a.key} placeholder="deployment.kubernetes.io/revision"
onChange={e => updateAnnotation(i, { key: e.target.value })}
className="bg-slate-800 border-slate-700 text-slate-200 font-mono text-xs h-8" />
<Input value={a.value} placeholder="1"
onChange={e => updateAnnotation(i, { value: e.target.value })}
className="bg-slate-800 border-slate-700 text-slate-200 font-mono text-xs h-8" />
<Button variant="ghost" size="icon" className="h-8 w-8 text-red-400 hover:text-red-300"
onClick={() => removeAnnotation(i)}><Trash2 className="h-3.5 w-3.5" /></Button>
</div>
))}
</CardContent>
</Card>
{/* 保存栏 */}
<div className="sticky bottom-0 bg-slate-950/80 backdrop-blur border-t border-slate-800 p-3 flex items-center justify-between">
<p className="text-[11px] text-slate-500">
保存将通过 <code className="text-slate-400">PUT application/yaml</code> 提交(自动剥离 status/managedFields 等只读字段)
</p>
<div className="flex gap-2">
<Button variant="outline" className="border-slate-700 text-slate-400" onClick={onCancel}>取消</Button>
<Button className="bg-blue-600 hover:bg-blue-700" disabled={saving} onClick={save}>
{saving ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : <Save className="h-4 w-4 mr-1" />}
保存
</Button>
</div>
</div>
</div>
);
}
@@ -1,6 +1,6 @@
// DeploymentWorkload.tsx — Kuboard 风格 Deployment 详情主组件 // DeploymentWorkload.tsx — Kuboard 风格 Deployment 详情主组件
// - 顶部操作菜单: 编辑 | 预览/编辑YAML | 调整镜像版本 | 伸缩 | 日志/终端 | 重启 | 暂停/恢复更新 | CI/CD发布 | 删除 | 元数据 | 运行时 | 服务 | 应用路由 // - 顶部操作菜单: 编辑 | 预览/编辑YAML | 调整镜像版本 | 伸缩 | 日志/终端 | 重启 | 暂停/恢复更新 | CI/CD发布 | 删除 | 元数据 | 运行时 | 服务 | 应用路由
// - 双模式: view(只读增强看板)/ edit(表单编辑器) // - 三模式: view(只读增强看板)/ edit(可视化编辑器)/ yaml(YAML编辑器)
// 作者: Logan <admin@attacker.club> // 作者: Logan <admin@attacker.club>
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@@ -17,7 +17,7 @@ import {
Trash2, FileText, Terminal, RefreshCw, Loader2, X, Trash2, FileText, Terminal, RefreshCw, Loader2, X,
ChevronDown, Cpu, Network, GitBranch, Info, ChevronDown, Cpu, Network, GitBranch, Info,
CheckCircle, AlertTriangle, XCircle, CheckCircle, AlertTriangle, XCircle,
Pause, Play, Rocket, Pause, Play, Rocket, Container, Settings,
} from 'lucide-react'; } from 'lucide-react';
import { kubernetesApi } from '@/services/api'; import { kubernetesApi } from '@/services/api';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
@@ -25,16 +25,19 @@ import { useUserStore } from '@/store';
import { YamlViewer } from './YamlViewer'; import { YamlViewer } from './YamlViewer';
import { DeploymentView } from './DeploymentView'; import { DeploymentView } from './DeploymentView';
import { DeploymentEdit } from './DeploymentEdit'; import { DeploymentEdit } from './DeploymentEdit';
import { DeploymentEditBasic } from './DeploymentEditBasic';
type WorkloadMode = 'view' | 'edit' | 'yaml';
export interface DeploymentWorkloadProps { export interface DeploymentWorkloadProps {
clusterCode: string; clusterCode: string;
namespace: string; namespace: string;
deploymentName: string; deploymentName: string;
/** 'view' | 'edit' — 来自路由参数 */ /** 'view' | 'edit' | 'yaml' — 来自路由参数 */
mode: 'view' | 'edit'; mode: WorkloadMode;
onBack: () => void; onBack: () => void;
/** 跳转到另一模式(用于"编辑"按钮、保存成功后切换) */ /** 跳转到另一模式(用于"编辑"按钮、保存成功后切换) */
onModeChange: (mode: 'view' | 'edit') => void; onModeChange: (mode: WorkloadMode) => void;
} }
type ActionModal = type ActionModal =
@@ -287,7 +290,7 @@ export function DeploymentWorkload({ clusterCode, namespace, deploymentName, mod
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* 模式切换 Tab */} {/* 模式切换 Tab — 三模式:查看 / 编辑 / YAML */}
<div className="inline-flex rounded-md border border-slate-700 overflow-hidden"> <div className="inline-flex rounded-md border border-slate-700 overflow-hidden">
<button <button
className={`px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'view' ? 'bg-slate-700 text-slate-100' : 'bg-transparent text-slate-400 hover:text-slate-200'}`} className={`px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'view' ? 'bg-slate-700 text-slate-100' : 'bg-transparent text-slate-400 hover:text-slate-200'}`}
@@ -301,6 +304,12 @@ export function DeploymentWorkload({ clusterCode, namespace, deploymentName, mod
> >
<Pencil className="h-3 w-3 inline mr-1" />编辑模式 <Pencil className="h-3 w-3 inline mr-1" />编辑模式
</button> </button>
<button
className={`px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'yaml' ? 'bg-emerald-600 text-white' : 'bg-transparent text-slate-400 hover:text-slate-200'}`}
onClick={() => onModeChange('yaml')}
>
<FileCode className="h-3 w-3 inline mr-1" />YAML编辑
</button>
</div> </div>
<Button variant="outline" size="sm" className="border-slate-700 text-slate-400" onClick={loadData} title="刷新"> <Button variant="outline" size="sm" className="border-slate-700 text-slate-400" onClick={loadData} title="刷新">
@@ -372,7 +381,7 @@ export function DeploymentWorkload({ clusterCode, namespace, deploymentName, mod
</div> </div>
</div> </div>
{/* ============ 主体:view / edit ============ */} {/* ============ 主体:view / edit / yaml ============ */}
{mode === 'view' ? ( {mode === 'view' ? (
<DeploymentView <DeploymentView
clusterCode={clusterCode} clusterCode={clusterCode}
@@ -386,8 +395,21 @@ export function DeploymentWorkload({ clusterCode, namespace, deploymentName, mod
onShowPodTerminal={handleShowPodTerminal} onShowPodTerminal={handleShowPodTerminal}
onShowPodYaml={handleShowPodYaml} onShowPodYaml={handleShowPodYaml}
/> />
) : mode === 'yaml' ? (
/* YAML 编辑模式 — 内联 CodeMirror 编辑器 */
<YamlViewer
clusterCode={clusterCode}
namespace={namespace}
resource="deployments"
name={deploymentName}
cleanBeforeEdit
inline
onSaved={loadData}
onClose={() => onModeChange('view')}
/>
) : ( ) : (
<DeploymentEdit /* 可视化编辑模式 — Tab 系统 */
<EditModeContainer
clusterCode={clusterCode} clusterCode={clusterCode}
namespace={namespace} namespace={namespace}
deploymentName={deploymentName} deploymentName={deploymentName}
@@ -513,6 +535,83 @@ export function DeploymentWorkload({ clusterCode, namespace, deploymentName, mod
); );
} }
// ============ 编辑模式 Tab 容器(M6: 基本信息Tab + 容器编辑预留) ============
function EditModeContainer({ clusterCode, namespace, deploymentName, manifest, onSaved, onCancel }: {
clusterCode: string;
namespace: string;
deploymentName: string;
manifest: any | null;
onSaved: () => void;
onCancel: () => void;
}) {
type EditTab = 'basic' | 'containers' | 'storage' | 'advanced' | 'service';
const [activeTab, setActiveTab] = useState<EditTab>('basic');
const tabs: Array<{ key: EditTab; label: string; icon: typeof Settings; available: boolean; badge?: string }> = [
{ key: 'basic', label: '基本信息', icon: Settings, available: true },
{ key: 'containers', label: '容器信息', icon: Container, available: true, badge: 'M7' },
{ key: 'storage', label: '存储挂载', icon: Container, available: false, badge: 'M8' },
{ key: 'advanced', label: '高级设置', icon: Settings, available: false, badge: 'M8' },
{ key: 'service', label: '服务/应用路由', icon: Network, available: false, badge: 'M9' },
];
return (
<div className="space-y-4">
{/* Tab 导航 */}
<div className="flex items-center gap-1 border-b border-slate-800 pb-px overflow-x-auto">
{tabs.map(tab => {
const active = activeTab === tab.key;
const Icon = tab.icon;
return (
<button
key={tab.key}
disabled={!tab.available}
onClick={() => tab.available && setActiveTab(tab.key)}
className={`flex items-center gap-1.5 px-3 py-2 text-xs font-medium border-b-2 transition-colors whitespace-nowrap
${active
? 'border-blue-500 text-blue-400'
: tab.available
? 'border-transparent text-slate-400 hover:text-slate-200'
: 'border-transparent text-slate-600 cursor-not-allowed'
}`}
>
<Icon className="h-3.5 w-3.5" />
{tab.label}
{tab.badge && (
<span className={`text-[9px] px-1 py-0.5 rounded ${active ? 'bg-blue-500/20 text-blue-300' : 'bg-slate-800 text-slate-600'}`}>
{tab.badge}
</span>
)}
</button>
);
})}
</div>
{/* Tab 内容 */}
{activeTab === 'basic' ? (
<DeploymentEditBasic
clusterCode={clusterCode}
namespace={namespace}
deploymentName={deploymentName}
manifest={manifest}
onSaved={onSaved}
onCancel={onCancel}
/>
) : activeTab === 'containers' ? (
/* M7 预留:容器编辑 — 暂用旧 DeploymentEdit */
<DeploymentEdit
clusterCode={clusterCode}
namespace={namespace}
deploymentName={deploymentName}
manifest={manifest}
onSaved={onSaved}
onCancel={onCancel}
/>
) : null}
</div>
);
}
// ============ 通用 Modal 外壳 ============ // ============ 通用 Modal 外壳 ============
function ModalShell({ title, children, onClose }: { title: string; children: React.ReactNode; onClose: () => void }) { function ModalShell({ title, children, onClose }: { title: string; children: React.ReactNode; onClose: () => void }) {
return ( return (
+115 -14
View File
@@ -1,9 +1,16 @@
import { useState, useEffect } from 'react'; // YamlViewer.tsx — YAML 查看/编辑器(CodeMirror 6 语法高亮)
// 支持:语法高亮 + 行号 + 基本校验 + 编辑/保存 + 只读字段清理
// 作者: Logan <admin@attacker.club>
import { useState, useEffect, useMemo, useCallback } from 'react';
import { X, Copy, Check, Save, RefreshCw, Loader2, Sparkles } from 'lucide-react'; import { X, Copy, Check, Save, RefreshCw, Loader2, Sparkles } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { kubernetesApi } from '@/services/api'; import { kubernetesApi } from '@/services/api';
import { cleanYAML } from './k8s-yaml-utils'; import { cleanYAML } from './k8s-yaml-utils';
import CodeMirror from '@uiw/react-codemirror';
import { yaml } from '@codemirror/lang-yaml';
import { oneDark } from '@codemirror/theme-one-dark';
import { EditorView } from '@codemirror/view';
interface YamlViewerProps { interface YamlViewerProps {
clusterCode: string; clusterCode: string;
@@ -16,13 +23,17 @@ interface YamlViewerProps {
cleanBeforeEdit?: boolean; cleanBeforeEdit?: boolean;
/** 打开即自动进入编辑模式(编辑按钮专用入口) */ /** 打开即自动进入编辑模式(编辑按钮专用入口) */
autoEdit?: boolean; autoEdit?: boolean;
/** 内联模式(非弹窗)— 用于 YAML 编辑模式 Tab */
inline?: boolean;
/** 内联模式下 YAML 内容直接传入 */
initialYaml?: string;
} }
export function YamlViewer({ clusterCode, namespace, resource, name, onClose, onSaved, cleanBeforeEdit = false, autoEdit = false }: YamlViewerProps) { export function YamlViewer({ clusterCode, namespace, resource, name, onClose, onSaved, cleanBeforeEdit = false, autoEdit = false, inline = false, initialYaml }: YamlViewerProps) {
const { toast } = useToast(); const { toast } = useToast();
const [rawYaml, setRawYaml] = useState(''); // 原始(只读展示用) const [rawYaml, setRawYaml] = useState(initialYaml || ''); // 原始(只读展示用)
const [editYaml, setEditYaml] = useState(''); // 编辑态(可能被清理过) const [editYaml, setEditYaml] = useState(''); // 编辑态(可能被清理过)
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(!initialYaml);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [editing, setEditing] = useState(autoEdit); // autoEdit → 初始即编辑态 const [editing, setEditing] = useState(autoEdit); // autoEdit → 初始即编辑态
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@@ -30,6 +41,12 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on
const effectiveClean = cleanBeforeEdit || autoEdit; const effectiveClean = cleanBeforeEdit || autoEdit;
const loadYaml = async () => { const loadYaml = async () => {
if (initialYaml) {
setRawYaml(initialYaml);
setEditYaml(effectiveClean ? cleanYAML(initialYaml) : initialYaml);
setLoading(false);
return;
}
setLoading(true); setLoading(true);
try { try {
const text = await kubernetesApi.getResourceYaml(clusterCode, namespace, resource, name); const text = await kubernetesApi.getResourceYaml(clusterCode, namespace, resource, name);
@@ -44,7 +61,6 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on
}; };
const enterEdit = () => { const enterEdit = () => {
// 进入编辑前可选清理
const cleaned = effectiveClean ? cleanYAML(rawYaml) : rawYaml; const cleaned = effectiveClean ? cleanYAML(rawYaml) : rawYaml;
setEditYaml(cleaned); setEditYaml(cleaned);
setEditing(true); setEditing(true);
@@ -77,9 +93,84 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on
const display = editing ? editYaml : rawYaml; const display = editing ? editYaml : rawYaml;
// CodeMirror 主题扩展
const extensions = useMemo(() => [
yaml(),
EditorView.lineWrapping,
...(editing ? [] : [EditorView.editable.of(false)]),
], [editing]);
// 内联模式 — 不显示外层弹窗,适合嵌入 Tab 页面
if (inline) {
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<p className="text-xs text-slate-500">
<code className="text-slate-400">{resource}/{name}</code> · {namespace} · {clusterCode}
</p>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" className="h-7 border-slate-700 text-slate-300 text-xs" onClick={copyYaml}>
{copied ? <Check className="h-3 w-3 mr-1 text-emerald-400" /> : <Copy className="h-3 w-3 mr-1" />}
{copied ? '已复制' : '复制'}
</Button>
{!editing ? (
<>
{effectiveClean && (
<Button variant="outline" size="sm" className="h-7 border-slate-700 text-slate-300 text-xs" onClick={enterEdit} title="清理 status / managedFields 等只读字段后编辑">
<Sparkles className="h-3 w-3 mr-1 text-amber-400" />清理并编辑
</Button>
)}
{!effectiveClean && (
<Button variant="outline" size="sm" className="h-7 border-slate-700 text-slate-300 text-xs" onClick={() => setEditing(true)}>
编辑
</Button>
)}
</>
) : (
<>
<Button variant="outline" size="sm" className="h-7 border-slate-700 text-slate-300 text-xs" onClick={() => { setEditing(false); loadYaml(); }}>
取消
</Button>
<Button size="sm" className="h-7 bg-blue-600 hover:bg-blue-700 text-xs" onClick={saveYaml} disabled={saving}>
{saving ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : <Save className="h-3 w-3 mr-1" />}
保存
</Button>
</>
)}
</div>
</div>
<div className="border border-slate-700 rounded-lg overflow-hidden">
{loading ? (
<div className="flex items-center justify-center h-96"><Loader2 className="h-6 w-6 animate-spin text-slate-500" /></div>
) : (
<CodeMirror
value={display}
height="600px"
theme={oneDark}
extensions={extensions}
onChange={(val) => editing && setEditYaml(val)}
basicSetup={{
lineNumbers: true,
foldGutter: true,
highlightActiveLine: editing,
highlightActiveLineGutter: editing,
autocompletion: true,
bracketMatching: true,
closeBrackets: true,
indentOnInput: true,
}}
style={{ fontSize: '12px' }}
/>
)}
</div>
</div>
);
}
// 弹窗模式(原有行为)
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}>
<div className="bg-slate-900 border border-slate-700 rounded-xl w-[90vw] max-w-4xl max-h-[80vh] flex flex-col shadow-2xl" onClick={e => e.stopPropagation()}> <div className="bg-slate-900 border border-slate-700 rounded-xl w-[90vw] max-w-4xl max-h-[85vh] flex flex-col shadow-2xl" onClick={e => e.stopPropagation()}>
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-4 border-b border-slate-700"> <div className="flex items-center justify-between p-4 border-b border-slate-700">
<div> <div>
@@ -122,15 +213,25 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on
<div className="flex-1 overflow-auto p-4"> <div className="flex-1 overflow-auto p-4">
{loading ? ( {loading ? (
<div className="flex items-center justify-center h-full"><Loader2 className="h-6 w-6 animate-spin text-slate-500" /></div> <div className="flex items-center justify-center h-full"><Loader2 className="h-6 w-6 animate-spin text-slate-500" /></div>
) : editing ? (
<textarea
value={editYaml}
onChange={e => setEditYaml(e.target.value)}
className="w-full h-full min-h-[500px] bg-slate-950 text-emerald-400 font-mono text-xs p-4 rounded border border-slate-700 focus:outline-none focus:border-blue-500 resize-none"
spellCheck={false}
/>
) : ( ) : (
<pre className="text-emerald-400 font-mono text-xs whitespace-pre overflow-x-auto">{display}</pre> <CodeMirror
value={display}
height="600px"
theme={oneDark}
extensions={extensions}
onChange={(val) => editing && setEditYaml(val)}
basicSetup={{
lineNumbers: true,
foldGutter: true,
highlightActiveLine: editing,
highlightActiveLineGutter: editing,
autocompletion: true,
bracketMatching: true,
closeBrackets: true,
indentOnInput: true,
}}
style={{ fontSize: '12px' }}
/>
)} )}
</div> </div>
</div> </div>