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

@@ -1,15 +1,15 @@
// YamlViewer — YAML 查看/编辑(默认精简视图,工具栏:复制 / 编辑 / 保存)// Logan.Li
import { useState, useEffect, useMemo, useCallback } from 'react';
import { X, Copy, Check, Save, Loader2, Pencil } from 'lucide-react';
import { useState, useEffect, useCallback } from 'react';
import { X, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ModalOverlay } from '@/components/ui/modal-overlay';
import { useToast } from '@/hooks/use-toast';
import { kubernetesApi } from '@/api';
import { parseEditableManifest, prepareYamlForSave } from './k8s-manifest-utils';
import CodeMirror from '@uiw/react-codemirror';
import { yaml } from '@codemirror/lang-yaml';
import { oneDark } from '@codemirror/theme-one-dark';
import { EditorView } from '@codemirror/view';
import { getYamlResourceConfig, yamlDownloadFilename } from './k8s-yaml-resource-config';
import { YamlEditorToolbar } from './YamlEditorToolbar';
import { YamlCodeMirror } from './YamlCodeMirror';
import { downloadYamlFile } from '@/utils/export';
interface YamlViewerProps {
clusterCode: string;
@@ -18,58 +18,26 @@ interface YamlViewerProps {
name: string;
onClose: () => void;
onSaved?: () => void;
/** @deprecated 始终默认精简,保留兼容 */
cleanBeforeEdit?: boolean;
autoEdit?: boolean;
inline?: boolean;
initialYaml?: string;
}
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>
);
/** 覆盖资源配置的只读行为(Pod 默认只读)// Logan.Li */
readOnly?: boolean;
}
export function YamlViewer({
clusterCode, namespace, resource, name, onClose, onSaved,
autoEdit = false, inline = false, initialYaml,
autoEdit = false, inline = false, initialYaml, readOnly: readOnlyProp,
}: YamlViewerProps) {
const { toast } = useToast();
const resCfg = getYamlResourceConfig(resource);
const readOnly = readOnlyProp ?? resCfg.readOnly;
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);
const [editing, setEditing] = useState(autoEdit && !readOnly);
const [copied, setCopied] = useState(false);
const [showRaw, setShowRaw] = useState(false);
@@ -130,29 +98,31 @@ export function YamlViewer({
} finally { setSaving(false); }
};
const currentYaml = () => (editing ? editYaml : (showRaw ? rawYaml : displayYaml));
const copyYaml = async () => {
const content = editing ? editYaml : (showRaw ? rawYaml : displayYaml);
await navigator.clipboard.writeText(content);
await navigator.clipboard.writeText(currentYaml());
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const downloadYaml = () => {
downloadYamlFile(yamlDownloadFilename(resource, namespace, name), currentYaml());
toast({ title: '已下载', description: yamlDownloadFilename(resource, namespace, name) });
};
const display = editing ? editYaml : (showRaw ? rawYaml : displayYaml);
const extensions = useMemo(() => [
yaml(),
EditorView.lineWrapping,
...(editing ? [] : [EditorView.editable.of(false)]),
], [editing]);
const toolbar = (
<YamlToolbar
<YamlEditorToolbar
editing={editing}
saving={saving}
copied={copied}
showRaw={showRaw}
inline={inline}
compact={inline}
readOnly={readOnly}
onCopy={copyYaml}
onDownload={downloadYaml}
onEdit={enterEdit}
onCancel={cancelEdit}
onSave={saveYaml}
@@ -161,40 +131,29 @@ export function YamlViewer({
);
const editor = loading ? (
<div className={`flex items-center justify-center ${inline ? 'h-96' : 'h-full'}`}>
<div className="flex items-center justify-center h-full min-h-[200px]">
<Loader2 className="h-6 w-6 animate-spin text-slate-500" />
</div>
) : (
<CodeMirror
<YamlCodeMirror
value={display}
height={inline ? '600px' : '100%'}
theme={oneDark}
extensions={extensions}
editing={editing}
fill
fontSize={inline ? '12px' : '13px'}
onChange={(val) => 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 (
<div className="space-y-3">
<div className="flex items-center justify-between gap-3 flex-wrap">
<div className="flex flex-col gap-3 min-h-[400px] h-[calc(100vh-220px)] max-h-[720px]">
<div className="flex items-center justify-between gap-3 flex-wrap shrink-0">
<p className="text-xs text-slate-500">
<code className="text-slate-400">{resource}/{name}</code> · {namespace}
</p>
{toolbar}
</div>
<div className="border border-slate-700 rounded-lg overflow-hidden">{editor}</div>
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">{editor}</div>
</div>
);
}
@@ -202,12 +161,15 @@ export function YamlViewer({
return (
<ModalOverlay onClose={onClose}>
<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 h-[85vh] flex flex-col shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<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 gap-3 shrink-0">
<div className="min-w-0">
<h2 className="text-lg font-bold text-slate-100 truncate">YAML — {resource}/{name}</h2>
<h2 className="text-lg font-bold text-slate-100 truncate">
{resCfg.label} YAML — {name}
{readOnly && <span className="ml-2 text-xs font-normal text-slate-500">只读</span>}
</h2>
<p className="text-xs text-slate-500">{namespace} · {clusterCode}</p>
</div>
<div className="flex items-center gap-2 shrink-0">
@@ -215,7 +177,7 @@ export function YamlViewer({
<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 className="flex-1 min-h-0 overflow-hidden p-4">{editor}</div>
<div className="flex-1 min-h-0 flex flex-col overflow-hidden p-4">{editor}</div>
</div>
</ModalOverlay>
);