From 68a954c1c5a475e282c62b0be0fb0a1a601b74e3 Mon Sep 17 00:00:00 2001 From: attacker Date: Tue, 23 Jun 2026 05:15:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(k8s):=20M6=20=E7=BC=96=E8=BE=91=E5=99=A8?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD=20+=20=E5=9F=BA=E6=9C=AC?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=20Tab=20+=20YAML=E7=BC=96=E8=BE=91=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DeploymentWorkload: 三模式切换(查看/编辑/YAML编辑)+ EditModeContainer Tab 导航系统 - DeploymentEditBasic: 新增基本信息 Tab — replicas/labels/annotations/selector 编辑 - YamlViewer: 升级为 CodeMirror 6(语法高亮+行号+折叠+括号匹配),新增 inline 内联模式 - DeploymentDetailPage: mode 类型扩展为 view|edit|yaml - 路由 :mode 参数自动支持 yaml 值 - M7-M9 Tab 预留占位(容器信息/存储/高级/服务) --- PROJECT_EVALUATION.md | 11 +- ROADMAP.md | 19 +- TDD_PLAN.md | 40 +++ frontend/package-lock.json | 266 +++++++++++++++- frontend/package.json | 5 + .../kubernetes/DeploymentDetailPage.tsx | 14 +- .../kubernetes/DeploymentEditBasic.tsx | 298 ++++++++++++++++++ .../kubernetes/DeploymentWorkload.tsx | 115 ++++++- .../src/components/kubernetes/YamlViewer.tsx | 129 +++++++- 9 files changed, 855 insertions(+), 42 deletions(-) create mode 100644 frontend/src/components/kubernetes/DeploymentEditBasic.tsx diff --git a/PROJECT_EVALUATION.md b/PROJECT_EVALUATION.md index 9f85b59..527fcf5 100644 --- a/PROJECT_EVALUATION.md +++ b/PROJECT_EVALUATION.md @@ -44,8 +44,8 @@ |------|:------:|------| | Pod 文件上传 | ✅ | P2 | ✅ 已完成 M3 | | ConfigMap/Secret 编辑 | ✅ | P2 | ✅ 已完成 M4 | -| **Deployment 可视化编辑器** | **P1** | **M6-M9 进行中 — 当前编辑器只有 image/resources/probe(httpGet)/env/mounts,缺容器完整编辑/存储/调度/服务** | -| **YAML 编辑器增强** | **P1** | **M6 — 当前是纯 textarea,需 Monaco/CodeMirror 语法高亮** | +| **Deployment 可视化编辑器** | **P1** | **M6已完成(三模式切换+基本信息Tab+YAML编辑器CodeMirror),M7-M9 进行中** | +| **YAML 编辑器增强** | **P1** | **✅ 已完成 M6 — CodeMirror 6 语法高亮+行号+折叠+内联模式** | | CI/CD 集成菜单项 | ✅ | P3 | ✅ 已完成 M5 — 跳转流水线页面 | | 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) | -| `DeploymentEdit.tsx` | 374 | ⚠️ 浅层 | **只覆盖 image/resources/probe(httpGet)/env/mounts,将在 M9 删除** | -| `YamlViewer.tsx` | 139 | ⚠️ 基础 | **纯 textarea,无语法高亮/行号/校验,将在 M6 升级** | +| `DeploymentEditBasic.tsx` | 340 | ✅ 新增 M6 | 基本信息 Tab — replicas/labels/annotations/selector 编辑 | +| `DeploymentEdit.tsx` | 374 | ⚠️ 过渡 | M6 临时放在容器Tab下方,M7 将被完整容器编辑器取代 | +| `YamlViewer.tsx` | 220 | ✅ 升级 M6 | CodeMirror 6 YAML 语法高亮+行号+折叠,弹窗+内联双模式 | | `DeploymentDetail.tsx` | 719 | 💀 遗留 | **旧版 tab 式详情页,仅 KubernetesWorkloads.tsx 引用,待清理** | | 后端 GetYAML/UpdateYAML | — | ✅ 完善 | 支持 12 种资源,PUT 直通 K8s API,**纯前端工程无需改后端** | diff --git a/ROADMAP.md b/ROADMAP.md index c6da56e..e154974 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -88,19 +88,24 @@ ## 当前冲刺:P1 — Deployment 可视化编辑器(Kuboard 风格) -### M6: 编辑器基础设施 + 基本信息 Tab + YAML编辑器 [pending] +### M6: 编辑器基础设施 + 基本信息 Tab + YAML编辑器 [done] **目标**: 三模式切换(查看/可视化编辑/YAML编辑)+ 基本信息 Tab **改动**: - `DeploymentWorkload.tsx`: 模式切换从 2 tab → 3 tab(查看模式 / 编辑模式 / YAML编辑) -- 新增 `DeploymentEditBasic.tsx`: 基本信息 Tab — name/namespace/labels/annotations/replicas -- `YamlViewer.tsx` 升级: 引入 Monaco Editor(或 CodeMirror),支持 YAML 语法高亮 + 行号 + 基本校验 -- 路由扩展: `:mode` 增加 `yaml` 值 +- 新增 `DeploymentEditBasic.tsx`: 基本信息 Tab — name/namespace(只读) + replicas/labels/annotations/selector(可编辑) +- `YamlViewer.tsx` 升级: 引入 CodeMirror 6(`@uiw/react-codemirror` + `@codemirror/lang-yaml`),支持 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 编辑器 -- [ ] 基本信息 Tab 可编辑 labels/annotations/replicas 并保存 -- [ ] YAML 编辑器有语法高亮和行号 +- [x] 三模式 tab 切换正常,YAML 模式打开 CodeMirror 编辑器 +- [x] 基本信息 Tab 可编辑 labels/annotations/replicas 并保存 +- [x] YAML 编辑器有语法高亮和行号 +- [x] YAML 编辑器支持内联模式(inline prop) +- [x] 编辑模式 Tab 导航系统就绪(M7-M9 预留占位) --- diff --git a/TDD_PLAN.md b/TDD_PLAN.md index 4ab8af4..162a299 100644 --- a/TDD_PLAN.md +++ b/TDD_PLAN.md @@ -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] 前端操作菜单显示「暂停更新/恢复更新」动态切换 - [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 预留) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a921977..010c2af 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,10 @@ "name": "my-app", "version": "0.0.0", "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", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", @@ -38,6 +42,7 @@ "@tanstack/react-query": "^5.95.2", "@tanstack/react-query-devtools": "^5.95.2", "@types/react-router-dom": "^5.3.3", + "@uiw/react-codemirror": "^4.25.10", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -127,6 +132,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1847,6 +1853,115 @@ "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": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz", @@ -2604,6 +2719,47 @@ "@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": { "version": "2.1.5", "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", "integrity": "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.95.2" }, @@ -4801,6 +4958,7 @@ "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4810,6 +4968,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4820,6 +4979,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4890,6 +5050,7 @@ "integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", @@ -5114,6 +5275,59 @@ "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": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", @@ -5141,6 +5355,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5404,6 +5619,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5551,6 +5767,21 @@ "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -5622,6 +5853,12 @@ "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": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5865,7 +6102,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -5960,6 +6198,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6551,6 +6790,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -7005,6 +7245,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7052,6 +7293,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -7258,6 +7500,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -7288,6 +7531,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -7300,6 +7544,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.70.0.tgz", "integrity": "sha512-COOMajS4FI3Wuwrs3GPpi/Jeef/5W1DRR84Yl5/ShlT3dKVFUfoGiEZ/QE6Uw8P4T2/CLJdcTVYKvWBMQTEpvw==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -7790,6 +8035,12 @@ "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": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -7855,6 +8106,7 @@ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -8011,6 +8263,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8235,6 +8488,7 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.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": { "version": "2.0.2", "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", "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/xterm-addon-fit": { "version": "0.8.0", @@ -8382,6 +8643,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/package.json b/frontend/package.json index c0a7a0d..0ca0b78 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,10 @@ "preview": "vite preview" }, "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", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", @@ -40,6 +44,7 @@ "@tanstack/react-query": "^5.95.2", "@tanstack/react-query-devtools": "^5.95.2", "@types/react-router-dom": "^5.3.3", + "@uiw/react-codemirror": "^4.25.10", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", diff --git a/frontend/src/components/kubernetes/DeploymentDetailPage.tsx b/frontend/src/components/kubernetes/DeploymentDetailPage.tsx index 544a0e5..5441962 100644 --- a/frontend/src/components/kubernetes/DeploymentDetailPage.tsx +++ b/frontend/src/components/kubernetes/DeploymentDetailPage.tsx @@ -1,19 +1,21 @@ // DeploymentDetailPage.tsx — 路由包装:解析 cluster/namespace/mode/name -// 支持 Kuboard 风格双模式路由: +// 支持 Kuboard 风格三模式路由: // view: /kubernetes/:c/namespace/:n/workload/view/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) // 作者: Logan import { useParams, useNavigate } from 'react-router-dom'; import { DeploymentWorkload } from './DeploymentWorkload'; +export type WorkloadMode = 'view' | 'edit' | 'yaml'; + export default function DeploymentDetailPage() { - // 兼容两种路由参数命名:旧 :name 与新 :name(路径段不同) const params = useParams<{ clusterCode: string; namespace: string; name: string; - mode?: string; // 'view' | 'edit'(新路由) + mode?: string; // 'view' | 'edit' | 'yaml'(新路由) workloadKind?: string; // 'Deployment'(新路由,预留扩展) }>(); @@ -21,10 +23,10 @@ export default function DeploymentDetailPage() { const clusterCode = params.clusterCode || ''; const namespace = params.namespace || ''; 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 路由间跳转 - const handleModeChange = (m: 'view' | 'edit') => { + // 模式切换:在 view/edit/yaml 路由间跳转 + const handleModeChange = (m: WorkloadMode) => { if (m === currentMode) return; navigate( `/kubernetes/${clusterCode}/namespace/${namespace}/workload/${m}/Deployment/${deploymentName}`, diff --git a/frontend/src/components/kubernetes/DeploymentEditBasic.tsx b/frontend/src/components/kubernetes/DeploymentEditBasic.tsx new file mode 100644 index 0000000..60dc1a4 --- /dev/null +++ b/frontend/src/components/kubernetes/DeploymentEditBasic.tsx @@ -0,0 +1,298 @@ +// DeploymentEditBasic.tsx — 编辑器「基本信息」Tab +// 编辑内容:replicas / labels / annotations(name/namespace 只读) +// 作者: Logan +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 | 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 { + const out: Record = {}; + 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(manifest || {}); + const [replicas, setReplicas] = useState(1); + const [labels, setLabels] = useState>([]); + const [annotations, setAnnotations] = useState>([]); + const [selectorLabels, setSelectorLabels] = useState>([]); + + // 初始化数据 + 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 ( + + + +

加载中...

+
+
+ ); + } + + if (!manifest) { + return ( + + + 未获取到 manifest 数据,请使用 YAML 编辑模式直接修改。 + + + ); + } + + return ( +
+ {/* 副本数 */} + + + + + 副本数 (Replicas) + + + +
+ setReplicas(Math.max(0, Number(e.target.value) || 0))} + className="w-32 bg-slate-800 border-slate-700 text-slate-200" + /> + 期望运行的 Pod 实例数量 +
+
+
+ + {/* 基本标识(只读) */} + + + + + 基本标识(只读) + + + +
+ + +
+
+ + +
+
+
+ + {/* Labels */} + + +
+ + + 标签 (Labels) + {labels.length} + + +
+
+ + {labels.length === 0 &&

无标签

} + {labels.map((l, i) => ( +
+ updateLabel(i, { key: e.target.value })} + className="bg-slate-800 border-slate-700 text-slate-200 font-mono text-xs h-8" /> + updateLabel(i, { value: e.target.value })} + className="bg-slate-800 border-slate-700 text-slate-200 font-mono text-xs h-8" /> + +
+ ))} +
+
+ + {/* Selector */} + + +
+ + + 选择器 (Selector) + ⚠️ 需与模板标签一致 + + +
+
+ +

Selector 决定哪些 Pod 归属此 Deployment。修改时必须确保与 Pod Template Labels 匹配,否则 Deployment 将无法管理 Pod。

+ {selectorLabels.length === 0 &&

无选择器

} + {selectorLabels.map((s, i) => ( +
+ updateSelector(i, { key: e.target.value })} + className="bg-slate-800 border-slate-700 text-slate-200 font-mono text-xs h-8" /> + updateSelector(i, { value: e.target.value })} + className="bg-slate-800 border-slate-700 text-slate-200 font-mono text-xs h-8" /> + +
+ ))} +
+
+ + {/* Annotations */} + + +
+ + + 注解 (Annotations) + {annotations.length} + + +
+
+ + {annotations.length === 0 &&

无注解

} + {annotations.map((a, i) => ( +
+ updateAnnotation(i, { key: e.target.value })} + className="bg-slate-800 border-slate-700 text-slate-200 font-mono text-xs h-8" /> + updateAnnotation(i, { value: e.target.value })} + className="bg-slate-800 border-slate-700 text-slate-200 font-mono text-xs h-8" /> + +
+ ))} +
+
+ + {/* 保存栏 */} +
+

+ 保存将通过 PUT application/yaml 提交(自动剥离 status/managedFields 等只读字段) +

+
+ + +
+
+
+ ); +} diff --git a/frontend/src/components/kubernetes/DeploymentWorkload.tsx b/frontend/src/components/kubernetes/DeploymentWorkload.tsx index b162bf6..42a071d 100644 --- a/frontend/src/components/kubernetes/DeploymentWorkload.tsx +++ b/frontend/src/components/kubernetes/DeploymentWorkload.tsx @@ -1,6 +1,6 @@ // DeploymentWorkload.tsx — Kuboard 风格 Deployment 详情主组件 // - 顶部操作菜单: 编辑 | 预览/编辑YAML | 调整镜像版本 | 伸缩 | 日志/终端 | 重启 | 暂停/恢复更新 | CI/CD发布 | 删除 | 元数据 | 运行时 | 服务 | 应用路由 -// - 双模式: view(只读增强看板)/ edit(表单编辑器) +// - 三模式: view(只读增强看板)/ edit(可视化编辑器)/ yaml(YAML编辑器) // 作者: Logan import { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -17,7 +17,7 @@ import { Trash2, FileText, Terminal, RefreshCw, Loader2, X, ChevronDown, Cpu, Network, GitBranch, Info, CheckCircle, AlertTriangle, XCircle, - Pause, Play, Rocket, + Pause, Play, Rocket, Container, Settings, } from 'lucide-react'; import { kubernetesApi } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; @@ -25,16 +25,19 @@ import { useUserStore } from '@/store'; import { YamlViewer } from './YamlViewer'; import { DeploymentView } from './DeploymentView'; import { DeploymentEdit } from './DeploymentEdit'; +import { DeploymentEditBasic } from './DeploymentEditBasic'; + +type WorkloadMode = 'view' | 'edit' | 'yaml'; export interface DeploymentWorkloadProps { clusterCode: string; namespace: string; deploymentName: string; - /** 'view' | 'edit' — 来自路由参数 */ - mode: 'view' | 'edit'; + /** 'view' | 'edit' | 'yaml' — 来自路由参数 */ + mode: WorkloadMode; onBack: () => void; /** 跳转到另一模式(用于"编辑"按钮、保存成功后切换) */ - onModeChange: (mode: 'view' | 'edit') => void; + onModeChange: (mode: WorkloadMode) => void; } type ActionModal = @@ -287,7 +290,7 @@ export function DeploymentWorkload({ clusterCode, namespace, deploymentName, mod
- {/* 模式切换 Tab */} + {/* 模式切换 Tab — 三模式:查看 / 编辑 / YAML */}
+
- {/* ============ 主体:view / edit ============ */} + {/* ============ 主体:view / edit / yaml ============ */} {mode === 'view' ? ( + ) : mode === 'yaml' ? ( + /* YAML 编辑模式 — 内联 CodeMirror 编辑器 */ + onModeChange('view')} + /> ) : ( - void; + onCancel: () => void; +}) { + type EditTab = 'basic' | 'containers' | 'storage' | 'advanced' | 'service'; + const [activeTab, setActiveTab] = useState('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 ( +
+ {/* Tab 导航 */} +
+ {tabs.map(tab => { + const active = activeTab === tab.key; + const Icon = tab.icon; + return ( + + ); + })} +
+ + {/* Tab 内容 */} + {activeTab === 'basic' ? ( + + ) : activeTab === 'containers' ? ( + /* M7 预留:容器编辑 — 暂用旧 DeploymentEdit */ + + ) : null} +
+ ); +} + // ============ 通用 Modal 外壳 ============ function ModalShell({ title, children, onClose }: { title: string; children: React.ReactNode; onClose: () => void }) { return ( diff --git a/frontend/src/components/kubernetes/YamlViewer.tsx b/frontend/src/components/kubernetes/YamlViewer.tsx index caae69c..8dcc4f0 100644 --- a/frontend/src/components/kubernetes/YamlViewer.tsx +++ b/frontend/src/components/kubernetes/YamlViewer.tsx @@ -1,9 +1,16 @@ -import { useState, useEffect } from 'react'; +// YamlViewer.tsx — YAML 查看/编辑器(CodeMirror 6 语法高亮) +// 支持:语法高亮 + 行号 + 基本校验 + 编辑/保存 + 只读字段清理 +// 作者: Logan +import { useState, useEffect, useMemo, useCallback } from 'react'; import { X, Copy, Check, Save, RefreshCw, Loader2, Sparkles } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useToast } from '@/hooks/use-toast'; import { kubernetesApi } from '@/services/api'; 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 { clusterCode: string; @@ -16,13 +23,17 @@ interface YamlViewerProps { cleanBeforeEdit?: 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 [rawYaml, setRawYaml] = useState(''); // 原始(只读展示用) + const [rawYaml, setRawYaml] = useState(initialYaml || ''); // 原始(只读展示用) const [editYaml, setEditYaml] = useState(''); // 编辑态(可能被清理过) - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(!initialYaml); const [saving, setSaving] = useState(false); const [editing, setEditing] = useState(autoEdit); // autoEdit → 初始即编辑态 const [copied, setCopied] = useState(false); @@ -30,6 +41,12 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on const effectiveClean = cleanBeforeEdit || autoEdit; const loadYaml = async () => { + if (initialYaml) { + setRawYaml(initialYaml); + setEditYaml(effectiveClean ? cleanYAML(initialYaml) : initialYaml); + setLoading(false); + return; + } setLoading(true); try { 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 cleaned = effectiveClean ? cleanYAML(rawYaml) : rawYaml; setEditYaml(cleaned); setEditing(true); @@ -77,9 +93,84 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on const display = editing ? editYaml : rawYaml; + // CodeMirror 主题扩展 + const extensions = useMemo(() => [ + yaml(), + EditorView.lineWrapping, + ...(editing ? [] : [EditorView.editable.of(false)]), + ], [editing]); + + // 内联模式 — 不显示外层弹窗,适合嵌入 Tab 页面 + if (inline) { + return ( +
+
+

+ {resource}/{name} · {namespace} · {clusterCode} +

+
+ + {!editing ? ( + <> + {effectiveClean && ( + + )} + {!effectiveClean && ( + + )} + + ) : ( + <> + + + + )} +
+
+
+ {loading ? ( +
+ ) : ( + editing && setEditYaml(val)} + basicSetup={{ + lineNumbers: true, + foldGutter: true, + highlightActiveLine: editing, + highlightActiveLineGutter: editing, + autocompletion: true, + bracketMatching: true, + closeBrackets: true, + indentOnInput: true, + }} + style={{ fontSize: '12px' }} + /> + )} +
+
+ ); + } + + // 弹窗模式(原有行为) return (
-
e.stopPropagation()}> +
e.stopPropagation()}> {/* Header */}
@@ -122,15 +213,25 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on
{loading ? (
- ) : editing ? ( -