Files
aiops/frontend/src/components/kubernetes/YamlViewer.tsx
T
attacker 5364d93ed7 fix(q1): 修复 tsc -b 构建中断 — 回退不安全的 unknown→any + 移除 Ansible 残留引用
- 781处 tsc -b 错误清零(unknown 属性访问/隐式any返回类型/类型转换不匹配)
- 移除 App.tsx 中已删除的 AnsibleManager/AnsibleWorkflow 导入和路由
- 修复 DeploymentView 中 _prefix 解构导致的属性不存在错误
- 修复 Header/Sidebar/PipelineEditor 中无效的类型断言
- asList() 返回类型 unknown[]→any[], 恢复 AgentMonitor 正常工作
- 放宽 noImplicitAny(临时), 待 types/ 定义完善后逐步恢复
- ESLint: unused=0, console=0, any=1041(Q1 后续降低)
- npm run build: ✅ 0 errors
2026-06-25 07:17:47 +08:00

260 lines
11 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.tsx — YAML 查看/编辑器(CodeMirror 6 语法高亮)
// 支持:语法高亮 + 行号 + 基本校验 + 编辑/保存 + 只读字段清理
// 作者: Logan <admin@attacker.club>
import { useState, useEffect, useMemo} from 'react';
import { X, Copy, Check, Save, Loader2, Sparkles} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useToast } from '@/hooks/use-toast';
import { kubernetesApi } from '@/services/api';
import { cleanYAML } from './k8s-yaml-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';
interface YamlViewerProps {
clusterCode: string;
namespace: string;
resource: string; // e.g. "deployments", "services"
name: string;
onClose: () => void;
onSaved?: () => void;
/** 进入编辑模式前是否清理只读/噪音字段(managedFields/uid/status 等) */
cleanBeforeEdit?: boolean;
/** 打开即自动进入编辑模式(编辑按钮专用入口) */
autoEdit?: boolean;
/** 内联模式(非弹窗)— 用于 YAML 编辑模式 Tab */
inline?: boolean;
/** 内联模式下 YAML 内容直接传入 */
initialYaml?: string;
}
export function YamlViewer({ clusterCode, namespace, resource, name, onClose, onSaved, cleanBeforeEdit = false, autoEdit = false, inline = false, initialYaml }: YamlViewerProps) {
const { toast } = useToast();
const [rawYaml, setRawYaml] = useState(initialYaml || ''); // 原始(只读展示用)
const [editYaml, setEditYaml] = useState(''); // 编辑态(可能被清理过)
const [loading, setLoading] = useState(!initialYaml);
const [saving, setSaving] = useState(false);
const [editing, setEditing] = useState(autoEdit); // autoEdit → 初始即编辑态
const [copied, setCopied] = useState(false);
// autoEdit 时隐式启用清理(编辑入口必须有清理,否则 status/managedFields 会被 K8s 拒绝)
const effectiveClean = cleanBeforeEdit || autoEdit;
const loadYaml = async () => {
if (initialYaml) {
setRawYaml(initialYaml);
setEditYaml(effectiveClean ? cleanYAML(initialYaml) : initialYaml);
setLoading(false);
return;
}
setLoading(true);
try {
const text = await kubernetesApi.getResourceYaml(clusterCode, namespace, resource, name);
setRawYaml(text);
setEditYaml(effectiveClean ? cleanYAML(text) : text);
} catch (e: any) {
toast({ title: '获取YAML失败', description: (e as Record<string, any>).message, variant: 'destructive' });
const err = `# Error: ${e.message}`;
setRawYaml(err);
setEditYaml(err);
} finally { setLoading(false); }
};
const enterEdit = () => {
const cleaned = effectiveClean ? cleanYAML(rawYaml) : rawYaml;
setEditYaml(cleaned);
setEditing(true);
if (effectiveClean) {
toast({ title: '已清理只读字段', description: '已剥离 status / managedFields / uid 等' });
}
};
// 视图模式:当 cleanBeforeEdit 时,只读视图也默认展示精简版(剥离 status/managedFields/uid/resourceVersion)
// 用户可点击「原始」按钮切换回未处理的完整服务端输出
const [showRaw, setShowRaw] = useState(false);
const saveYaml = async () => {
setSaving(true);
try {
await kubernetesApi.updateResourceYaml(clusterCode, namespace, resource, name, editYaml);
toast({ title: 'YAML已应用', description: `${resource}/${name} 更新成功` });
setEditing(false);
setRawYaml(editYaml);
onSaved?.();
} catch (e: any) {
toast({ title: '保存失败', description: (e as Record<string, any>).message, variant: 'destructive' });
} finally { setSaving(false); }
};
const copyYaml = async () => {
const content = editing ? editYaml : rawYaml;
await navigator.clipboard.writeText(content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
useEffect(() => { loadYaml(); }, []);
// 编辑态用 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 页面
if (inline) {
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<p className="text-xs text-slate-500">
<code className="text-slate-400">{resource}/{name}</code> · {namespace} · {clusterCode}
</p>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" className="h-7 border-slate-700 text-slate-300 text-xs" onClick={copyYaml}>
{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-7 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-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 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
value={display}
height="600px"
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: '12px' }}
/>
)}
</div>
</div>
);
}
// 弹窗模式(原有行为)
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}>
<div className="bg-slate-900 border border-slate-700 rounded-xl w-[90vw] max-w-4xl max-h-[85vh] flex flex-col shadow-2xl" onClick={e => e.stopPropagation()}>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-slate-700">
<div>
<h2 className="text-lg font-bold text-slate-100">{resource}/{name}</h2>
<p className="text-xs text-slate-500">{namespace} · {clusterCode}</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" className="h-8 border-slate-700 text-slate-300" onClick={copyYaml}>
{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>
</div>
</div>
{/* Body */}
<div className="flex-1 overflow-auto 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="600px"
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: '12px' }}
/>
)}
</div>
</div>
</div>
);
}