/* 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, Save, Loader2, Plus, Trash2, FileText, Network, Shield, Database, ChevronRight} from 'lucide-react'; import { Button } from '@/components/ui/button'; import { ModalOverlay } from '@/components/ui/modal-overlay'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; 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'; 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(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: '可视化编辑不可用', description: '无法解析资源结构,已切换到 YAML 编辑模式' }); } } catch (e) { toast({ title: '加载失败', description: e instanceof Error ? e.message : String(e), variant: 'destructive' }); } finally { setLoading(false); } }, [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: '请填写名称', variant: 'destructive' }); return; } await kubernetesApi.applyYAML(clusterCode, yamlStr); toast({ title: '创建成功', description: `${resource}/${resName}` }); } else { await kubernetesApi.updateResourceYaml(clusterCode, namespace, resource, name, yamlStr); toast({ title: '保存成功', description: `${resource}/${name} 已更新` }); } onSaved?.(); onClose(); } catch (e) { toast({ title: isCreate ? '创建失败' : '保存失败', 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: '创建成功', description: 'YAML 已应用' }); } else { await kubernetesApi.updateResourceYaml(clusterCode, namespace, resource, name, yamlStr); toast({ title: '保存成功', description: `${resource}/${name} 已更新` }); } setYamlEditing(false); setDisplayYaml(yamlStr); setEditYaml(yamlStr); if (!isCreate) setRawYaml(yamlStr); onSaved?.(); onClose(); } catch (e) { toast({ title: '保存失败', 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: '无法解析 YAML', description: '请检查语法后继续用 YAML 模式', 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: '已下载', description: fname }); }; const resCfg = getYamlResourceConfig(resource); const resourceMeta: Record; 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 (
e.stopPropagation()} > {/* Header:长名称/域名截断,避免挤压模式切换按钮 // Logan.Li */}

{isCreate ? `新建 ${meta.title}` : `${meta.title} 编辑`}

{isCreate ? `${namespace} · ${clusterCode}` : `${name} · ${namespace} · ${clusterCode}`}

{/* Mode switcher */}
{/* Body */}
{loading ? (
) : mode === 'yaml' ? (
{ 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)} />
yamlEditing && setEditYaml(val)} />
) : manifest ? (
{resource === 'configmaps' && } {resource === 'secrets' && } {resource === 'services' && } {resource === 'ingresses' && } {resource === 'pvcs' && } {!['configmaps', 'secrets', 'services', 'ingresses', 'pvcs'].includes(resource) && (

该资源类型暂不支持可视化编辑

)}
) : (

无法解析资源结构

)}
); } // ══════════════════════════════════════════════ // ConfigMap 编辑器 — 键值对表格 // ══════════════════════════════════════════════ function ConfigMapEditor({ manifest, onSave, saving, isCreate }: { manifest: any; onSave: (m: any) => void; saving: boolean; isCreate?: boolean }) { const data = manifest?.data || {}; const [resName, setResName] = useState(manifest?.metadata?.name || ''); const [entries, setEntries] = useState<{ key: string; value: string }[]>( Object.entries(data).map(([key, value]) => ({ key, value: String(value) })) ); const [expandAll, setExpandAll] = useState(false); const update = (i: number, field: 'key' | 'value', val: string) => { const next = [...entries]; next[i] = { ...next[i], [field]: val }; setEntries(next); }; const addRow = () => setEntries([...entries, { key: '', value: '' }]); const removeRow = (i: number) => setEntries(entries.filter((_, idx) => idx !== i)); const handleSave = () => { const newData: Record = {}; entries.forEach(e => { if (e.key.trim()) newData[e.key.trim()] = e.value; }); onSave({ ...manifest, metadata: { ...manifest.metadata, name: resName.trim() }, data: newData }); }; return (
setResName(e.target.value)} readOnly={!isCreate} className="mt-1 font-mono text-xs bg-[var(--dg-bg-tertiary)]" />
{entries.map((entry, i) => (
update(i, 'key', e.target.value)} placeholder="键名 (如 LOG_LEVEL)" className="flex-1 font-mono text-xs h-8 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />