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:
1 parent
2e66908e20
commit
68a954c1c5
9 files changed
+855
-42
No files matched your search
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user