import { useRef, useEffect, useState } 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 { Maximize2, Minimize2, X, FolderOpen } from 'lucide-react'; import { cn } from '@/lib/utils'; import { attachTerminalResize, mountXterm } from '@/lib/xterm-utils'; import { useUserStore } from '@/store'; import { pipelineApi } from '@/api'; import { useTranslation } from 'react-i18next'; import { TerminalLoadingOverlay } from '@/components/common/TerminalLoadingOverlay'; import type { JsonData } from '@/types/common'; interface WorkspaceTermDialogProps { open: boolean; onOpenChange: (open: boolean) => void; runId: number | null; buildNumber: number; } /** * Workspace 终端 Dialog — 进入流水线构建目录查看文件 */ export function WorkspaceTermDialog({ open, onOpenChange, runId, buildNumber, }: WorkspaceTermDialogProps) { const { t } = useTranslation(); 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 [workspace, setWorkspace] = useState(''); const [hasOutput, setHasOutput] = useState(false); // 获取 workspace 路径 useEffect(() => { if (!open || !runId) return; setHasOutput(false); pipelineApi.getWorkspace(runId) .then((data: JsonData) => setWorkspace(data?.workspace_path || '')) .catch(() => {}); }, [open, runId]); // 连接终端 useEffect(() => { if (!open || !runId) return; const term = new XTerm({ cursorBlink: true, fontSize: 13, fontFamily: 'Menlo, Monaco, "Courier New", monospace', allowProposedApi: true, theme: { background: '#0a0e14', foreground: '#d6d6d6', cursor: '#d6d6d6', selectionBackground: 'rgba(255,255,255,0.2)', }, }); const fit = new FitAddon(); term.loadAddon(fit); termRef.current = term; fitRef.current = fit; const proto = location.protocol === 'https:' ? 'wss' : 'ws'; const token = useUserStore.getState().token || ''; const wsUrl = `${proto}://${location.host}/ws/pipeline/${runId}/workspace-shell?token=${encodeURIComponent(token)}`; 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 { setHasOutput(true); term.write(typeof event.data === 'string' ? event.data : ''); } }; ws.onopen = () => { setConnected(true); mountXterm(term, fit, () => containerRef.current, ws); }; ws.onclose = () => { setConnected(false); setHasOutput(true); term.write(`\r\n\x1b[31m${t('deploy.terminal.connClosed')}\x1b[0m\r\n`); }; ws.onerror = () => { setHasOutput(true); term.write(`\r\n\x1b[31m${t('deploy.terminal.connError')}\x1b[0m\r\n`); }; return () => { try { ws.close(); term.dispose(); } catch { /* noop */ } termRef.current = null; wsRef.current = null; setConnected(false); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, runId]); // 容器尺寸变化时 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()} 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' : '' )} style={fullscreen ? { maxWidth: '100vw', width: '100vw', height: '100vh' } : { maxWidth: '900px', width: '90vw' }} > {t("deploy.workspaceTerminalTitle", { num: buildNumber })} {t("common.buildTerminal")}
#{buildNumber} 构建目录 {workspace && ( {workspace} )}
); }