From d7dcabc3daae1c10f110bbb1d1741a6450468714 Mon Sep 17 00:00:00 2001 From: CyberL1 Date: Thu, 16 Jan 2025 06:10:37 -0500 Subject: [PATCH] feat: web terminal --- frontend/package-lock.json | 27 ++++++ frontend/package.json | 3 + frontend/src/components/WebTerminal.tsx | 62 ++++++++++++++ frontend/src/main.tsx | 12 ++- .../{[name].tsx => [name]/index.tsx} | 7 +- .../src/pages/containers/[name]/terminal.tsx | 34 ++++++++ package-lock.json | 84 +++++++++++++++++++ package.json | 2 + src/index.ts | 2 + src/routes/containers/_id/terminal.ts | 52 ++++++++++++ 10 files changed, 280 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/WebTerminal.tsx rename frontend/src/pages/containers/{[name].tsx => [name]/index.tsx} (89%) create mode 100644 frontend/src/pages/containers/[name]/terminal.tsx create mode 100644 src/routes/containers/_id/terminal.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0c157cf..04f4183 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,9 @@ "@mui/icons-material": "^6.1.8", "@mui/lab": "^6.0.0-beta.23", "@mui/material": "^6.1.8", + "@xterm/addon-attach": "^0.11.0", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router": "^7.0.1" @@ -2316,6 +2319,30 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, + "node_modules/@xterm/addon-attach": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-attach/-/addon-attach-0.11.0.tgz", + "integrity": "sha512-JboCN0QAY6ZLY/SSB/Zl2cQ5zW1Eh4X3fH7BnuR1NB7xGRhzbqU2Npmpiw/3zFlxDaU88vtKzok44JKi2L2V2Q==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/addon-fit": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", + "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "license": "MIT" + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index a89cdac..30537ce 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,9 @@ "@mui/icons-material": "^6.1.8", "@mui/lab": "^6.0.0-beta.23", "@mui/material": "^6.1.8", + "@xterm/addon-attach": "^0.11.0", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router": "^7.0.1" diff --git a/frontend/src/components/WebTerminal.tsx b/frontend/src/components/WebTerminal.tsx new file mode 100644 index 0000000..2b59551 --- /dev/null +++ b/frontend/src/components/WebTerminal.tsx @@ -0,0 +1,62 @@ +import { useEffect, useRef } from "react"; +import { Terminal } from "@xterm/xterm"; +import { AttachAddon } from "@xterm/addon-attach"; +import { FitAddon } from "@xterm/addon-fit"; +import "@xterm/xterm/css/xterm.css"; +import { useLoaderData } from "react-router"; +import { Container } from "../types"; + +export default function WebTerminal() { + const container = useLoaderData() as Container; + const terminalRef = useRef(null); + + useEffect(() => { + document.title = `Terminal: ${container.name}`; + + if (terminalRef.current) { + const terminal = new Terminal({ rows: 67 }); + + const socket = new WebSocket( + `ws://127.0.0.1:3000/containers/${container.id}/terminal`, + ); + + const fitAddon = new FitAddon(); + terminal.loadAddon(fitAddon); + + terminal.open(terminalRef.current); + fitAddon.fit(); + + terminal.write("Connecting to the container\r\n"); + + socket.onopen = () => { + const attachAddon = new AttachAddon(socket); + terminal.loadAddon(attachAddon); + + terminal.clear(); + terminal.focus(); + + socket.send( + JSON.stringify({ + rows: terminal.rows, + cols: terminal.cols, + }), + ); + }; + + window.addEventListener("resize", () => { + fitAddon.fit(); + }); + + terminal.onResize(({ rows, cols }) => { + socket.send(JSON.stringify({ rows, cols })); + }); + + return () => { + socket.close(); + terminal.dispose(); + }; + } + }, []); + + return
; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index dc1ef9f..6903389 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -7,8 +7,9 @@ import Containers, { Loader as ContainersLoader, } from "./pages/containers/index.tsx"; import ContainerPage, { - Loader as ContainerPageLoader, -} from "./pages/containers/[name].tsx"; + Loader as ContainerDataLoader, +} from "./pages/containers/[name]/index.tsx"; +import TerminalPage from "./pages/containers/[name]/terminal.tsx"; const router = createBrowserRouter([ { @@ -24,7 +25,12 @@ const router = createBrowserRouter([ { path: "/containers/:name", element: , - loader: ContainerPageLoader, + loader: ContainerDataLoader, + }, + { + path: "/containers/:name/terminal", + element: , + loader: ContainerDataLoader, }, ], }, diff --git a/frontend/src/pages/containers/[name].tsx b/frontend/src/pages/containers/[name]/index.tsx similarity index 89% rename from frontend/src/pages/containers/[name].tsx rename to frontend/src/pages/containers/[name]/index.tsx index a2ca66b..9bacede 100644 --- a/frontend/src/pages/containers/[name].tsx +++ b/frontend/src/pages/containers/[name]/index.tsx @@ -1,8 +1,8 @@ import { useLoaderData, useRevalidator } from "react-router"; -import { Container } from "../../types"; +import { Container } from "../../../types"; import { Button, ButtonGroup, Paper, Typography } from "@mui/material"; import { useState } from "react"; -import ContainerStats from "../../components/ContainerStats"; +import ContainerStats from "../../../components/ContainerStats"; interface Params { name: string; @@ -32,6 +32,9 @@ export default function ContainerPage() { Managing: {container.name} +