From eeba67785039f3d025823ded921dff7921fa9426 Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Fri, 26 Jun 2026 15:19:26 +0800 Subject: [PATCH] =?UTF-8?q?refactor(k8s):=20=E7=BB=9F=E4=B8=80=20YAML=20?= =?UTF-8?q?=E7=B2=BE=E7=AE=80=E8=A7=86=E5=9B=BE=E4=B8=8E=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E6=A0=8F=EF=BC=88=E5=A4=8D=E5=88=B6/=E7=BC=96=E8=BE=91/?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 精简管道:对象剥离 + cleanYAML 默认值清理,查看/编辑默认精简 - 排除 status/managedFields/uid/ownerReferences 等主流噪音字段 - 移除「清理并编辑」「精简」按钮,改为「编辑」+ 文字链「原始 YAML」 - 保存走 prepareYamlForSave,ResourceVisualEditor YAML 模式同步 --- .../kubernetes/ResourceVisualEditor.tsx | 16 +- .../src/components/kubernetes/YamlViewer.tsx | 309 ++++++++---------- .../kubernetes/k8s-manifest-utils.ts | 101 ++++-- 3 files changed, 212 insertions(+), 214 deletions(-) diff --git a/frontend/src/components/kubernetes/ResourceVisualEditor.tsx b/frontend/src/components/kubernetes/ResourceVisualEditor.tsx index 5aeac79..0a2b3be 100644 --- a/frontend/src/components/kubernetes/ResourceVisualEditor.tsx +++ b/frontend/src/components/kubernetes/ResourceVisualEditor.tsx @@ -12,9 +12,8 @@ import { kubernetesApi } from '@/api'; import CodeMirror from '@uiw/react-codemirror'; import { yaml } from '@codemirror/lang-yaml'; import { oneDark } from '@codemirror/theme-one-dark'; -import { cleanYAML } from './k8s-yaml-utils'; import { createEmptyManifest } from './resource-create-templates'; -import { parseManifest, parseEditableManifest, dumpManifest, prepareManifestForSave } from './k8s-manifest-utils'; +import { parseManifest, parseEditableManifest, prepareManifestForSave, prepareYamlForSave } from './k8s-manifest-utils'; interface ResourceVisualEditorProps { clusterCode: string; @@ -89,7 +88,7 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o const handleSaveYaml = async () => { setSaving(true); try { - const yamlStr = cleanYAML(editYaml); + const yamlStr = await prepareYamlForSave(editYaml); if (isCreate) { await kubernetesApi.applyYAML(clusterCode, yamlStr); toast({ title: '创建成功', description: 'YAML 已应用' }); @@ -116,10 +115,7 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o }; const switchToYaml = async () => { - if (manifest) { - const dumped = await dumpManifest(manifest); - setEditYaml(cleanYAML(dumped)); - } + if (manifest) setEditYaml(await prepareManifestForSave(manifest)); setMode('yaml'); }; const resourceMeta: Record = { @@ -170,7 +166,6 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
) : mode === 'yaml' ? (
-

⚠️ YAML 编辑模式 — 修改后点击保存。已自动清理只读字段(status/managedFields/uid)

- + + {!editing ? ( + + ) : ( + <> + + + + )} + +
+ ); +} + +export function YamlViewer({ + clusterCode, namespace, resource, name, onClose, onSaved, + autoEdit = false, inline = false, initialYaml, +}: YamlViewerProps) { const { toast } = useToast(); - const [rawYaml, setRawYaml] = useState(initialYaml || ''); // 原始(只读展示用) - const [editYaml, setEditYaml] = useState(''); // 编辑态(可能被清理过) + const [rawYaml, setRawYaml] = useState(initialYaml || ''); + const [displayYaml, setDisplayYaml] = useState(''); + const [editYaml, setEditYaml] = useState(''); const [loading, setLoading] = useState(!initialYaml); const [saving, setSaving] = useState(false); - const [editing, setEditing] = useState(autoEdit); // autoEdit → 初始即编辑态 + const [editing, setEditing] = useState(autoEdit); const [copied, setCopied] = useState(false); - // autoEdit 时隐式启用清理(编辑入口必须有清理,否则 status/managedFields 会被 K8s 拒绝) - const effectiveClean = cleanBeforeEdit || autoEdit; + const [showRaw, setShowRaw] = useState(false); - const toEditableYaml = async (text: string) => { - if (!effectiveClean) return text; - const { yaml } = await parseEditableManifest(text); - return yaml; - }; + const applyStripped = useCallback(async (text: string) => { + const { yaml: stripped } = await parseEditableManifest(text); + setDisplayYaml(stripped); + setEditYaml(stripped); + return stripped; + }, []); - const loadYaml = async () => { + const loadYaml = useCallback(async () => { if (initialYaml) { setRawYaml(initialYaml); - setEditYaml(await toEditableYaml(initialYaml)); + await applyStripped(initialYaml); setLoading(false); return; } @@ -59,211 +91,132 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on try { const text = await kubernetesApi.getResourceYaml(clusterCode, namespace, resource, name); setRawYaml(text); - setEditYaml(await toEditableYaml(text)); + await applyStripped(text); } catch (e) { toast({ title: '获取YAML失败', description: (e as Record).message, variant: 'destructive' }); - const err = `# Error: ${e.message}`; + const err = `# Error: ${(e as Error).message}`; setRawYaml(err); + setDisplayYaml(err); setEditYaml(err); } finally { setLoading(false); } - }; + }, [clusterCode, namespace, resource, name, initialYaml, applyStripped, toast]); - const enterEdit = async () => { - setEditYaml(await toEditableYaml(rawYaml)); + useEffect(() => { loadYaml(); }, [loadYaml]); + + const enterEdit = () => { + setEditYaml(displayYaml); setEditing(true); - if (effectiveClean) { - toast({ title: '已清理只读字段', description: '已剥离 status / managedFields / uid 等' }); - } + setShowRaw(false); }; - // 视图模式:当 cleanBeforeEdit 时,只读视图也默认展示精简版(剥离 status/managedFields/uid/resourceVersion) - // 用户可点击「原始」按钮切换回未处理的完整服务端输出 - const [showRaw, setShowRaw] = useState(false); + const cancelEdit = async () => { + setEditing(false); + await loadYaml(); + }; const saveYaml = async () => { setSaving(true); try { - await kubernetesApi.updateResourceYaml(clusterCode, namespace, resource, name, editYaml); - toast({ title: 'YAML已应用', description: `${resource}/${name} 更新成功` }); + const yamlStr = await prepareYamlForSave(editYaml); + await kubernetesApi.updateResourceYaml(clusterCode, namespace, resource, name, yamlStr); + toast({ title: '保存成功', description: `${resource}/${name} 已更新` }); setEditing(false); - setRawYaml(editYaml); + setRawYaml(yamlStr); + setDisplayYaml(yamlStr); + setEditYaml(yamlStr); onSaved?.(); } catch (e) { - toast({ title: '保存失败', description: (e as Record).message, variant: 'destructive' }); + toast({ title: '保存失败', description: (e as Error)?.message || String(e), variant: 'destructive' }); } finally { setSaving(false); } }; const copyYaml = async () => { - const content = editing ? editYaml : rawYaml; + const content = editing ? editYaml : (showRaw ? rawYaml : displayYaml); await navigator.clipboard.writeText(content); setCopied(true); setTimeout(() => setCopied(false), 2000); }; - useEffect(() => { loadYaml(); }, []); + const display = editing ? editYaml : (showRaw ? rawYaml : displayYaml); - // 编辑态用 editYaml;只读态根据 effectiveClean + showRaw 决定是否清理 - const display = editing - ? editYaml - : (effectiveClean && !showRaw ? cleanYAML(rawYaml) : rawYaml); - - // CodeMirror 主题扩展 const extensions = useMemo(() => [ yaml(), EditorView.lineWrapping, ...(editing ? [] : [EditorView.editable.of(false)]), ], [editing]); - // 内联模式 — 不显示外层弹窗,适合嵌入 Tab 页面 + const toolbar = ( + setShowRaw((v) => !v)} + /> + ); + + const editor = loading ? ( +
+ +
+ ) : ( + editing && setEditYaml(val)} + basicSetup={{ + lineNumbers: true, + foldGutter: true, + highlightActiveLine: editing, + highlightActiveLineGutter: editing, + autocompletion: editing, + bracketMatching: true, + closeBrackets: editing, + indentOnInput: editing, + }} + style={{ fontSize: inline ? '12px' : '13px', height: inline ? undefined : '100%' }} + /> + ); + if (inline) { return (
-
+

- {resource}/{name} · {namespace} · {clusterCode} + {resource}/{name} · {namespace}

-
- - {effectiveClean && !editing && ( - - )} - {!editing ? ( - <> - {effectiveClean && ( - - )} - {!effectiveClean && ( - - )} - - ) : ( - <> - - - - )} -
-
-
- {loading ? ( -
- ) : ( - editing && setEditYaml(val)} - basicSetup={{ - lineNumbers: true, - foldGutter: true, - highlightActiveLine: editing, - highlightActiveLineGutter: editing, - autocompletion: true, - bracketMatching: true, - closeBrackets: true, - indentOnInput: true, - }} - style={{ fontSize: '12px' }} - /> - )} + {toolbar}
+
{editor}
); } - // 弹窗模式 — Portal 到 body,避免 PageTransition transform 导致偏移 // Logan.Li return (
e.stopPropagation()} > - {/* Header */} -
-
-

{editing ? 'YAML 编辑' : 'YAML 查看'} — {resource}/{name}

-

{namespace} · {clusterCode}{editing ? ' · 已清理只读字段' : ''}

+
+
+

YAML — {resource}/{name}

+

{namespace} · {clusterCode}

-
- - {effectiveClean && !editing && ( - - )} - {!editing ? ( - <> - {effectiveClean && ( - - )} - {!effectiveClean && ( - - )} - - ) : ( - <> - - - - )} +
+ {toolbar}
- {/* Body */} -
- {loading ? ( -
- ) : ( - editing && setEditYaml(val)} - basicSetup={{ - lineNumbers: true, - foldGutter: true, - highlightActiveLine: editing, - highlightActiveLineGutter: editing, - autocompletion: true, - bracketMatching: true, - closeBrackets: true, - indentOnInput: true, - }} - style={{ fontSize: '13px', height: '100%' }} - /> - )} -
+
{editor}
); -} +} \ No newline at end of file diff --git a/frontend/src/components/kubernetes/k8s-manifest-utils.ts b/frontend/src/components/kubernetes/k8s-manifest-utils.ts index 051d95d..e38f236 100644 --- a/frontend/src/components/kubernetes/k8s-manifest-utils.ts +++ b/frontend/src/components/kubernetes/k8s-manifest-utils.ts @@ -1,40 +1,73 @@ -// K8s manifest 解析/序列化共用工具 // Logan.Li +// K8s manifest 解析/序列化/精简共用工具 // Logan.Li import { cleanYAML } from './k8s-yaml-utils'; +/** 主流方案:编辑/展示时应排除的 metadata 注解 // Logan.Li */ const STRIP_ANNOTATIONS = new Set([ 'kubectl.kubernetes.io/last-applied-configuration', 'deployment.kubernetes.io/revision', 'kubectl.kubernetes.io/restartedAt', + 'control-plane.alpha.kubernetes.io/leader', + 'autoscaling.alpha.kubernetes.io/conditions', + 'kubernetes.io/config.hash', + 'kubeadm.kubernetes.io/etcd.advertise-client-urls', ]); -/** 对象级剥离运行时字段(比 cleanYAML 文本处理更可靠)// Logan.Li */ +/** metadata 运行时字段(kubectl/Kuboard/Lens 等均不展示)// Logan.Li */ +const STRIP_METADATA_KEYS = [ + 'managedFields', 'resourceVersion', 'uid', 'creationTimestamp', 'generation', + 'selfLink', 'deletionTimestamp', 'ownerReferences', 'finalizers', 'clusterName', 'generateName', +] as const; + +function stripMetadataBlock(meta: Record | undefined): void { + if (!meta || typeof meta !== 'object') return; + for (const k of STRIP_METADATA_KEYS) delete meta[k]; + if (meta.annotations) { + for (const k of Object.keys(meta.annotations)) { + if (STRIP_ANNOTATIONS.has(k)) delete meta.annotations[k]; + } + if (Object.keys(meta.annotations).length === 0) delete meta.annotations; + } + if (meta.labels) { + delete meta.labels['pod-template-hash']; + if (Object.keys(meta.labels).length === 0) delete meta.labels; + } +} + +/** 递归移除空对象(如 securityContext: {})// Logan.Li */ +function pruneEmptyObjects(node: unknown): void { + if (!node || typeof node !== 'object') return; + if (Array.isArray(node)) { + node.forEach(pruneEmptyObjects); + return; + } + const obj = node as Record; + for (const key of Object.keys(obj)) { + const val = obj[key]; + if (val && typeof val === 'object') { + if (Array.isArray(val)) val.forEach(pruneEmptyObjects); + else pruneEmptyObjects(val); + if (!Array.isArray(val) && typeof val === 'object' && Object.keys(val as object).length === 0) { + delete obj[key]; + } + } + } +} + +/** 对象级剥离运行时字段 // Logan.Li */ export function stripRuntimeFields(manifest: Record): Record { const cloned = JSON.parse(JSON.stringify(manifest)); delete cloned.status; - if (cloned.metadata) { - delete cloned.metadata.managedFields; - delete cloned.metadata.resourceVersion; - delete cloned.metadata.uid; - delete cloned.metadata.creationTimestamp; - delete cloned.metadata.generation; - delete cloned.metadata.selfLink; - if (cloned.metadata.annotations) { - for (const k of Object.keys(cloned.metadata.annotations)) { - if (STRIP_ANNOTATIONS.has(k)) delete cloned.metadata.annotations[k]; - } - if (Object.keys(cloned.metadata.annotations).length === 0) delete cloned.metadata.annotations; - } - if (cloned.metadata.labels) { - delete cloned.metadata.labels['pod-template-hash']; - if (Object.keys(cloned.metadata.labels).length === 0) delete cloned.metadata.labels; - } - } + stripMetadataBlock(cloned.metadata); + const tpl = cloned.spec?.template; + if (tpl?.metadata) stripMetadataBlock(tpl.metadata); + if (cloned.spec?.nodeName) delete cloned.spec.nodeName; + pruneEmptyObjects(cloned); return cloned; } export async function parseManifest(text: string): Promise | null> { if (!text?.trim()) return null; - try { return JSON.parse(text); } catch {} + try { return JSON.parse(text); } catch { /* fallthrough */ } try { const jsyaml: Record = await import('js-yaml'); const parsed = jsyaml.load(text); @@ -44,10 +77,17 @@ export async function parseManifest(text: string): Promise | export async function dumpManifest(manifest: Record): Promise { const jsyaml: Record = await import('js-yaml'); - return jsyaml.dump(manifest, { lineWidth: 120, noRefs: true }); + return jsyaml.dump(manifest, { lineWidth: 120, noRefs: true, sortKeys: false }); } -/** 原始 YAML → 可编辑 manifest + 精简 YAML(可视化/编辑共用)// Logan.Li */ +/** manifest → 精简 YAML(对象剥离 + 文本级默认值清理)// Logan.Li */ +export async function manifestToEditableYaml(manifest: Record): Promise { + const stripped = stripRuntimeFields(manifest); + const dumped = await dumpManifest(stripped); + return cleanYAML(dumped); +} + +/** 原始 YAML → 可编辑 manifest + 精简 YAML // Logan.Li */ export async function parseEditableManifest(text: string): Promise<{ manifest: Record | null; yaml: string; @@ -55,11 +95,18 @@ export async function parseEditableManifest(text: string): Promise<{ const parsed = await parseManifest(text); if (!parsed) return { manifest: null, yaml: text }; const stripped = stripRuntimeFields(parsed); - return { manifest: stripped, yaml: await dumpManifest(stripped) }; + const yaml = await manifestToEditableYaml(stripped); + return { manifest: stripped, yaml }; } -/** 保存前剥离 status 等噪音字段 // Logan.Li */ +/** 保存前:剥离运行时字段 + 清理默认值 // Logan.Li */ export async function prepareManifestForSave(manifest: Record): Promise { - const yaml = await dumpManifest(stripRuntimeFields(manifest)); - return cleanYAML(yaml); + return manifestToEditableYaml(manifest); +} + +/** 从 YAML 文本保存(解析失败时仅做文本清理)// Logan.Li */ +export async function prepareYamlForSave(text: string): Promise { + const parsed = await parseManifest(text); + if (parsed) return prepareManifestForSave(parsed); + return cleanYAML(text); } \ No newline at end of file