feat: web terminal

This commit is contained in:
2025-01-16 06:10:37 -05:00
parent 8d5a57614e
commit d7dcabc3da
10 changed files with 280 additions and 5 deletions

View File

@ -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",

View File

@ -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"

View File

@ -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<HTMLDivElement>(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 <div ref={terminalRef} style={{ height: "auto" }} />;
}

View File

@ -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: <ContainerPage />,
loader: ContainerPageLoader,
loader: ContainerDataLoader,
},
{
path: "/containers/:name/terminal",
element: <TerminalPage />,
loader: ContainerDataLoader,
},
],
},

View File

@ -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}
</Typography>
<ButtonGroup>
<Button href={`/containers/${container.name}/terminal`}>
Terminal
</Button>
<Button
loading={isPowerStateLocked}
onClick={async () => {

View File

@ -0,0 +1,34 @@
import { useLoaderData } from "react-router";
import { Container } from "../../../types";
import { Paper, Typography } from "@mui/material";
import WebTerminal from "../../../components/WebTerminal";
interface Params {
name: string;
}
export async function Loader({ params }: { params: Params }) {
const container = await fetch(`/api/containers/${params.name}`);
const data = await container.json();
return data;
}
export default function TerminalPage() {
const container = useLoaderData() as Container & { statusCode: number };
if (container.statusCode === 404) {
return "Container not found";
}
return (
<Paper square sx={{ padding: 1 }}>
<Paper sx={{ display: "flex" }} variant="outlined">
<Typography variant="h6" sx={{ flexGrow: 1 }}>
Terminal: {container.name}
</Typography>
</Paper>
<WebTerminal />
</Paper>
);
}