Files
aiops/frontend/src/components/kubernetes/ResourceVisualEditor.tsx
T
attacker 892cb85676 refactor(quality): Q6 前端巨型组件拆分 — ResourceVisualEditor 882L→282L (-68%), 提取 5 个独立资源编辑器文件
将 ResourceVisualEditor.tsx (882L, 项目最大前端组件) 中的 5 个内联资源编辑器
提取为 resource-visual-editors/ 子目录下的独立文件:
- ConfigMapEditor.tsx (95L) — ConfigMap 键值对表格
- SecretEditor.tsx (114L) — Secret Base64 编解码
- ServiceEditor.tsx (155L) — Service 端口/类型/选择器
- IngressEditor.tsx (215L) — Ingress Host/Path 路由规则
- PvcEditor.tsx (90L) — PVC 存储配置

主文件仅保留加载/保存逻辑 + YAML/可视化切换 + 资源类型分发。
清理 11 个 unused import。零逻辑变更,纯机械提取。

验证: tsc -b=0, npm build✓ (16.11s), ESLint errors=0, 17/17 测试通过
2026-07-06 20:20:59 +08:00

283 lines
14 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
// ResourceVisualEditor — ConfigMap/Secret/Service/Ingress/PVC 可视化编辑 // Logan.Li
import { useState, useEffect, useCallback } from 'react';
import type { ComponentType } from 'react';
import { X, Loader2, FileText, Network, Shield, Database, ChevronRight} 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 { createEmptyManifest } from './resource-create-templates';
import { parseManifest, parseEditableManifest, prepareManifestForSave, prepareYamlForSave } from './k8s-manifest-utils';
import { YamlEditorToolbar } from './YamlEditorToolbar';
import { YamlCodeMirror } from './YamlCodeMirror';
import { getYamlResourceConfig, yamlDownloadFilename } from './k8s-yaml-resource-config';
import { downloadYamlFile } from '@/utils/export';
import i18n from '@/i18n';
import { ConfigMapEditor } from './resource-visual-editors/ConfigMapEditor';
import { SecretEditor } from './resource-visual-editors/SecretEditor';
import { ServiceEditor } from './resource-visual-editors/ServiceEditor';
import { IngressEditor } from './resource-visual-editors/IngressEditor';
import { PvcEditor } from './resource-visual-editors/PvcEditor';
interface ResourceVisualEditorProps {
clusterCode: string;
namespace: string;
resource: string; // 'configmaps' | 'secrets' | 'services' | 'ingresses' | 'pvcs'
name: string;
onClose: () => void;
onSaved?: () => void;
/** 新建模式:不拉取远端 YAML,使用空模板 // Logan.Li */
isCreate?: boolean;
}
export function ResourceVisualEditor({ clusterCode, namespace, resource, name, onClose, onSaved, isCreate = false }: ResourceVisualEditorProps) {
const { toast } = useToast();
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [rawYaml, setRawYaml] = useState('');
const [manifest, setManifest] = useState<any>(null);
const [mode, setMode] = useState<'visual' | 'yaml'>('visual');
const [displayYaml, setDisplayYaml] = useState('');
const [editYaml, setEditYaml] = useState('');
const [yamlEditing, setYamlEditing] = useState(false);
const [showRawYaml, setShowRawYaml] = useState(false);
const [yamlCopied, setYamlCopied] = useState(false);
const loadData = useCallback(async () => {
setLoading(true);
try {
if (isCreate) {
const empty = createEmptyManifest(resource, namespace, name || '');
const yamlStr = await prepareManifestForSave(empty);
setManifest(empty);
setDisplayYaml(yamlStr);
setEditYaml(yamlStr);
setRawYaml('');
setYamlEditing(true);
return;
}
const text = await kubernetesApi.getResourceYaml(clusterCode, namespace, resource, name);
setRawYaml(text);
// 先解析原始 YAML,再用对象级剥离;cleanYAML 会破坏 managedFields 导致解析失败 // Logan.Li
const { manifest: parsed, yaml: editableYaml } = await parseEditableManifest(text);
setDisplayYaml(editableYaml);
setEditYaml(editableYaml);
setYamlEditing(false);
setShowRawYaml(false);
setManifest(parsed);
if (!parsed) {
setMode('yaml');
toast({ title: i18n.t('kubernetes.resourcevisuale.s80ca4f8e'), description: i18n.t('kubernetes.resourcevisuale.scc94aa41') });
}
} catch (e) {
toast({ title: i18n.t('kubernetes.msg.loadFailed'), description: e instanceof Error ? e.message : String(e), variant: 'destructive' });
} finally { setLoading(false); }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [clusterCode, namespace, resource, name, isCreate]);
useEffect(() => { loadData(); }, [loadData]);
const handleSave = async (updatedManifest: any) => {
setSaving(true);
try {
const yamlStr = await prepareManifestForSave(updatedManifest);
const resName = updatedManifest?.metadata?.name || name;
if (isCreate) {
if (!resName?.trim()) {
toast({ title: i18n.t('deploy.form.nameRequired'), variant: 'destructive' });
return;
}
await kubernetesApi.applyYAML(clusterCode, yamlStr);
toast({ title: i18n.t('monitor.dashboard.custom.toastCreateSuccess'), description: `${resource}/${resName}` });
} else {
await kubernetesApi.updateResourceYaml(clusterCode, namespace, resource, name, yamlStr);
toast({ title: i18n.t('kubernetes.msg.saveSuccess'), description: `${resource}/${name} 已更新` });
}
onSaved?.();
onClose();
} catch (e) {
toast({ title: isCreate ? i18n.t('monitor.dashboard.custom.toastCreateFailed') : i18n.t('kubernetes.msg.saveFailed'), description: e instanceof Error ? e.message : String(e), variant: 'destructive' });
} finally { setSaving(false); }
};
const handleSaveYaml = async () => {
setSaving(true);
try {
const yamlStr = await prepareYamlForSave(editYaml);
if (isCreate) {
await kubernetesApi.applyYAML(clusterCode, yamlStr);
toast({ title: i18n.t('monitor.dashboard.custom.toastCreateSuccess'), description: i18n.t('kubernetes.resourcevisuale.se5ff4ba7') });
} else {
await kubernetesApi.updateResourceYaml(clusterCode, namespace, resource, name, yamlStr);
toast({ title: i18n.t('kubernetes.msg.saveSuccess'), description: `${resource}/${name} 已更新` });
}
setYamlEditing(false);
setDisplayYaml(yamlStr);
setEditYaml(yamlStr);
if (!isCreate) setRawYaml(yamlStr);
onSaved?.();
onClose();
} catch (e) {
toast({ title: i18n.t('kubernetes.msg.saveFailed'), description: e instanceof Error ? e.message : String(e), variant: 'destructive' });
} finally { setSaving(false); }
};
// 可视化 ↔ YAML 双向同步 // Logan.Li
const switchToVisual = async () => {
const parsed = await parseManifest(editYaml);
if (!parsed) {
toast({ title: i18n.t('kubernetes.resourcevisuale.sf53bdf65'), description: i18n.t('kubernetes.resourcevisuale.sf2219a47'), variant: 'destructive' });
return;
}
setManifest(parsed);
setMode('visual');
};
const switchToYaml = async () => {
if (manifest) {
const yamlStr = await prepareManifestForSave(manifest);
setDisplayYaml(yamlStr);
setEditYaml(yamlStr);
}
setYamlEditing(false);
setShowRawYaml(false);
setMode('yaml');
};
const currentYamlText = () => (yamlEditing ? editYaml : (showRawYaml ? rawYaml : displayYaml));
const copyYamlContent = async () => {
await navigator.clipboard.writeText(currentYamlText());
setYamlCopied(true);
setTimeout(() => setYamlCopied(false), 2000);
};
const downloadYamlContent = () => {
const fname = yamlDownloadFilename(resource, namespace, name || manifest?.metadata?.name || 'resource');
downloadYamlFile(fname, currentYamlText());
toast({ title: i18n.t('kubernetes.yamlviewertsx.s9719fec8'), description: fname });
};
const resCfg = getYamlResourceConfig(resource);
const resourceMeta: Record<string, { title: string; icon: ComponentType<{ className?: string }>; color: string }> = {
configmaps: { title: 'ConfigMap', icon: FileText, color: 'dg-text-blue' },
secrets: { title: 'Secret', icon: Shield, color: 'dg-text-yellow' },
services: { title: 'Service', icon: Network, color: 'dg-text-cyan' },
ingresses: { title: 'Ingress', icon: ChevronRight, color: 'dg-text-purple' },
pvcs: { title: 'PersistentVolumeClaim', icon: Database, color: 'dg-text-green' },
};
const meta = resourceMeta[resource] || resourceMeta.configmaps;
const MetaIcon = meta.icon;
return (
<ModalOverlay onClose={onClose}>
<div
className="bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-xl w-[90vw] max-w-3xl h-[85vh] flex flex-col shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
{/* Header:长名称/域名截断,避免挤压模式切换按钮 // Logan.Li */}
<div className="flex items-center justify-between p-4 border-b border-[var(--dg-border)] gap-3 shrink-0">
<div className="flex items-center gap-2 min-w-0 flex-1">
<MetaIcon className={`h-5 w-5 shrink-0 ${meta.color}`} />
<div className="min-w-0">
<h2 className="text-lg font-bold text-[var(--dg-text-primary)] truncate">
{isCreate ? `新建 ${meta.title}` : `${meta.title} 编辑`}
</h2>
<p
className="text-xs text-[var(--dg-text-muted)] truncate"
title={isCreate ? `${namespace} · ${clusterCode}` : `${name} · ${namespace} · ${clusterCode}`}
>
{isCreate ? `${namespace} · ${clusterCode}` : `${name} · ${namespace} · ${clusterCode}`}
</p>
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
{/* Mode switcher */}
<div className="flex rounded-lg border border-[var(--dg-border)] overflow-hidden">
<button
className={`px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'visual' ? 'bg-blue-600 text-white' : 'bg-transparent text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-tertiary)]'}`}
onClick={() => { if (mode === 'yaml') switchToVisual(); else setMode('visual'); }}
disabled={!manifest && mode !== 'yaml'}
> {i18n.t('k8s.resource.visualEditor')} </button>
<button
className={`px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'yaml' ? 'bg-blue-600 text-white' : 'bg-transparent text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-tertiary)]'}`}
onClick={() => { if (mode === 'visual') switchToYaml(); else setMode('yaml'); }}
>YAML 编辑</button>
</div>
<Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)]" onClick={onClose}><X className="h-4 w-4" /></Button>
</div>
</div>
{/* Body */}
<div className={`flex-1 min-h-0 p-4 ${mode === 'yaml' ? 'flex flex-col overflow-hidden' : 'flex flex-col overflow-hidden'}`}>
{loading ? (
<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' ? (
<div className="flex flex-col flex-1 min-h-0 gap-3">
<div className="flex justify-end shrink-0">
<YamlEditorToolbar
editing={yamlEditing}
saving={saving}
copied={yamlCopied}
showRaw={showRawYaml}
compact
readOnly={resCfg.readOnly}
showRawToggle={!isCreate && !!rawYaml}
onCopy={copyYamlContent}
onDownload={downloadYamlContent}
onEdit={() => { setEditYaml(displayYaml); setYamlEditing(true); setShowRawYaml(false); }}
onCancel={async () => {
setYamlEditing(false);
if (isCreate) {
const empty = createEmptyManifest(resource, namespace, name || '');
const yamlStr = await prepareManifestForSave(empty);
setDisplayYaml(yamlStr);
setEditYaml(yamlStr);
return;
}
const { yaml: stripped } = await parseEditableManifest(rawYaml);
setDisplayYaml(stripped);
setEditYaml(stripped);
}}
onSave={handleSaveYaml}
onToggleRaw={() => setShowRawYaml((v) => !v)}
/>
</div>
<YamlCodeMirror
value={yamlEditing ? editYaml : (showRawYaml ? rawYaml : displayYaml)}
editing={yamlEditing}
fill
fontSize="12px"
onChange={(val) => yamlEditing && setEditYaml(val)}
/>
</div>
) : manifest ? (
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
{resource === 'configmaps' && <ConfigMapEditor manifest={manifest} onSave={handleSave} saving={saving} isCreate={isCreate} />}
{resource === 'secrets' && <SecretEditor manifest={manifest} onSave={handleSave} saving={saving} isCreate={isCreate} />}
{resource === 'services' && <ServiceEditor manifest={manifest} onSave={handleSave} saving={saving} isCreate={isCreate} />}
{resource === 'ingresses' && <IngressEditor manifest={manifest} clusterCode={clusterCode} onSave={handleSave} saving={saving} isCreate={isCreate} />}
{resource === 'pvcs' && <PvcEditor manifest={manifest} onSave={handleSave} saving={saving} isCreate={isCreate} />}
{!['configmaps', 'secrets', 'services', 'ingresses', 'pvcs'].includes(resource) && (
<div className="flex-1 flex items-center justify-center text-center text-[var(--dg-text-muted)]">
<div>
<p> {i18n.t('kubernetes.resourcevisuale.sfeb34f55')} </p>
<Button variant="outline" size="sm" className="mt-4" onClick={() => setMode('yaml')}> {i18n.t('kubernetes.resourcevisuale.s54c78d4f')} </Button>
</div>
</div>
)}
</div>
) : (
<div className="text-center py-12 text-[var(--dg-text-muted)]">
<p> {i18n.t('kubernetes.resourcevisuale.s8fe0aeb0')} </p>
<Button variant="outline" size="sm" className="mt-4" onClick={() => setMode('yaml')}> {i18n.t('kubernetes.resourcevisuale.s54c78d4f')} </Button>
</div>
)}
</div>
</div>
</ModalOverlay>
);
}