mirror of
https://github.com/CyberL1/dlinux-dashboard.git
synced 2025-01-22 01:19:18 -05:00
feat: web terminal
This commit is contained in:
parent
8d5a57614e
commit
d7dcabc3da
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>
|
||||
);
|
||||
}
|
84
package-lock.json
generated
84
package-lock.json
generated
@ -9,6 +9,8 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@fastify/websocket": "^11.0.2",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"dockerode": "^4.0.3",
|
||||
"dotenv": "^16.4.7",
|
||||
"fastify": "^5.2.1"
|
||||
@ -258,6 +260,27 @@
|
||||
"ipaddr.js": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/websocket": {
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/websocket/-/websocket-11.0.2.tgz",
|
||||
"integrity": "sha512-1oyJkNSZNJGjo/A5fXvlpEcm1kTBD91nRAN9lA7RNVsVNsyC5DuhOXdNL9/4UawVe7SKvzPT/QVI4RdtE9ylnA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"duplexify": "^4.1.3",
|
||||
"fastify-plugin": "^5.0.0",
|
||||
"ws": "^8.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.5.tgz",
|
||||
@ -372,6 +395,22 @@
|
||||
"undici-types": "~6.20.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",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/abstract-logging": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
|
||||
@ -659,6 +698,18 @@
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexify": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
|
||||
"integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.4.1",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1",
|
||||
"stream-shift": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
@ -783,6 +834,12 @@
|
||||
"toad-cache": "^3.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fastify-plugin": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.0.1.tgz",
|
||||
"integrity": "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
|
||||
@ -1228,6 +1285,12 @@
|
||||
"nan": "^2.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stream-shift": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
|
||||
"integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
@ -1363,6 +1426,27 @@
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
@ -15,6 +15,8 @@
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@fastify/websocket": "^11.0.2",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"dockerode": "^4.0.3",
|
||||
"dotenv": "^16.4.7",
|
||||
"fastify": "^5.2.1"
|
||||
|
@ -1,8 +1,10 @@
|
||||
import fastifyWebsocket from "@fastify/websocket";
|
||||
import "dotenv/config";
|
||||
import fastify from "fastify";
|
||||
import { readdirSync } from "fs";
|
||||
|
||||
const app = fastify();
|
||||
app.register(fastifyWebsocket);
|
||||
|
||||
const routes = readdirSync(`${import.meta.dirname}/routes`, {
|
||||
recursive: true,
|
||||
|
52
src/routes/containers/_id/terminal.ts
Normal file
52
src/routes/containers/_id/terminal.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import type { Container } from "#src/types/Container.ts";
|
||||
import { getContainer } from "#src/utils/containers.ts";
|
||||
import type { FastifyInstance, FastifyRequest } from "fastify";
|
||||
import { PassThrough } from "stream";
|
||||
|
||||
export default (fastify: FastifyInstance) => {
|
||||
fastify.get(
|
||||
"/",
|
||||
{ websocket: true },
|
||||
async (connection, req: FastifyRequest<{ Params: Container }>) => {
|
||||
const container = getContainer(req.params.id);
|
||||
|
||||
const exec = await container.exec({
|
||||
Cmd: ["/bin/bash"],
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
});
|
||||
|
||||
const stream = await exec.start({ hijack: true, stdin: true });
|
||||
|
||||
const stdout = new PassThrough();
|
||||
const stderr = new PassThrough();
|
||||
|
||||
container.modem.demuxStream(stream, stdout, stderr);
|
||||
|
||||
connection.on("message", (data: Buffer) => {
|
||||
console.log(new TextDecoder().decode(data));
|
||||
|
||||
if (new TextDecoder().decode(data).startsWith("{")) {
|
||||
const parsed = JSON.parse(data.toString());
|
||||
|
||||
exec.resize({
|
||||
h: parsed.rows,
|
||||
w: parsed.cols,
|
||||
});
|
||||
} else {
|
||||
stream.write(data);
|
||||
}
|
||||
});
|
||||
|
||||
stdout.on("data", (chunk) => {
|
||||
connection.send(chunk.toString());
|
||||
});
|
||||
|
||||
stderr.on("data", (chunk) => {
|
||||
connection.send(chunk.toString());
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user