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

@@ -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>