feat(k8s): M4 ConfigMap/Secret 在线编辑

- YamlViewer 增加 autoEdit prop,打开即进入编辑模式
- NamespaceResourcePage ConfigMap/Secret 行增加编辑按钮(铅笔图标)
- 统一 cleanBeforeEdit=true,编辑前自动清理 status/managedFields/uid
- 端到端验证: GET YAML → PUT 更新 → curl 确认 K8s label 生效
- 更新 ROADMAP/PROJECT_EVALUATION
This commit is contained in:
attacker committed 2026-06-23 02:33:31 +08:00
1 parent 174382ff81
commit 8637036995
4 files changed
+34 -13

No files matched your search

+1
View File
@@ -32,6 +32,7 @@
| Service/Ingress 列表 | ✅ | ✅ | ✅ | | Service/Ingress 列表 | ✅ | ✅ | ✅ |
| HPA 列表/创建/删除 | ✅ | ✅ | ✅ | | HPA 列表/创建/删除 | ✅ | ✅ | ✅ |
| ConfigMap/Secret 列表 | ✅ | ✅ | ✅ | | ConfigMap/Secret 列表 | ✅ | ✅ | ✅ |
| ConfigMap/Secret 编辑 | ✅ UpdateResourceYAMLNS | ✅ YamlViewer(autoEdit) | ✅ curl 验证 |
| Events 列表 | ✅ ListEvents | ✅ EventsPage | ✅ | | Events 列表 | ✅ ListEvents | ✅ EventsPage | ✅ |
| 容量分析 | ✅ GetCapacityAnalysis | ✅ CapacityPage | ✅ | | 容量分析 | ✅ GetCapacityAnalysis | ✅ CapacityPage | ✅ |
| kubectl 终端 | ✅ KubectlShell(WS) | ✅ KubectlTerminal | ✅ | | kubectl 终端 | ✅ KubectlShell(WS) | ✅ KubectlTerminal | ✅ |
+8 -5
View File
@@ -54,16 +54,19 @@
--- ---
### M4: ConfigMap/Secret 编辑 [pending] ### M4: ConfigMap/Secret 编辑 [done]
**目标**: 目前只读,增加在线编辑能力 **目标**: 目前只读,增加在线编辑能力
**改动**: **改动**:
- 复用已有 `UpdateResourceYAMLNS` 端点 - 后端 `UpdateResourceYAMLNS` 端点已支持 configmaps/secrets(无需改动)
- 前端 ConfigMap/Secret 列表增加编辑弹窗(YAML 编辑器) - 前端 `YamlViewer` 增加 `autoEdit` prop — 打开即进入编辑模式
- `NamespaceResourcePage` ConfigMap/Secret 行增加显式编辑按钮(铅笔图标)
- 统一 `cleanBeforeEdit=true` — 编辑前自动清理 status/managedFields/uid 等只读字段
- `k8s-yaml-utils.ts` 的 `cleanYAML()` 提供行级清理
**验收标准**: **验收标准**:
- [ ] 可在线编辑 ConfigMap 并保存 - [x] 可在线编辑 ConfigMap 并保存
- [ ] 保存后 K8s 中实际生效 - [x] 保存后 K8s 中实际生效(curl 验证 label 增删 → GET 确认)
--- ---
@@ -13,7 +13,7 @@ import {
import { import {
RefreshCw, Search, Loader2, Boxes, Container, RefreshCw, Search, Loader2, Boxes, Container,
Network, FileText, Shield, Activity, Clock, ChevronRight, Network, FileText, Shield, Activity, Clock, ChevronRight,
Download, FileCode, Download, FileCode, Pencil,
} from 'lucide-react'; } from 'lucide-react';
import { YamlViewer } from './YamlViewer'; import { YamlViewer } from './YamlViewer';
import { kubernetesApi } from '@/services/api'; import { kubernetesApi } from '@/services/api';
@@ -29,7 +29,7 @@ const RESOURCE_CONFIG: Record<ResourceType, {
subtitle: string; subtitle: string;
icon: React.ComponentType<any>; icon: React.ComponentType<any>;
columns: string[]; columns: string[];
renderRow: (item: any, opts: { clusterCode: string; namespace: string; onDetail?: (item: any) => void; onYaml?: (name: string) => void }) => React.ReactNode; renderRow: (item: any, opts: { clusterCode: string; namespace: string; onDetail?: (item: any) => void; onYaml?: (name: string) => void; onEdit?: (name: string) => void }) => React.ReactNode;
}> = { }> = {
deployments: { deployments: {
title: 'Deployments', subtitle: 'Deployment 工作负载运行状态', icon: Boxes, title: 'Deployments', subtitle: 'Deployment 工作负载运行状态', icon: Boxes,
@@ -120,7 +120,14 @@ const RESOURCE_CONFIG: Record<ResourceType, {
<TableCell className="font-medium text-slate-200">{c.name}</TableCell> <TableCell className="font-medium text-slate-200">{c.name}</TableCell>
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{c.namespace}</Badge></TableCell> <TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{c.namespace}</Badge></TableCell>
<TableCell className="text-sm text-slate-400">{c.key_count || '-'}</TableCell> <TableCell className="text-sm text-slate-400">{c.key_count || '-'}</TableCell>
<TableCell><Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400" title="查看YAML" onClick={() => opts.onYaml?.(c.name)}><FileCode className="h-3.5 w-3.5" /></Button></TableCell> <TableCell>
<div className="flex items-center gap-0.5">
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400" title="查看YAML"
onClick={() => opts.onYaml?.(c.name)}><FileCode className="h-3.5 w-3.5" /></Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-emerald-400 hover:text-emerald-300" title="编辑"
onClick={() => opts.onEdit?.(c.name)}><Pencil className="h-3.5 w-3.5" /></Button>
</div>
</TableCell>
</TableRow> </TableRow>
), ),
}, },
@@ -132,7 +139,14 @@ const RESOURCE_CONFIG: Record<ResourceType, {
<TableCell className="font-medium text-slate-200">{s.name}</TableCell> <TableCell className="font-medium text-slate-200">{s.name}</TableCell>
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{s.namespace}</Badge></TableCell> <TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{s.namespace}</Badge></TableCell>
<TableCell className="text-sm text-slate-400">{s.type || '-'}</TableCell> <TableCell className="text-sm text-slate-400">{s.type || '-'}</TableCell>
<TableCell><Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400" title="查看YAML" onClick={() => opts.onYaml?.(s.name)}><FileCode className="h-3.5 w-3.5" /></Button></TableCell> <TableCell>
<div className="flex items-center gap-0.5">
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400" title="查看YAML"
onClick={() => opts.onYaml?.(s.name)}><FileCode className="h-3.5 w-3.5" /></Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-emerald-400 hover:text-emerald-300" title="编辑"
onClick={() => opts.onEdit?.(s.name)}><Pencil className="h-3.5 w-3.5" /></Button>
</div>
</TableCell>
</TableRow> </TableRow>
), ),
}, },
@@ -227,7 +241,7 @@ export default function NamespaceResourcePage() {
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [yamlItem, setYamlItem] = useState<{resource: string; name: string} | null>(null); const [yamlItem, setYamlItem] = useState<{resource: string; name: string; editMode?: boolean} | null>(null);
useEffect(() => { useEffect(() => {
kubernetesApi.getClusters().then((res: any) => { kubernetesApi.getClusters().then((res: any) => {
@@ -285,6 +299,7 @@ export default function NamespaceResourcePage() {
}; };
const handleYaml = (name: string) => setYamlItem({ resource: type, name }); const handleYaml = (name: string) => setYamlItem({ resource: type, name });
const handleEdit = (name: string) => setYamlItem({ resource: type, name, editMode: true });
const handleExportCSV = () => kubernetesApi.exportAsCSV(`${type}-${clusterCode}-${namespace}.csv`, filtered); const handleExportCSV = () => kubernetesApi.exportAsCSV(`${type}-${clusterCode}-${namespace}.csv`, filtered);
const handleExportJSON = () => kubernetesApi.exportAsJSON(`${type}-${clusterCode}-${namespace}.json`, filtered); const handleExportJSON = () => kubernetesApi.exportAsJSON(`${type}-${clusterCode}-${namespace}.json`, filtered);
@@ -314,6 +329,8 @@ export default function NamespaceResourcePage() {
name={yamlItem.name} name={yamlItem.name}
onClose={() => setYamlItem(null)} onClose={() => setYamlItem(null)}
onSaved={loadData} onSaved={loadData}
cleanBeforeEdit={true}
autoEdit={yamlItem.editMode === true}
/> />
)} )}
<div className="flex items-center gap-2 text-sm text-slate-500"> <div className="flex items-center gap-2 text-sm text-slate-500">
@@ -369,7 +386,7 @@ export default function NamespaceResourcePage() {
) : filtered.length === 0 ? ( ) : filtered.length === 0 ? (
<TableRow><TableCell colSpan={config.columns.length} className="text-center text-slate-500 py-12"><config.icon className="h-8 w-8 mx-auto mb-2 opacity-50" />暂无 {config.title}</TableCell></TableRow> <TableRow><TableCell colSpan={config.columns.length} className="text-center text-slate-500 py-12"><config.icon className="h-8 w-8 mx-auto mb-2 opacity-50" />暂无 {config.title}</TableCell></TableRow>
) : ( ) : (
filtered.map((item) => config.renderRow(item, { clusterCode: clusterCode || '', namespace: namespace || '', onDetail: handleDetail, onYaml: handleYaml })) filtered.map((item) => config.renderRow(item, { clusterCode: clusterCode || '', namespace: namespace || '', onDetail: handleDetail, onYaml: handleYaml, onEdit: handleEdit }))
)} )}
</TableBody> </TableBody>
</Table> </Table>
@@ -93,12 +93,12 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on
</Button> </Button>
{!editing ? ( {!editing ? (
<> <>
{cleanBeforeEdit && ( {effectiveClean && (
<Button variant="outline" size="sm" className="h-8 border-slate-700 text-slate-300" onClick={enterEdit} title="清理 status / managedFields 等只读字段后编辑"> <Button variant="outline" size="sm" className="h-8 border-slate-700 text-slate-300" onClick={enterEdit} title="清理 status / managedFields 等只读字段后编辑">
<Sparkles className="h-3 w-3 mr-1 text-amber-400" />清理并编辑 <Sparkles className="h-3 w-3 mr-1 text-amber-400" />清理并编辑
</Button> </Button>
)} )}
{!cleanBeforeEdit && ( {!effectiveClean && (
<Button variant="outline" size="sm" className="h-8 border-slate-700 text-slate-300" onClick={() => setEditing(true)}> <Button variant="outline" size="sm" className="h-8 border-slate-700 text-slate-300" onClick={() => setEditing(true)}>
编辑 编辑
</Button> </Button>