refactor(k8s): 统一 YAML 精简视图与工具栏(复制/编辑/保存)
- 精简管道:对象剥离 + cleanYAML 默认值清理,查看/编辑默认精简 - 排除 status/managedFields/uid/ownerReferences 等主流噪音字段 - 移除「清理并编辑」「精简」按钮,改为「编辑」+ 文字链「原始 YAML」 - 保存走 prepareYamlForSave,ResourceVisualEditor YAML 模式同步
This commit is contained in:
1 parent
d66772481f
commit
eeba677850
3 files changed
+201
-203
No files matched your search
@@ -12,9 +12,8 @@ import { kubernetesApi } from '@/api';
|
|||||||
import CodeMirror from '@uiw/react-codemirror';
|
import CodeMirror from '@uiw/react-codemirror';
|
||||||
import { yaml } from '@codemirror/lang-yaml';
|
import { yaml } from '@codemirror/lang-yaml';
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
import { cleanYAML } from './k8s-yaml-utils';
|
|
||||||
import { createEmptyManifest } from './resource-create-templates';
|
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 {
|
interface ResourceVisualEditorProps {
|
||||||
clusterCode: string;
|
clusterCode: string;
|
||||||
@@ -89,7 +88,7 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
|||||||
const handleSaveYaml = async () => {
|
const handleSaveYaml = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const yamlStr = cleanYAML(editYaml);
|
const yamlStr = await prepareYamlForSave(editYaml);
|
||||||
if (isCreate) {
|
if (isCreate) {
|
||||||
await kubernetesApi.applyYAML(clusterCode, yamlStr);
|
await kubernetesApi.applyYAML(clusterCode, yamlStr);
|
||||||
toast({ title: '创建成功', description: 'YAML 已应用' });
|
toast({ title: '创建成功', description: 'YAML 已应用' });
|
||||||
@@ -116,10 +115,7 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
|||||||
};
|
};
|
||||||
|
|
||||||
const switchToYaml = async () => {
|
const switchToYaml = async () => {
|
||||||
if (manifest) {
|
if (manifest) setEditYaml(await prepareManifestForSave(manifest));
|
||||||
const dumped = await dumpManifest(manifest);
|
|
||||||
setEditYaml(cleanYAML(dumped));
|
|
||||||
}
|
|
||||||
setMode('yaml');
|
setMode('yaml');
|
||||||
};
|
};
|
||||||
const resourceMeta: Record<string, { title: string; icon: any; color: string }> = {
|
const resourceMeta: Record<string, { title: string; icon: any; color: string }> = {
|
||||||
@@ -170,7 +166,6 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
|||||||
<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>
|
<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' ? (
|
) : mode === 'yaml' ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<p className="text-xs text-amber-400/80">⚠️ YAML 编辑模式 — 修改后点击保存。已自动清理只读字段(status/managedFields/uid)</p>
|
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={editYaml}
|
value={editYaml}
|
||||||
height="550px"
|
height="550px"
|
||||||
@@ -181,7 +176,10 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
|||||||
style={{ fontSize: '12px' }}
|
style={{ fontSize: '12px' }}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button variant="outline" size="sm" onClick={() => { setEditYaml(cleanYAML(rawYaml)); }}>重置</Button>
|
<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}>
|
<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" />}
|
{saving ? <Loader2 className="h-3.5 w-3.5 animate-spin mr-1" /> : <Save className="h-3.5 w-3.5 mr-1" />}
|
||||||
保存
|
保存
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
// YamlViewer.tsx — YAML 查看/编辑器(CodeMirror 6 语法高亮)
|
// YamlViewer — YAML 查看/编辑(默认精简视图,工具栏:复制 / 编辑 / 保存)// Logan.Li
|
||||||
// 支持:语法高亮 + 行号 + 基本校验 + 编辑/保存 + 只读字段清理
|
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
// 作者: Logan <admin@attacker.club>
|
import { X, Copy, Check, Save, Loader2, Pencil } from 'lucide-react';
|
||||||
import { useState, useEffect, useMemo} from 'react';
|
|
||||||
import { X, Copy, Check, Save, Loader2, Sparkles} from 'lucide-react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { ModalOverlay } from '@/components/ui/modal-overlay';
|
import { ModalOverlay } from '@/components/ui/modal-overlay';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { kubernetesApi } from '@/api';
|
import { kubernetesApi } from '@/api';
|
||||||
import { cleanYAML } from './k8s-yaml-utils';
|
import { parseEditableManifest, prepareYamlForSave } from './k8s-manifest-utils';
|
||||||
import { parseEditableManifest } from './k8s-manifest-utils';
|
|
||||||
import CodeMirror from '@uiw/react-codemirror';
|
import CodeMirror from '@uiw/react-codemirror';
|
||||||
import { yaml } from '@codemirror/lang-yaml';
|
import { yaml } from '@codemirror/lang-yaml';
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
@@ -17,41 +14,76 @@ import { EditorView } from '@codemirror/view';
|
|||||||
interface YamlViewerProps {
|
interface YamlViewerProps {
|
||||||
clusterCode: string;
|
clusterCode: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
resource: string; // e.g. "deployments", "services"
|
resource: string;
|
||||||
name: string;
|
name: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSaved?: () => void;
|
onSaved?: () => void;
|
||||||
/** 进入编辑模式前是否清理只读/噪音字段(managedFields/uid/status 等) */
|
/** @deprecated 始终默认精简,保留兼容 */
|
||||||
cleanBeforeEdit?: boolean;
|
cleanBeforeEdit?: boolean;
|
||||||
/** 打开即自动进入编辑模式(编辑按钮专用入口) */
|
|
||||||
autoEdit?: boolean;
|
autoEdit?: boolean;
|
||||||
/** 内联模式(非弹窗)— 用于 YAML 编辑模式 Tab */
|
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
/** 内联模式下 YAML 内容直接传入 */
|
|
||||||
initialYaml?: string;
|
initialYaml?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function YamlViewer({ clusterCode, namespace, resource, name, onClose, onSaved, cleanBeforeEdit = false, autoEdit = false, inline = false, initialYaml }: YamlViewerProps) {
|
function YamlToolbar({
|
||||||
|
editing, saving, copied, showRaw, inline,
|
||||||
|
onCopy, onEdit, onCancel, onSave, onToggleRaw,
|
||||||
|
}: {
|
||||||
|
editing: boolean; saving: boolean; copied: boolean; showRaw: boolean; inline?: boolean;
|
||||||
|
onCopy: () => void; onEdit: () => void; onCancel: () => void; onSave: () => void; onToggleRaw: () => void;
|
||||||
|
}) {
|
||||||
|
const sz = inline ? 'h-7 text-xs' : 'h-8';
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<Button variant="outline" size="sm" className={`${sz} border-slate-700 text-slate-300`} onClick={onCopy}>
|
||||||
|
{copied ? <Check className="h-3 w-3 mr-1 text-emerald-400" /> : <Copy className="h-3 w-3 mr-1" />}
|
||||||
|
{copied ? '已复制' : '复制'}
|
||||||
|
</Button>
|
||||||
|
{!editing ? (
|
||||||
|
<Button variant="outline" size="sm" className={`${sz} border-slate-700 text-slate-300`} onClick={onEdit}>
|
||||||
|
<Pencil className="h-3 w-3 mr-1" />编辑
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button variant="outline" size="sm" className={`${sz} border-slate-700 text-slate-300`} onClick={onCancel}>取消</Button>
|
||||||
|
<Button size="sm" className={`${sz} bg-blue-600 hover:bg-blue-700`} onClick={onSave} disabled={saving}>
|
||||||
|
{saving ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : <Save className="h-3 w-3 mr-1" />}
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<button type="button" onClick={onToggleRaw} className="text-[11px] text-slate-500 hover:text-slate-300 underline-offset-2 hover:underline">
|
||||||
|
{showRaw ? '精简视图' : '原始 YAML'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function YamlViewer({
|
||||||
|
clusterCode, namespace, resource, name, onClose, onSaved,
|
||||||
|
autoEdit = false, inline = false, initialYaml,
|
||||||
|
}: YamlViewerProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [rawYaml, setRawYaml] = useState(initialYaml || ''); // 原始(只读展示用)
|
const [rawYaml, setRawYaml] = useState(initialYaml || '');
|
||||||
const [editYaml, setEditYaml] = useState(''); // 编辑态(可能被清理过)
|
const [displayYaml, setDisplayYaml] = useState('');
|
||||||
|
const [editYaml, setEditYaml] = useState('');
|
||||||
const [loading, setLoading] = useState(!initialYaml);
|
const [loading, setLoading] = useState(!initialYaml);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [editing, setEditing] = useState(autoEdit); // autoEdit → 初始即编辑态
|
const [editing, setEditing] = useState(autoEdit);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
// autoEdit 时隐式启用清理(编辑入口必须有清理,否则 status/managedFields 会被 K8s 拒绝)
|
const [showRaw, setShowRaw] = useState(false);
|
||||||
const effectiveClean = cleanBeforeEdit || autoEdit;
|
|
||||||
|
|
||||||
const toEditableYaml = async (text: string) => {
|
const applyStripped = useCallback(async (text: string) => {
|
||||||
if (!effectiveClean) return text;
|
const { yaml: stripped } = await parseEditableManifest(text);
|
||||||
const { yaml } = await parseEditableManifest(text);
|
setDisplayYaml(stripped);
|
||||||
return yaml;
|
setEditYaml(stripped);
|
||||||
};
|
return stripped;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const loadYaml = async () => {
|
const loadYaml = useCallback(async () => {
|
||||||
if (initialYaml) {
|
if (initialYaml) {
|
||||||
setRawYaml(initialYaml);
|
setRawYaml(initialYaml);
|
||||||
setEditYaml(await toEditableYaml(initialYaml));
|
await applyStripped(initialYaml);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -59,113 +91,83 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on
|
|||||||
try {
|
try {
|
||||||
const text = await kubernetesApi.getResourceYaml(clusterCode, namespace, resource, name);
|
const text = await kubernetesApi.getResourceYaml(clusterCode, namespace, resource, name);
|
||||||
setRawYaml(text);
|
setRawYaml(text);
|
||||||
setEditYaml(await toEditableYaml(text));
|
await applyStripped(text);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: '获取YAML失败', description: (e as Record<string, any>).message, variant: 'destructive' });
|
toast({ title: '获取YAML失败', description: (e as Record<string, any>).message, variant: 'destructive' });
|
||||||
const err = `# Error: ${e.message}`;
|
const err = `# Error: ${(e as Error).message}`;
|
||||||
setRawYaml(err);
|
setRawYaml(err);
|
||||||
|
setDisplayYaml(err);
|
||||||
setEditYaml(err);
|
setEditYaml(err);
|
||||||
} finally { setLoading(false); }
|
} finally { setLoading(false); }
|
||||||
};
|
}, [clusterCode, namespace, resource, name, initialYaml, applyStripped, toast]);
|
||||||
|
|
||||||
const enterEdit = async () => {
|
useEffect(() => { loadYaml(); }, [loadYaml]);
|
||||||
setEditYaml(await toEditableYaml(rawYaml));
|
|
||||||
|
const enterEdit = () => {
|
||||||
|
setEditYaml(displayYaml);
|
||||||
setEditing(true);
|
setEditing(true);
|
||||||
if (effectiveClean) {
|
setShowRaw(false);
|
||||||
toast({ title: '已清理只读字段', description: '已剥离 status / managedFields / uid 等' });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 视图模式:当 cleanBeforeEdit 时,只读视图也默认展示精简版(剥离 status/managedFields/uid/resourceVersion)
|
const cancelEdit = async () => {
|
||||||
// 用户可点击「原始」按钮切换回未处理的完整服务端输出
|
setEditing(false);
|
||||||
const [showRaw, setShowRaw] = useState(false);
|
await loadYaml();
|
||||||
|
};
|
||||||
|
|
||||||
const saveYaml = async () => {
|
const saveYaml = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
await kubernetesApi.updateResourceYaml(clusterCode, namespace, resource, name, editYaml);
|
const yamlStr = await prepareYamlForSave(editYaml);
|
||||||
toast({ title: 'YAML已应用', description: `${resource}/${name} 更新成功` });
|
await kubernetesApi.updateResourceYaml(clusterCode, namespace, resource, name, yamlStr);
|
||||||
|
toast({ title: '保存成功', description: `${resource}/${name} 已更新` });
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
setRawYaml(editYaml);
|
setRawYaml(yamlStr);
|
||||||
|
setDisplayYaml(yamlStr);
|
||||||
|
setEditYaml(yamlStr);
|
||||||
onSaved?.();
|
onSaved?.();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: '保存失败', description: (e as Record<string, any>).message, variant: 'destructive' });
|
toast({ title: '保存失败', description: (e as Error)?.message || String(e), variant: 'destructive' });
|
||||||
} finally { setSaving(false); }
|
} finally { setSaving(false); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyYaml = async () => {
|
const copyYaml = async () => {
|
||||||
const content = editing ? editYaml : rawYaml;
|
const content = editing ? editYaml : (showRaw ? rawYaml : displayYaml);
|
||||||
await navigator.clipboard.writeText(content);
|
await navigator.clipboard.writeText(content);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
setTimeout(() => setCopied(false), 2000);
|
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(() => [
|
const extensions = useMemo(() => [
|
||||||
yaml(),
|
yaml(),
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
...(editing ? [] : [EditorView.editable.of(false)]),
|
...(editing ? [] : [EditorView.editable.of(false)]),
|
||||||
], [editing]);
|
], [editing]);
|
||||||
|
|
||||||
// 内联模式 — 不显示外层弹窗,适合嵌入 Tab 页面
|
const toolbar = (
|
||||||
if (inline) {
|
<YamlToolbar
|
||||||
return (
|
editing={editing}
|
||||||
<div className="space-y-3">
|
saving={saving}
|
||||||
<div className="flex items-center justify-between">
|
copied={copied}
|
||||||
<p className="text-xs text-slate-500">
|
showRaw={showRaw}
|
||||||
<code className="text-slate-400">{resource}/{name}</code> · {namespace} · {clusterCode}
|
inline={inline}
|
||||||
</p>
|
onCopy={copyYaml}
|
||||||
<div className="flex items-center gap-2">
|
onEdit={enterEdit}
|
||||||
<Button variant="outline" size="sm" className="h-7 border-slate-700 text-slate-300 text-xs" onClick={copyYaml}>
|
onCancel={cancelEdit}
|
||||||
{copied ? <Check className="h-3 w-3 mr-1 text-emerald-400" /> : <Copy className="h-3 w-3 mr-1" />}
|
onSave={saveYaml}
|
||||||
{copied ? '已复制' : '复制'}
|
onToggleRaw={() => setShowRaw((v) => !v)}
|
||||||
</Button>
|
/>
|
||||||
{effectiveClean && !editing && (
|
);
|
||||||
<Button variant="outline" size="sm" className={`h-7 text-xs ${showRaw ? 'border-slate-700 text-slate-500' : 'border-emerald-700/50 text-emerald-400'}`}
|
|
||||||
onClick={() => setShowRaw(v => !v)} title="切换 精简/原始 视图">
|
const editor = loading ? (
|
||||||
<Sparkles className="h-3 w-3 mr-1" />{showRaw ? '原始' : '精简'}
|
<div className={`flex items-center justify-center ${inline ? 'h-96' : 'h-full'}`}>
|
||||||
</Button>
|
<Loader2 className="h-6 w-6 animate-spin text-slate-500" />
|
||||||
)}
|
|
||||||
{!editing ? (
|
|
||||||
<>
|
|
||||||
{effectiveClean && (
|
|
||||||
<Button variant="outline" size="sm" className="h-7 border-slate-700 text-slate-300 text-xs" onClick={enterEdit} title="清理 status / managedFields 等只读字段后编辑">
|
|
||||||
<Sparkles className="h-3 w-3 mr-1 text-amber-400" />编辑
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{!effectiveClean && (
|
|
||||||
<Button variant="outline" size="sm" className="h-7 border-slate-700 text-slate-300 text-xs" onClick={() => setEditing(true)}>
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Button variant="outline" size="sm" className="h-7 border-slate-700 text-slate-300 text-xs" onClick={() => { setEditing(false); loadYaml(); }}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" className="h-7 bg-blue-600 hover:bg-blue-700 text-xs" onClick={saveYaml} disabled={saving}>
|
|
||||||
{saving ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : <Save className="h-3 w-3 mr-1" />}
|
|
||||||
保存
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="border border-slate-700 rounded-lg overflow-hidden">
|
|
||||||
{loading ? (
|
|
||||||
<div className="flex items-center justify-center h-96"><Loader2 className="h-6 w-6 animate-spin text-slate-500" /></div>
|
|
||||||
) : (
|
) : (
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={display}
|
value={display}
|
||||||
height="600px"
|
height={inline ? '600px' : '100%'}
|
||||||
theme={oneDark}
|
theme={oneDark}
|
||||||
extensions={extensions}
|
extensions={extensions}
|
||||||
onChange={(val) => editing && setEditYaml(val)}
|
onChange={(val) => editing && setEditYaml(val)}
|
||||||
@@ -174,95 +176,46 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on
|
|||||||
foldGutter: true,
|
foldGutter: true,
|
||||||
highlightActiveLine: editing,
|
highlightActiveLine: editing,
|
||||||
highlightActiveLineGutter: editing,
|
highlightActiveLineGutter: editing,
|
||||||
autocompletion: true,
|
autocompletion: editing,
|
||||||
bracketMatching: true,
|
bracketMatching: true,
|
||||||
closeBrackets: true,
|
closeBrackets: editing,
|
||||||
indentOnInput: true,
|
indentOnInput: editing,
|
||||||
}}
|
}}
|
||||||
style={{ fontSize: '12px' }}
|
style={{ fontSize: inline ? '12px' : '13px', height: inline ? undefined : '100%' }}
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
|
|
||||||
|
if (inline) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
<code className="text-slate-400">{resource}/{name}</code> · {namespace}
|
||||||
|
</p>
|
||||||
|
{toolbar}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="border border-slate-700 rounded-lg overflow-hidden">{editor}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 弹窗模式 — Portal 到 body,避免 PageTransition transform 导致偏移 // Logan.Li
|
|
||||||
return (
|
return (
|
||||||
<ModalOverlay onClose={onClose}>
|
<ModalOverlay onClose={onClose}>
|
||||||
<div
|
<div
|
||||||
className="bg-slate-900 border border-slate-700 rounded-xl w-[92vw] max-w-5xl max-h-[85vh] flex flex-col shadow-2xl"
|
className="bg-slate-900 border border-slate-700 rounded-xl w-[92vw] max-w-5xl max-h-[85vh] flex flex-col shadow-2xl"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
<div className="flex items-center justify-between p-4 border-b border-slate-700 gap-3">
|
||||||
<div className="flex items-center justify-between p-4 border-b border-slate-700">
|
<div className="min-w-0">
|
||||||
<div>
|
<h2 className="text-lg font-bold text-slate-100 truncate">YAML — {resource}/{name}</h2>
|
||||||
<h2 className="text-lg font-bold text-slate-100">{editing ? 'YAML 编辑' : 'YAML 查看'} — {resource}/{name}</h2>
|
<p className="text-xs text-slate-500">{namespace} · {clusterCode}</p>
|
||||||
<p className="text-xs text-slate-500">{namespace} · {clusterCode}{editing ? ' · 已清理只读字段' : ''}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<Button variant="outline" size="sm" className="h-8 border-slate-700 text-slate-300" onClick={copyYaml}>
|
{toolbar}
|
||||||
{copied ? <Check className="h-3 w-3 mr-1 text-emerald-400" /> : <Copy className="h-3 w-3 mr-1" />}
|
|
||||||
{copied ? '已复制' : '复制'}
|
|
||||||
</Button>
|
|
||||||
{effectiveClean && !editing && (
|
|
||||||
<Button variant="outline" size="sm" className={`h-8 text-xs ${showRaw ? 'border-slate-700 text-slate-500' : 'border-emerald-700/50 text-emerald-400'}`}
|
|
||||||
onClick={() => setShowRaw(v => !v)} title="切换 精简/原始 视图">
|
|
||||||
<Sparkles className="h-3 w-3 mr-1" />{showRaw ? '原始' : '精简'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{!editing ? (
|
|
||||||
<>
|
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
{!effectiveClean && (
|
|
||||||
<Button variant="outline" size="sm" className="h-8 border-slate-700 text-slate-300" onClick={() => setEditing(true)}>
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Button variant="outline" size="sm" className="h-8 border-slate-700 text-slate-300" onClick={() => { setEditing(false); loadYaml(); }}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" className="h-8 bg-blue-600 hover:bg-blue-700" onClick={saveYaml} disabled={saving}>
|
|
||||||
{saving ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : <Save className="h-3 w-3 mr-1" />}
|
|
||||||
保存
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400" onClick={onClose}><X className="h-4 w-4" /></Button>
|
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400" onClick={onClose}><X className="h-4 w-4" /></Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Body */}
|
<div className="flex-1 min-h-0 overflow-hidden p-4">{editor}</div>
|
||||||
<div className="flex-1 min-h-0 overflow-hidden p-4">
|
|
||||||
{loading ? (
|
|
||||||
<div className="flex items-center justify-center h-full"><Loader2 className="h-6 w-6 animate-spin text-slate-500" /></div>
|
|
||||||
) : (
|
|
||||||
<CodeMirror
|
|
||||||
value={display}
|
|
||||||
height="100%"
|
|
||||||
theme={oneDark}
|
|
||||||
extensions={extensions}
|
|
||||||
onChange={(val) => 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%' }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ModalOverlay>
|
</ModalOverlay>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,40 +1,73 @@
|
|||||||
// K8s manifest 解析/序列化共用工具 // Logan.Li
|
// K8s manifest 解析/序列化/精简共用工具 // Logan.Li
|
||||||
import { cleanYAML } from './k8s-yaml-utils';
|
import { cleanYAML } from './k8s-yaml-utils';
|
||||||
|
|
||||||
|
/** 主流方案:编辑/展示时应排除的 metadata 注解 // Logan.Li */
|
||||||
const STRIP_ANNOTATIONS = new Set([
|
const STRIP_ANNOTATIONS = new Set([
|
||||||
'kubectl.kubernetes.io/last-applied-configuration',
|
'kubectl.kubernetes.io/last-applied-configuration',
|
||||||
'deployment.kubernetes.io/revision',
|
'deployment.kubernetes.io/revision',
|
||||||
'kubectl.kubernetes.io/restartedAt',
|
'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<string, any> | 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<string, unknown>;
|
||||||
|
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<string, any>): Record<string, any> {
|
export function stripRuntimeFields(manifest: Record<string, any>): Record<string, any> {
|
||||||
const cloned = JSON.parse(JSON.stringify(manifest));
|
const cloned = JSON.parse(JSON.stringify(manifest));
|
||||||
delete cloned.status;
|
delete cloned.status;
|
||||||
if (cloned.metadata) {
|
stripMetadataBlock(cloned.metadata);
|
||||||
delete cloned.metadata.managedFields;
|
const tpl = cloned.spec?.template;
|
||||||
delete cloned.metadata.resourceVersion;
|
if (tpl?.metadata) stripMetadataBlock(tpl.metadata);
|
||||||
delete cloned.metadata.uid;
|
if (cloned.spec?.nodeName) delete cloned.spec.nodeName;
|
||||||
delete cloned.metadata.creationTimestamp;
|
pruneEmptyObjects(cloned);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cloned;
|
return cloned;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function parseManifest(text: string): Promise<Record<string, any> | null> {
|
export async function parseManifest(text: string): Promise<Record<string, any> | null> {
|
||||||
if (!text?.trim()) return null;
|
if (!text?.trim()) return null;
|
||||||
try { return JSON.parse(text); } catch {}
|
try { return JSON.parse(text); } catch { /* fallthrough */ }
|
||||||
try {
|
try {
|
||||||
const jsyaml: Record<string, any> = await import('js-yaml');
|
const jsyaml: Record<string, any> = await import('js-yaml');
|
||||||
const parsed = jsyaml.load(text);
|
const parsed = jsyaml.load(text);
|
||||||
@@ -44,10 +77,17 @@ export async function parseManifest(text: string): Promise<Record<string, any> |
|
|||||||
|
|
||||||
export async function dumpManifest(manifest: Record<string, any>): Promise<string> {
|
export async function dumpManifest(manifest: Record<string, any>): Promise<string> {
|
||||||
const jsyaml: Record<string, any> = await import('js-yaml');
|
const jsyaml: Record<string, any> = 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<string, any>): Promise<string> {
|
||||||
|
const stripped = stripRuntimeFields(manifest);
|
||||||
|
const dumped = await dumpManifest(stripped);
|
||||||
|
return cleanYAML(dumped);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 原始 YAML → 可编辑 manifest + 精简 YAML // Logan.Li */
|
||||||
export async function parseEditableManifest(text: string): Promise<{
|
export async function parseEditableManifest(text: string): Promise<{
|
||||||
manifest: Record<string, any> | null;
|
manifest: Record<string, any> | null;
|
||||||
yaml: string;
|
yaml: string;
|
||||||
@@ -55,11 +95,18 @@ export async function parseEditableManifest(text: string): Promise<{
|
|||||||
const parsed = await parseManifest(text);
|
const parsed = await parseManifest(text);
|
||||||
if (!parsed) return { manifest: null, yaml: text };
|
if (!parsed) return { manifest: null, yaml: text };
|
||||||
const stripped = stripRuntimeFields(parsed);
|
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<string, any>): Promise<string> {
|
export async function prepareManifestForSave(manifest: Record<string, any>): Promise<string> {
|
||||||
const yaml = await dumpManifest(stripRuntimeFields(manifest));
|
return manifestToEditableYaml(manifest);
|
||||||
return cleanYAML(yaml);
|
}
|
||||||
|
|
||||||
|
/** 从 YAML 文本保存(解析失败时仅做文本清理)// Logan.Li */
|
||||||
|
export async function prepareYamlForSave(text: string): Promise<string> {
|
||||||
|
const parsed = await parseManifest(text);
|
||||||
|
if (parsed) return prepareManifestForSave(parsed);
|
||||||
|
return cleanYAML(text);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user