feat(k8s): 统一 YAML 查看编辑并补齐 Pod/导出能力

- 抽取 YamlCodeMirror/YamlEditorToolbar,修复弹窗滚动与固定高度布局
- 对齐集群导出精简规则(Service/Ingress/Workload/PVC 注解等)
- Deployment/Pod/Service/Ingress 分资源配置:Pod 只读,支持单文件下载
- 修复 Deployment Pod YAML 弹窗未渲染,补全 PodsTab 与详情页 YAML 入口
- 命名空间列表页新增导出 YAML(tar.gz),与集群导出 API 一致
This commit is contained in:
Logan.Li committed 2026-06-26 15:25:46 +08:00
1 parent eeba677850
commit fd7b04b9d6
15 files changed
+651 -150

No files matched your search

@@ -9,11 +9,12 @@ import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useToast } from '@/hooks/use-toast';
import { kubernetesApi } from '@/api';
import CodeMirror from '@uiw/react-codemirror';
import { yaml } from '@codemirror/lang-yaml';
import { oneDark } from '@codemirror/theme-one-dark';
import { createEmptyManifest } from './resource-create-templates';
import { parseManifest, parseEditableManifest, prepareManifestForSave, prepareYamlForSave } from './k8s-manifest-utils';
import { YamlEditorToolbar } from './YamlEditorToolbar';
import { YamlCodeMirror } from './YamlCodeMirror';
import { getYamlResourceConfig, yamlDownloadFilename } from './k8s-yaml-resource-config';
import { downloadYamlFile } from '@/utils/export';
interface ResourceVisualEditorProps {
clusterCode: string;
@@ -33,23 +34,33 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
const [rawYaml, setRawYaml] = useState('');
const [manifest, setManifest] = useState<any>(null);
const [mode, setMode] = useState<'visual' | 'yaml'>('visual');
const [displayYaml, setDisplayYaml] = useState('');
const [editYaml, setEditYaml] = useState('');
const [yamlEditing, setYamlEditing] = useState(false);
const [showRawYaml, setShowRawYaml] = useState(false);
const [yamlCopied, setYamlCopied] = useState(false);
const loadData = useCallback(async () => {
setLoading(true);
try {
if (isCreate) {
const empty = createEmptyManifest(resource, namespace, name || '');
const yamlStr = await prepareManifestForSave(empty);
setManifest(empty);
setEditYaml(JSON.stringify(empty, null, 2));
setDisplayYaml(yamlStr);
setEditYaml(yamlStr);
setRawYaml('');
setYamlEditing(true);
return;
}
const text = await kubernetesApi.getResourceYaml(clusterCode, namespace, resource, name);
setRawYaml(text);
// 先解析原始 YAML,再用对象级剥离;cleanYAML 会破坏 managedFields 导致解析失败 // Logan.Li
const { manifest: parsed, yaml: editableYaml } = await parseEditableManifest(text);
setDisplayYaml(editableYaml);
setEditYaml(editableYaml);
setYamlEditing(false);
setShowRawYaml(false);
setManifest(parsed);
if (!parsed) {
setMode('yaml');
@@ -94,8 +105,12 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
toast({ title: '创建成功', description: 'YAML 已应用' });
} else {
await kubernetesApi.updateResourceYaml(clusterCode, namespace, resource, name, yamlStr);
toast({ title: 'YAML 已应用', description: `${resource}/${name} 更新成功` });
toast({ title: '保存成功', description: `${resource}/${name} 已更新` });
}
setYamlEditing(false);
setDisplayYaml(yamlStr);
setEditYaml(yamlStr);
if (!isCreate) setRawYaml(yamlStr);
onSaved?.();
onClose();
} catch (e) {
@@ -115,9 +130,31 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
};
const switchToYaml = async () => {
if (manifest) setEditYaml(await prepareManifestForSave(manifest));
if (manifest) {
const yamlStr = await prepareManifestForSave(manifest);
setDisplayYaml(yamlStr);
setEditYaml(yamlStr);
}
setYamlEditing(false);
setShowRawYaml(false);
setMode('yaml');
};
const currentYamlText = () => (yamlEditing ? editYaml : (showRawYaml ? rawYaml : displayYaml));
const copyYamlContent = async () => {
await navigator.clipboard.writeText(currentYamlText());
setYamlCopied(true);
setTimeout(() => setYamlCopied(false), 2000);
};
const downloadYamlContent = () => {
const fname = yamlDownloadFilename(resource, namespace, name || manifest?.metadata?.name || 'resource');
downloadYamlFile(fname, currentYamlText());
toast({ title: '已下载', description: fname });
};
const resCfg = getYamlResourceConfig(resource);
const resourceMeta: Record<string, { title: string; icon: any; color: string }> = {
configmaps: { title: 'ConfigMap', icon: FileText, color: 'text-blue-400' },
secrets: { title: 'Secret', icon: Shield, color: 'text-amber-400' },
@@ -131,7 +168,7 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
return (
<ModalOverlay onClose={onClose}>
<div
className="bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-xl w-[90vw] max-w-3xl max-h-[85vh] flex flex-col shadow-2xl"
className="bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-xl w-[90vw] max-w-3xl h-[85vh] flex flex-col shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
@@ -161,30 +198,47 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
</div>
{/* Body */}
<div className="flex-1 overflow-auto p-4">
<div className={`flex-1 min-h-0 p-4 ${mode === 'yaml' ? 'flex flex-col overflow-hidden' : 'overflow-auto'}`}>
{loading ? (
<div className="flex items-center justify-center h-full py-20"><Loader2 className="h-6 w-6 animate-spin text-[var(--dg-text-muted)]" /></div>
) : mode === 'yaml' ? (
<div className="space-y-3">
<CodeMirror
value={editYaml}
height="550px"
theme={oneDark}
extensions={[yaml()]}
onChange={(val) => setEditYaml(val)}
basicSetup={{ lineNumbers: true, foldGutter: true, highlightActiveLine: true, autocompletion: true }}
style={{ fontSize: '12px' }}
/>
<div className="flex justify-end gap-2">
<Button variant="outline" size="sm" onClick={async () => {
const { yaml: stripped } = await parseEditableManifest(rawYaml);
setEditYaml(stripped);
}}>重置</Button>
<Button size="sm" className="bg-blue-600 hover:bg-blue-700" onClick={handleSaveYaml} disabled={saving}>
{saving ? <Loader2 className="h-3.5 w-3.5 animate-spin mr-1" /> : <Save className="h-3.5 w-3.5 mr-1" />}
保存
</Button>
<div className="flex flex-col flex-1 min-h-0 gap-3">
<div className="flex justify-end shrink-0">
<YamlEditorToolbar
editing={yamlEditing}
saving={saving}
copied={yamlCopied}
showRaw={showRawYaml}
compact
readOnly={resCfg.readOnly}
showRawToggle={!isCreate && !!rawYaml}
onCopy={copyYamlContent}
onDownload={downloadYamlContent}
onEdit={() => { setEditYaml(displayYaml); setYamlEditing(true); setShowRawYaml(false); }}
onCancel={async () => {
setYamlEditing(false);
if (isCreate) {
const empty = createEmptyManifest(resource, namespace, name || '');
const yamlStr = await prepareManifestForSave(empty);
setDisplayYaml(yamlStr);
setEditYaml(yamlStr);
return;
}
const { yaml: stripped } = await parseEditableManifest(rawYaml);
setDisplayYaml(stripped);
setEditYaml(stripped);
}}
onSave={handleSaveYaml}
onToggleRaw={() => setShowRawYaml((v) => !v)}
/>
</div>
<YamlCodeMirror
value={yamlEditing ? editYaml : (showRawYaml ? rawYaml : displayYaml)}
editing={yamlEditing}
fill
fontSize="12px"
onChange={(val) => yamlEditing && setEditYaml(val)}
/>
</div>
) : manifest ? (
<>