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

@ -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>
);
}