diff --git a/src/components/fm.tsx b/src/components/fm.tsx index 7ee5eff..9530bae 100644 --- a/src/components/fm.tsx +++ b/src/components/fm.tsx @@ -67,9 +67,7 @@ const FMComponent: React.FC = ({ wsUrl, useEffect(() => { return () => { - if (wsRef.current) { - wsRef.current.close(); - } + wsRef.current?.close(); }; }, []); diff --git a/src/components/terminal.tsx b/src/components/terminal.tsx index e8a52d6..5be5a73 100644 --- a/src/components/terminal.tsx +++ b/src/components/terminal.tsx @@ -10,16 +10,15 @@ import { import { Terminal } from "@xterm/xterm"; import { AttachAddon } from "@xterm/addon-attach"; import { FitAddon } from '@xterm/addon-fit'; -import { useRef, useEffect, useState } from "react"; +import { useRef, useEffect, useState, useMemo } from "react"; import { sleep } from "@/lib/utils"; -import { IconButton } from "./xui/icon-button"; import "@xterm/xterm/css/xterm.css"; -import { createTerminal } from "@/api/terminal"; -import { ModelCreateTerminalResponse } from "@/types"; import { useParams } from 'react-router-dom'; import { Button } from "./ui/button"; import { toast } from "sonner"; import { FMCard } from "./fm"; +import useTerminal from "@/hooks/useTerminal"; +import { IconButton } from "./xui/icon-button"; interface XtermProps { wsUrl: string; @@ -27,18 +26,22 @@ interface XtermProps { } const XtermComponent: React.FC = ({ wsUrl, setClose, ...props }) => { - const terminalRef = useRef(null); + const terminalIdRef = useRef(null); + const terminalRef = useRef(null); const wsRef = useRef(null); useEffect(() => { return () => { - if (wsRef.current) { - wsRef.current.close(); - } + wsRef.current?.close(); + terminalRef.current?.dispose(); }; }, []); useEffect(() => { + terminalRef.current = new Terminal({ + cursorBlink: true, + fontSize: 16, + }); const ws = new WebSocket(wsUrl); wsRef.current = ws; ws.binaryType = "arraybuffer"; @@ -46,7 +49,7 @@ const XtermComponent: React.FC = ({ w onResize(); } ws.onclose = () => { - terminal.dispose(); + terminalRef.current?.dispose(); setClose(true); } ws.onerror = (e) => { @@ -57,18 +60,12 @@ const XtermComponent: React.FC = ({ w } }, [wsUrl]); - const terminal = useRef( - new Terminal({ - cursorBlink: true, - fontSize: 16, - }) - ).current; const fitAddon = useRef(new FitAddon()).current; const sendResize = useRef(false); const doResize = () => { - if (!terminalRef.current) return; + if (!terminalIdRef.current) return; fitAddon.fit(); @@ -104,11 +101,11 @@ const XtermComponent: React.FC = ({ w }; useEffect(() => { - if (!wsRef.current || !terminalRef.current) return; + if (!wsRef.current || !terminalIdRef.current || !terminalRef.current) return; const attachAddon = new AttachAddon(wsRef.current); - terminal.loadAddon(attachAddon); - terminal.loadAddon(fitAddon); - terminal.open(terminalRef.current); + terminalRef.current.loadAddon(attachAddon); + terminalRef.current.loadAddon(fitAddon); + terminalRef.current.open(terminalIdRef.current); window.addEventListener('resize', onResize); return () => { window.removeEventListener('resize', onResize); @@ -116,36 +113,15 @@ const XtermComponent: React.FC = ({ w wsRef.current.close(); } }; - }, [wsRef.current, terminal]); + }, [wsRef.current, terminalRef.current, terminalIdRef.current]); - return
; + return
; }; export const TerminalPage = () => { - const [terminal, setTerminal] = useState(null); - const [open, setOpen] = useState(false); - const { id } = useParams<{ id: string }>(); - - const fetchTerminal = async () => { - if (id && !terminal) { - try { - const createdTerminal = await createTerminal(Number(id)); - setTerminal(createdTerminal); - } catch (e) { - toast("Terminal API Error", { - description: "View console for details.", - }) - console.error("fetch error", e); - return; - } - } - } - - useEffect(() => { - fetchTerminal(); - }, [id]); - + const [open, setOpen] = useState(false); + const terminal = useTerminal(id ? parseInt(id) : undefined); return (
@@ -158,7 +134,7 @@ export const TerminalPage = () => {
{terminal?.session_id ? - + :

The server does not exist, or have not been connected yet.

} diff --git a/src/hooks/useTerminal.ts b/src/hooks/useTerminal.ts new file mode 100644 index 0000000..8616ee3 --- /dev/null +++ b/src/hooks/useTerminal.ts @@ -0,0 +1,23 @@ +import { createTerminal } from "@/api/terminal"; +import { ModelCreateTerminalResponse } from "@/types"; +import { useState, useEffect } from "react"; + +export default function useTerminal(serverId?: number) { + const [terminal, setTerminal] = useState(null); + + async function fetchTerminal() { + try { + const response = await createTerminal(serverId!); + setTerminal(response); + } catch (error) { + console.error("Failed to fetch terminal:", error); + } + } + + useEffect(() => { + if (!serverId) return; + fetchTerminal(); + }, [serverId]); + + return terminal; +} diff --git a/src/main.tsx b/src/main.tsx index 644c6dd..9bcf65c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -119,7 +119,5 @@ const router = createBrowserRouter([ ]); createRoot(document.getElementById('root')!).render( - - - , + ) diff --git a/vite.config.ts b/vite.config.ts index f46f9ba..37e8bb3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,15 +7,15 @@ export default defineConfig({ plugins: [react()], server: { proxy: { + '^/api/v1/ws/.*': { + target: "ws://localhost:8008", + changeOrigin: true, + ws: true, + }, '/api': { target: 'http://localhost:8008', changeOrigin: true, }, - '/api/v1/ws': { - target: 'http://localhost:8008', - changeOrigin: true, - ws: true, - }, }, }, resolve: {