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 列表 | ✅ | ✅ | ✅ |
|
| 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
@@ -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>
|
||||||
|
|||||||
Reference in new issue
Block a user