mirror of
https://github.com/CyberL1/dlinux-dashboard.git
synced 2025-06-28 16:19:43 -04:00
feat: web terminal
This commit is contained in:
27
frontend/package-lock.json
generated
27
frontend/package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
|
62
frontend/src/components/WebTerminal.tsx
Normal file
62
frontend/src/components/WebTerminal.tsx
Normal 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" }} />;
|
||||
}
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -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 () => {
|
34
frontend/src/pages/containers/[name]/terminal.tsx
Normal file
34
frontend/src/pages/containers/[name]/terminal.tsx
Normal 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>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user