import { useState, useRef, useEffect } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogTitle, } from '@/components/ui/dialog'; import { Terminal as XTerm } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; import 'xterm/css/xterm.css'; import { Terminal, Maximize2, Minimize2, X, Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; import { attachTerminalResize, mountXterm } from '@/lib/xterm-utils'; import { useUserStore } from '@/store'; import { TerminalLoadingOverlay } from '@/components/common/TerminalLoadingOverlay'; import i18n from '@/i18n'; interface KubectlTerminalProps { open: boolean; onOpenChange: (open: boolean) => void; clusterId: number | string; clusterName: string; } export function KubectlTerminal({ open, onOpenChange, clusterId, clusterName }: KubectlTerminalProps) { const containerRef = useRef(null); const termRef = useRef(null); const wsRef = useRef(null); const fitRef = useRef(null); const [connected, setConnected] = useState(false); const [fullscreen, setFullscreen] = useState(false); const [error, setError] = useState(''); const [hasOutput, setHasOutput] = useState(false); // 重置全屏状态 useEffect(() => { if (!open) setFullscreen(false); }, [open]); // 连接终端 useEffect(() => { if (!open || !clusterId) return; setHasOutput(false); const term = new XTerm({ cursorBlink: true, fontSize: 14, fontFamily: 'Menlo, Monaco, "Courier New", monospace', allowProposedApi: true, theme: { background: '#0f172a', foreground: '#e2e8f0', cursor: '#38bdf8', selectionBackground: '#1e40af', black: '#1e293b', red: '#ef4444', green: '#10b981', yellow: '#f59e0b', blue: '#3b82f6', magenta: '#a855f7', cyan: '#06b6d4', white: '#e2e8f0', brightBlack: '#475569', brightRed: '#f87171', brightGreen: '#34d399', brightYellow: '#fbbf24', brightBlue: '#60a5fa', brightMagenta: '#c084fc', brightCyan: '#22d3ee', brightWhite: '#f8fafc', }, }); const fit = new FitAddon(); term.loadAddon(fit); termRef.current = term; fitRef.current = fit; // WebSocket const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const baseUrl = import.meta.env.VITE_API_BASE_URL || `${proto}//${window.location.host}`; const token = useUserStore.getState().token || ''; const wsUrl = `${baseUrl.replace(/^http/, 'ws').replace('/api/v1', '')}/ws/kubectl?token=${encodeURIComponent(token)}&id=${clusterId}`; const ws = new WebSocket(wsUrl); wsRef.current = ws; term.onData((data) => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ status: 0, data })); } }); ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); if (msg.status === 0 && typeof msg.data === 'string') { setHasOutput(true); term.write(msg.data); } } catch { if (typeof event.data === 'string') { setHasOutput(true); term.write(event.data); } } }; ws.onopen = () => { setConnected(true); setError(''); mountXterm(term, fit, () => containerRef.current, ws); }; ws.onclose = () => { setConnected(false); setHasOutput(true); term.write(i18n.t('k8s.kubectl.disconnected')); }; ws.onerror = () => { setError(i18n.t('k8s.kubectl.failed')); setHasOutput(true); term.write(i18n.t('k8s.kubectl.connectError')); }; return () => { try { if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) { ws.close(); } term.dispose(); } catch { /* noop */ } termRef.current = null; wsRef.current = null; setConnected(false); }; }, [open, clusterId]); // ResizeObserver 监听容器变化自动 fit,并同步远端 PTY 尺寸。 // Logan.Li useEffect(() => { if (!open) return; let cleanup: (() => void) | undefined; let retry = 0; const attach = () => { const container = containerRef.current; const fit = fitRef.current; const term = termRef.current; if (!container || !fit || !term) { if (retry++ < 30) requestAnimationFrame(attach); return; } cleanup = attachTerminalResize(container, fit, term, wsRef.current); }; attach(); return () => cleanup?.(); }, [open, fullscreen]); return ( e.preventDefault()} // 禁用 Radix Dialog 默认的 ESC 关闭:kubectl 终端会用到 ESC,关闭只能点头部 X 按钮 // Logan.Li onEscapeKeyDown={(e) => e.preventDefault()} className={cn( 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] p-0 gap-0', '!duration-0 !transition-none', fullscreen ? '!max-w-none !w-screen !h-screen !rounded-none !translate-x-0 !translate-y-0 !left-0 !top-0' : '' )} style={fullscreen ? { maxWidth: '100vw', width: '100vw', height: '100vh' } : { maxWidth: '900px', width: '90vw' }} > kubectl — {clusterName} Kubernetes 集群命令行终端 {/* 头部 */}
kubectl — {clusterName} {!connected && !error && }
{/* 终端容器 */}
); }