feat(k8s): M6 编辑器基础设施 + 基本信息 Tab + YAML编辑器

- DeploymentWorkload: 三模式切换(查看/编辑/YAML编辑)+ EditModeContainer Tab 导航系统
- DeploymentEditBasic: 新增基本信息 Tab — replicas/labels/annotations/selector 编辑
- YamlViewer: 升级为 CodeMirror 6(语法高亮+行号+折叠+括号匹配),新增 inline 内联模式
- DeploymentDetailPage: mode 类型扩展为 view|edit|yaml
- 路由 :mode 参数自动支持 yaml 值
- M7-M9 Tab 预留占位(容器信息/存储/高级/服务)
This commit is contained in:
attacker committed 2026-06-23 05:15:42 +08:00
1 parent 2e66908e20
commit 68a954c1c5
9 files changed
+855 -42

No files matched your search

+115 -14
View File
@@ -1,9 +1,16 @@
import { useState, useEffect } from 'react';
// YamlViewer.tsx — YAML 查看/编辑器(CodeMirror 6 语法高亮)
// 支持:语法高亮 + 行号 + 基本校验 + 编辑/保存 + 只读字段清理
// 作者: Logan <admin@attacker.club>
import { useState, useEffect, useMemo, useCallback } from 'react';
import { X, Copy, Check, Save, RefreshCw, 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;
@@ -16,13 +23,17 @@ interface YamlViewerProps {
cleanBeforeEdit?: boolean;
/** 打开即自动进入编辑模式(编辑按钮专用入口) */
autoEdit?: boolean;
/** 内联模式(非弹窗)— 用于 YAML 编辑模式 Tab */
inline?: boolean;
/** 内联模式下 YAML 内容直接传入 */
initialYaml?: string;
}
export function YamlViewer({ clusterCode, namespace, resource, name, onClose, onSaved, cleanBeforeEdit = false, autoEdit = false }: YamlViewerProps) {
export function YamlViewer({ clusterCode, namespace, resource, name, onClose, onSaved, cleanBeforeEdit = false, autoEdit = false, inline = false, initialYaml }: YamlViewerProps) {
const { toast } = useToast();
const [rawYaml, setRawYaml] = useState(''); // 原始(只读展示用)
const [rawYaml, setRawYaml] = useState(initialYaml || ''); // 原始(只读展示用)
const [editYaml, setEditYaml] = useState(''); // 编辑态(可能被清理过)
const [loading, setLoading] = useState(true);
const [loading, setLoading] = useState(!initialYaml);
const [saving, setSaving] = useState(false);
const [editing, setEditing] = useState(autoEdit); // autoEdit → 初始即编辑态
const [copied, setCopied] = useState(false);
@@ -30,6 +41,12 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on
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);
@@ -44,7 +61,6 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on
};
const enterEdit = () => {
// 进入编辑前可选清理
const cleaned = effectiveClean ? cleanYAML(rawYaml) : rawYaml;
setEditYaml(cleaned);
setEditing(true);
@@ -77,9 +93,84 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on
const display = editing ? editYaml : 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>
{!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-[80vh] flex flex-col shadow-2xl" onClick={e => e.stopPropagation()}>
<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>
@@ -122,15 +213,25 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on
<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>
) : editing ? (
<textarea
value={editYaml}
onChange={e => setEditYaml(e.target.value)}
className="w-full h-full min-h-[500px] bg-slate-950 text-emerald-400 font-mono text-xs p-4 rounded border border-slate-700 focus:outline-none focus:border-blue-500 resize-none"
spellCheck={false}
/>
) : (
<pre className="text-emerald-400 font-mono text-xs whitespace-pre overflow-x-auto">{display}</pre>
<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>