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:
1 parent
174382ff81
commit
8637036995
4 files changed
+34
-13
No files matched your search
@@ -32,6 +32,7 @@
|
||||
| Service/Ingress 列表 | ✅ | ✅ | ✅ |
|
||||
| HPA 列表/创建/删除 | ✅ | ✅ | ✅ |
|
||||
| ConfigMap/Secret 列表 | ✅ | ✅ | ✅ |
|
||||
| ConfigMap/Secret 编辑 | ✅ UpdateResourceYAMLNS | ✅ YamlViewer(autoEdit) | ✅ curl 验证 |
|
||||
| Events 列表 | ✅ ListEvents | ✅ EventsPage | ✅ |
|
||||
| 容量分析 | ✅ GetCapacityAnalysis | ✅ CapacityPage | ✅ |
|
||||
| kubectl 终端 | ✅ KubectlShell(WS) | ✅ KubectlTerminal | ✅ |
|
||||
|
||||
+8
-5
@@ -54,16 +54,19 @@
|
||||
|
||||
---
|
||||
|
||||
### M4: ConfigMap/Secret 编辑 [pending]
|
||||
### M4: ConfigMap/Secret 编辑 [done]
|
||||
**目标**: 目前只读,增加在线编辑能力
|
||||
|
||||
**改动**:
|
||||
- 复用已有 `UpdateResourceYAMLNS` 端点
|
||||
- 前端 ConfigMap/Secret 列表增加编辑弹窗(YAML 编辑器)
|
||||
- 后端 `UpdateResourceYAMLNS` 端点已支持 configmaps/secrets(无需改动)
|
||||
- 前端 `YamlViewer` 增加 `autoEdit` prop — 打开即进入编辑模式
|
||||
- `NamespaceResourcePage` ConfigMap/Secret 行增加显式编辑按钮(铅笔图标)
|
||||
- 统一 `cleanBeforeEdit=true` — 编辑前自动清理 status/managedFields/uid 等只读字段
|
||||
- `k8s-yaml-utils.ts` 的 `cleanYAML()` 提供行级清理
|
||||
|
||||
**验收标准**:
|
||||
- [ ] 可在线编辑 ConfigMap 并保存
|
||||
- [ ] 保存后 K8s 中实际生效
|
||||
- [x] 可在线编辑 ConfigMap 并保存
|
||||
- [x] 保存后 K8s 中实际生效(curl 验证 label 增删 → GET 确认)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import {
|
||||
RefreshCw, Search, Loader2, Boxes, Container,
|
||||
Network, FileText, Shield, Activity, Clock, ChevronRight,
|
||||
Download, FileCode,
|
||||
Download, FileCode, Pencil,
|
||||
} from 'lucide-react';
|
||||
import { YamlViewer } from './YamlViewer';
|
||||
import { kubernetesApi } from '@/services/api';
|
||||
@@ -29,7 +29,7 @@ const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
subtitle: string;
|
||||
icon: React.ComponentType<any>;
|
||||
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: {
|
||||
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><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><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>
|
||||
),
|
||||
},
|
||||
@@ -132,7 +139,14 @@ const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
<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 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>
|
||||
),
|
||||
},
|
||||
@@ -227,7 +241,7 @@ export default function NamespaceResourcePage() {
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
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(() => {
|
||||
kubernetesApi.getClusters().then((res: any) => {
|
||||
@@ -285,6 +299,7 @@ export default function NamespaceResourcePage() {
|
||||
};
|
||||
|
||||
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 handleExportJSON = () => kubernetesApi.exportAsJSON(`${type}-${clusterCode}-${namespace}.json`, filtered);
|
||||
|
||||
@@ -314,6 +329,8 @@ export default function NamespaceResourcePage() {
|
||||
name={yamlItem.name}
|
||||
onClose={() => setYamlItem(null)}
|
||||
onSaved={loadData}
|
||||
cleanBeforeEdit={true}
|
||||
autoEdit={yamlItem.editMode === true}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-sm text-slate-500">
|
||||
@@ -369,7 +386,7 @@ export default function NamespaceResourcePage() {
|
||||
) : 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>
|
||||
) : (
|
||||
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>
|
||||
</Table>
|
||||
|
||||
@@ -93,12 +93,12 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on
|
||||
</Button>
|
||||
{!editing ? (
|
||||
<>
|
||||
{cleanBeforeEdit && (
|
||||
{effectiveClean && (
|
||||
<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" />清理并编辑
|
||||
</Button>
|
||||
)}
|
||||
{!cleanBeforeEdit && (
|
||||
{!effectiveClean && (
|
||||
<Button variant="outline" size="sm" className="h-8 border-slate-700 text-slate-300" onClick={() => setEditing(true)}>
|
||||
编辑
|
||||
</Button>
|
||||
|
||||
Reference in new issue
Block a user