Files
aiops/frontend/src/components/kubernetes/YamlViewer.tsx
T

202 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// YamlViewer — YAML 查看/编辑(默认精简视图,工具栏:复制 / 编辑 / 保存)// Logan.Li
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, parseEditableManifests, prepareYamlForSave } from './k8s-manifest-utils';
import { getYamlResourceConfig, yamlDownloadFilename } from './k8s-yaml-resource-config';
import { YamlEditorToolbar } from './YamlEditorToolbar';
import { YamlCodeMirror } from './YamlCodeMirror';
import { downloadYamlFile } from '@/utils/export';
import type { JsonData } from '@/types/common';
interface YamlViewerProps {
clusterCode: string;
namespace: string;
resource: string;
name: string;
onClose: () => void;
onSaved?: () => void;
autoEdit?: boolean;
inline?: boolean;
initialYaml?: string;
/** 覆盖资源配置的只读行为(Pod 默认只读)// Logan.Li */
readOnly?: boolean;
/** 自定义加载(如 Deployment + Service 多文档)// Logan.Li */
loadYamlFn?: () => Promise<string>;
/** 自定义保存(如 Deployment + Service 多文档)// Logan.Li */
saveYamlFn?: (yaml: string) => Promise<void>;
/** 资源描述补充(如 "含关联 Service")// Logan.Li */
resourceHint?: string;
}
export function YamlViewer({
clusterCode, namespace, resource, name, onClose, onSaved,
autoEdit = false, inline = false, initialYaml, readOnly: readOnlyProp,
loadYamlFn, saveYamlFn, resourceHint,
}: 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 && !readOnly);
const [copied, setCopied] = useState(false);
const [showRaw, setShowRaw] = useState(false);
const applyStripped = useCallback(async (text: string) => {
const { yaml: stripped } = text.includes('\n---')
? await parseEditableManifests(text)
: await parseEditableManifest(text);
setDisplayYaml(stripped);
setEditYaml(stripped);
return stripped;
}, []);
const loadYaml = useCallback(async () => {
if (initialYaml) {
setRawYaml(initialYaml);
await applyStripped(initialYaml);
setLoading(false);
return;
}
setLoading(true);
try {
const text = loadYamlFn
? await loadYamlFn()
: await kubernetesApi.getResourceYaml(clusterCode, namespace, resource, name);
setRawYaml(text);
await applyStripped(text);
} catch (e) {
toast({ title: '获取YAML失败', description: (e as JsonData).message, variant: 'destructive' });
const err = `# Error: ${(e as Error).message}`;
setRawYaml(err);
setDisplayYaml(err);
setEditYaml(err);
} finally { setLoading(false); }
}, [clusterCode, namespace, resource, name, initialYaml, loadYamlFn, applyStripped, toast]);
useEffect(() => { loadYaml(); }, [loadYaml]);
const enterEdit = () => {
setEditYaml(displayYaml);
setEditing(true);
setShowRaw(false);
};
const cancelEdit = async () => {
setEditing(false);
await loadYaml();
};
const saveYaml = async () => {
setSaving(true);
try {
const yamlStr = await prepareYamlForSave(editYaml);
if (saveYamlFn) {
await saveYamlFn(yamlStr);
} else {
await kubernetesApi.updateResourceYaml(clusterCode, namespace, resource, name, yamlStr);
}
toast({ title: '保存成功', description: saveYamlFn ? '多文档 YAML 已应用' : `${resource}/${name} 已更新` });
setEditing(false);
setRawYaml(yamlStr);
setDisplayYaml(yamlStr);
setEditYaml(yamlStr);
onSaved?.();
} catch (e) {
toast({ title: '保存失败', description: (e as Error)?.message || String(e), variant: 'destructive' });
} finally { setSaving(false); }
};
const currentYaml = () => (editing ? editYaml : (showRaw ? rawYaml : displayYaml));
const copyYaml = async () => {
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 toolbar = (
<YamlEditorToolbar
editing={editing}
saving={saving}
copied={copied}
showRaw={showRaw}
compact={inline}
readOnly={readOnly}
onCopy={copyYaml}
onDownload={downloadYaml}
onEdit={enterEdit}
onCancel={cancelEdit}
onSave={saveYaml}
onToggleRaw={() => setShowRaw((v) => !v)}
/>
);
const editor = loading ? (
<div className="flex items-center justify-center h-full min-h-[200px]">
<Loader2 className="h-6 w-6 animate-spin text-slate-500" />
</div>
) : (
<YamlCodeMirror
value={display}
editing={editing}
fill
fontSize={inline ? '12px' : '13px'}
onChange={(val) => editing && setEditYaml(val)}
/>
);
if (inline) {
return (
<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>
{resourceHint ? <span className="text-slate-600"> · {resourceHint}</span> : null}
{' · '}{namespace}
</p>
{toolbar}
</div>
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">{editor}</div>
</div>
);
}
return (
<ModalOverlay onClose={onClose}>
<div
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 shrink-0">
<div className="min-w-0 flex-1">
<h2 className="text-lg font-bold text-slate-100 truncate" title={`${resCfg.label} YAML — ${name}`}>
{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 truncate" title={`${namespace} · ${clusterCode}`}>{namespace} · {clusterCode}</p>
</div>
<div className="flex items-center gap-2 shrink-0">
{toolbar}
<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 flex flex-col overflow-hidden p-4">{editor}</div>
</div>
</ModalOverlay>
);
}