mirror of
https://github.com/CyberL1/dlinux-dashboard.git
synced 2025-04-02 14:08:36 -04:00
Compare commits
8 Commits
81bf5fb6af
...
3cd59266c2
Author | SHA1 | Date | |
---|---|---|---|
3cd59266c2 | |||
f1b7e063bb | |||
17c2a3042f | |||
5fcc8b4859 | |||
4ca8045df9 | |||
03ddc35df0 | |||
238c5a2cec | |||
e47e87199f |
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Code containers</title>
|
<title>DLinux</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
31
src/components/ContainerInfo.tsx
Normal file
31
src/components/ContainerInfo.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Typography } from "@mui/material";
|
||||||
|
import { InfoData } from "../types";
|
||||||
|
|
||||||
|
export default function ContainerInfo() {
|
||||||
|
const [info, setInfo] = useState<InfoData>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateInfo = async () => {
|
||||||
|
const stats = await fetch(`https://api.ssh.surf/info`, {
|
||||||
|
// @ts-ignore
|
||||||
|
headers: { "x-ssh-auth": localStorage.getItem("key") },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await stats.json();
|
||||||
|
setInfo(data);
|
||||||
|
};
|
||||||
|
updateInfo();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!info) {
|
||||||
|
return "Fetching container info...";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography>Hostname: {info.name}</Typography>
|
||||||
|
<Typography>Memory: {info.memory}</Typography>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,36 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { ContainerStats as ContainerStatsType } from "dockerode";
|
|
||||||
import { Typography } from "@mui/material";
|
|
||||||
|
|
||||||
export default function ContainerStats({ id }: { id: string }) {
|
|
||||||
const [stats, setStats] = useState<ContainerStatsType>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const sse = new EventSource(`/api/containers/${id}/stats`);
|
|
||||||
|
|
||||||
sse.onmessage = ({ data }) => {
|
|
||||||
const parsed = JSON.parse(data);
|
|
||||||
setStats(parsed);
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
sse.close();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!stats) {
|
|
||||||
return "Fetching container stats...";
|
|
||||||
}
|
|
||||||
|
|
||||||
const CPUPercentage =
|
|
||||||
((stats.cpu_stats.cpu_usage.total_usage -
|
|
||||||
stats.precpu_stats.cpu_usage.total_usage) /
|
|
||||||
(stats.cpu_stats.system_cpu_usage -
|
|
||||||
stats.precpu_stats.system_cpu_usage)) *
|
|
||||||
stats.cpu_stats.online_cpus *
|
|
||||||
100;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Typography>CPU Usage: {(CPUPercentage || 0).toFixed(2) + "%"}</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
42
src/components/Login.tsx
Normal file
42
src/components/Login.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Button, Paper, TextField } from "@mui/material";
|
||||||
|
import { FormEvent } from "react";
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
async function onSubmit(e: FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(e.target as HTMLFormElement);
|
||||||
|
const data = Object.fromEntries(formData);
|
||||||
|
|
||||||
|
const hello = await fetch(`https://api.ssh.surf/hello`, {
|
||||||
|
// @ts-ignore
|
||||||
|
headers: { "x-ssh-auth": data.key },
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await hello.json();
|
||||||
|
|
||||||
|
if (json.message.startsWith("Hello")) {
|
||||||
|
localStorage.setItem("key", data.key.toString());
|
||||||
|
localStorage.setItem("hostname", json.message.slice(7).replace("!", ""));
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
component="form"
|
||||||
|
autoComplete="off"
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
margin: "auto",
|
||||||
|
width: 300,
|
||||||
|
height: 95,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField label="API Key" type="password" name="key" variant="filled" />
|
||||||
|
<Button type="submit">Log in</Button>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
66
src/components/ProcessesTable.tsx
Normal file
66
src/components/ProcessesTable.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
Paper,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Process } from "../types";
|
||||||
|
|
||||||
|
export default function ProcessesTable({ container }: { container: string }) {
|
||||||
|
const [processes, setProcesses] = useState<Process[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateProcesses = async () => {
|
||||||
|
const processesForTable = [];
|
||||||
|
|
||||||
|
const apiCall = await fetch(
|
||||||
|
`https://process-list.syscall.lol/${container}`,
|
||||||
|
);
|
||||||
|
const processes = await apiCall.json();
|
||||||
|
|
||||||
|
for (const process of processes) {
|
||||||
|
const proc: Process = {
|
||||||
|
pid: process[1],
|
||||||
|
user: process[0],
|
||||||
|
command: process[7],
|
||||||
|
};
|
||||||
|
|
||||||
|
processesForTable.push(proc);
|
||||||
|
}
|
||||||
|
|
||||||
|
setProcesses(processesForTable);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateProcesses();
|
||||||
|
setInterval(updateProcesses, 3000);
|
||||||
|
}, [container]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>PID</TableCell>
|
||||||
|
<TableCell>User</TableCell>
|
||||||
|
<TableCell>Command</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{processes.map((process) => (
|
||||||
|
<TableRow key={process.pid}>
|
||||||
|
<TableCell component="th" scope="row">
|
||||||
|
{process.pid}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{process.user}</TableCell>
|
||||||
|
<TableCell>{process.command}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
@ -4,59 +4,61 @@ import { AttachAddon } from "@xterm/addon-attach";
|
|||||||
import { FitAddon } from "@xterm/addon-fit";
|
import { FitAddon } from "@xterm/addon-fit";
|
||||||
import "@xterm/xterm/css/xterm.css";
|
import "@xterm/xterm/css/xterm.css";
|
||||||
import { useLoaderData } from "react-router";
|
import { useLoaderData } from "react-router";
|
||||||
import { Container } from "../types";
|
import { InfoData } from "../types";
|
||||||
|
|
||||||
export default function WebTerminal() {
|
export default function WebTerminal() {
|
||||||
const container = useLoaderData() as Container;
|
let container = useLoaderData();
|
||||||
const terminalRef = useRef<HTMLDivElement>(null);
|
container = container.data as InfoData;
|
||||||
|
|
||||||
useEffect(() => {
|
const terminalRef = useRef<HTMLDivElement>(null);
|
||||||
document.title = `Terminal: ${container.name}`;
|
|
||||||
|
|
||||||
if (terminalRef.current) {
|
useEffect(() => {
|
||||||
const terminal = new Terminal({ rows: 67 });
|
document.title = `Terminal: ${container.name}`;
|
||||||
|
|
||||||
const socket = new WebSocket(
|
if (terminalRef.current) {
|
||||||
`ws://127.0.0.1:3000/containers/${container.id}/terminal`,
|
const terminal = new Terminal({ rows: 67 });
|
||||||
);
|
|
||||||
|
|
||||||
const fitAddon = new FitAddon();
|
const socket = new WebSocket(
|
||||||
terminal.loadAddon(fitAddon);
|
`ws://127.0.0.1:3000/containers/${container.id}/terminal`, // TODO: Replace this with dlinux ssh connector
|
||||||
|
);
|
||||||
|
|
||||||
terminal.open(terminalRef.current);
|
const fitAddon = new FitAddon();
|
||||||
fitAddon.fit();
|
terminal.loadAddon(fitAddon);
|
||||||
|
|
||||||
terminal.write("Connecting to the container\r\n");
|
terminal.open(terminalRef.current);
|
||||||
|
fitAddon.fit();
|
||||||
|
|
||||||
socket.onopen = () => {
|
terminal.write("Connecting to the container\r\n");
|
||||||
const attachAddon = new AttachAddon(socket);
|
|
||||||
terminal.loadAddon(attachAddon);
|
|
||||||
|
|
||||||
terminal.clear();
|
socket.onopen = () => {
|
||||||
terminal.focus();
|
const attachAddon = new AttachAddon(socket);
|
||||||
|
terminal.loadAddon(attachAddon);
|
||||||
|
|
||||||
socket.send(
|
terminal.clear();
|
||||||
JSON.stringify({
|
terminal.focus();
|
||||||
rows: terminal.rows,
|
|
||||||
cols: terminal.cols,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("resize", () => {
|
socket.send(
|
||||||
fitAddon.fit();
|
JSON.stringify({
|
||||||
});
|
rows: terminal.rows,
|
||||||
|
cols: terminal.cols,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
terminal.onResize(({ rows, cols }) => {
|
window.addEventListener("resize", () => {
|
||||||
socket.send(JSON.stringify({ rows, cols }));
|
fitAddon.fit();
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
terminal.onResize(({ rows, cols }) => {
|
||||||
socket.close();
|
socket.send(JSON.stringify({ rows, cols }));
|
||||||
terminal.dispose();
|
});
|
||||||
};
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return <div ref={terminalRef} style={{ height: "auto" }} />;
|
return () => {
|
||||||
|
socket.close();
|
||||||
|
terminal.dispose();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [container]);
|
||||||
|
|
||||||
|
return <div ref={terminalRef} style={{ height: "auto" }} />;
|
||||||
}
|
}
|
||||||
|
69
src/main.tsx
69
src/main.tsx
@ -3,41 +3,46 @@ import ReactDOM from "react-dom/client";
|
|||||||
import App from "./pages/App.tsx";
|
import App from "./pages/App.tsx";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { createBrowserRouter, RouterProvider } from "react-router";
|
import { createBrowserRouter, RouterProvider } from "react-router";
|
||||||
import Containers, {
|
import ContainerPage from "./pages/container/index.tsx";
|
||||||
Loader as ContainersLoader,
|
import TerminalPage from "./pages/container/terminal.tsx";
|
||||||
} from "./pages/containers/index.tsx";
|
import ProcessesPage from "./pages/container/processes.tsx";
|
||||||
import ContainerPage, {
|
|
||||||
Loader as ContainerDataLoader,
|
async function loader() {
|
||||||
} from "./pages/containers/[name]/index.tsx";
|
const info = await fetch(`https://api.ssh.surf/info`, {
|
||||||
import TerminalPage from "./pages/containers/[name]/terminal.tsx";
|
// @ts-ignore
|
||||||
|
headers: { "x-ssh-auth": localStorage.getItem("key") },
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await info.json();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: <App />,
|
element: <App />,
|
||||||
errorElement: <App error />,
|
errorElement: <App error />,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "/containers",
|
path: "/container",
|
||||||
element: <Containers />,
|
element: <ContainerPage />,
|
||||||
loader: ContainersLoader,
|
loader,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/containers/:name",
|
path: "/container/terminal",
|
||||||
element: <ContainerPage />,
|
element: <TerminalPage />,
|
||||||
loader: ContainerDataLoader,
|
loader,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/containers/:name/terminal",
|
path: "/container/processes",
|
||||||
element: <TerminalPage />,
|
element: <ProcessesPage />,
|
||||||
loader: ContainerDataLoader,
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
@ -3,103 +3,150 @@ import "@fontsource/roboto/400.css";
|
|||||||
import "@fontsource/roboto/500.css";
|
import "@fontsource/roboto/500.css";
|
||||||
import "@fontsource/roboto/700.css";
|
import "@fontsource/roboto/700.css";
|
||||||
import {
|
import {
|
||||||
AppBar,
|
AppBar,
|
||||||
Box,
|
Box,
|
||||||
Drawer,
|
createTheme,
|
||||||
Icon,
|
Drawer,
|
||||||
IconButton,
|
FormControlLabel,
|
||||||
List,
|
Icon,
|
||||||
ListItem,
|
IconButton,
|
||||||
ListItemButton,
|
List,
|
||||||
ListItemIcon,
|
ListItem,
|
||||||
ListItemText,
|
ListItemButton,
|
||||||
Toolbar,
|
ListItemIcon,
|
||||||
Typography,
|
ListItemText,
|
||||||
|
Switch,
|
||||||
|
ThemeProvider,
|
||||||
|
Toolbar,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
useColorScheme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import * as Icons from "@mui/icons-material";
|
import * as Icons from "@mui/icons-material";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Link, Outlet } from "react-router";
|
import { Link, Outlet } from "react-router";
|
||||||
import ErrorPage from "./Error";
|
import ErrorPage from "./Error";
|
||||||
|
import Login from "../components/Login";
|
||||||
|
|
||||||
interface Item {
|
interface Item {
|
||||||
title: string;
|
title: string;
|
||||||
icon: keyof typeof Icons;
|
icon: keyof typeof Icons;
|
||||||
href: string;
|
href: string;
|
||||||
|
openInNewTab?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sidebarItems: Item[] = [
|
const sidebarItems: Item[] = [
|
||||||
{ title: "Containers", icon: "Storage", href: "/containers" },
|
{ title: "Info", icon: "Info", href: "/container" },
|
||||||
|
{ title: "Terminal", icon: "Terminal", href: "/container/terminal" },
|
||||||
|
{ title: "Processes", icon: "Memory", href: "/container/processes" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const drawerWidth = 240;
|
const drawerWidth = 240;
|
||||||
|
|
||||||
function App({ error }: { error?: boolean }) {
|
const theme = createTheme({
|
||||||
const [isOpen, setOpen] = useState(false);
|
colorSchemes: {
|
||||||
|
dark: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
function App({ error }: { error?: boolean }) {
|
||||||
<>
|
const [isOpen, setOpen] = useState(false);
|
||||||
<AppBar
|
const { mode, setMode } = useColorScheme();
|
||||||
position="fixed"
|
|
||||||
sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
if (!mode) {
|
||||||
>
|
return null;
|
||||||
<Toolbar>
|
}
|
||||||
<IconButton
|
|
||||||
onClick={() => setOpen(!isOpen)}
|
return (
|
||||||
size="large"
|
<>
|
||||||
color="inherit"
|
<AppBar
|
||||||
sx={{ mr: 2 }}
|
position="fixed"
|
||||||
>
|
sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||||
<Icons.Menu />
|
>
|
||||||
</IconButton>
|
<Toolbar>
|
||||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
<IconButton
|
||||||
Code containers
|
onClick={() => setOpen(!isOpen)}
|
||||||
</Typography>
|
size="large"
|
||||||
</Toolbar>
|
color="inherit"
|
||||||
</AppBar>
|
sx={{ mr: 2 }}
|
||||||
<Drawer
|
>
|
||||||
variant="permanent"
|
<Icons.Menu />
|
||||||
sx={{
|
</IconButton>
|
||||||
width: isOpen ? drawerWidth : 56,
|
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||||
"& .MuiDrawer-paper": {
|
DLinux
|
||||||
width: isOpen ? drawerWidth : 56,
|
</Typography>
|
||||||
overflowX: "hidden",
|
<FormControlLabel
|
||||||
transition: (theme) =>
|
control={<Switch defaultChecked={mode === "system"} />}
|
||||||
theme.transitions.create("width", {
|
label="Automatic theme"
|
||||||
easing: theme.transitions.easing.sharp,
|
labelPlacement="start"
|
||||||
duration: theme.transitions.duration.enteringScreen,
|
onChange={() => setMode(mode === "system" ? "light" : "system")}
|
||||||
}),
|
/>
|
||||||
},
|
{mode != "system" && (
|
||||||
}}
|
<IconButton
|
||||||
>
|
onClick={() => setMode(mode === "dark" ? "light" : "dark")}
|
||||||
<Toolbar />
|
>
|
||||||
<List>
|
{mode === "dark" ? (
|
||||||
{sidebarItems.map((item) => (
|
<Icons.LightMode />
|
||||||
<ListItem
|
) : (
|
||||||
key={item.title}
|
<Icons.DarkMode htmlColor="#fff" />
|
||||||
component={Link}
|
)}
|
||||||
to={item.href}
|
</IconButton>
|
||||||
disablePadding
|
)}
|
||||||
>
|
</Toolbar>
|
||||||
<ListItemButton>
|
</AppBar>
|
||||||
<ListItemIcon>
|
<Drawer
|
||||||
<Icon component={Icons[item.icon]} />
|
variant="permanent"
|
||||||
</ListItemIcon>
|
sx={{
|
||||||
<ListItemText primary={item.title} />
|
width: isOpen ? drawerWidth : 56,
|
||||||
</ListItemButton>
|
"& .MuiDrawer-paper": {
|
||||||
</ListItem>
|
width: isOpen ? drawerWidth : 56,
|
||||||
))}
|
overflowX: "hidden",
|
||||||
</List>
|
transition: (theme) =>
|
||||||
</Drawer>
|
theme.transitions.create("width", {
|
||||||
<Toolbar />
|
easing: theme.transitions.easing.sharp,
|
||||||
<Box
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
component="main"
|
}),
|
||||||
sx={{ p: 2, marginLeft: isOpen ? "240px" : "56px" }}
|
},
|
||||||
>
|
}}
|
||||||
{error && <ErrorPage />}
|
>
|
||||||
<Outlet />
|
<Toolbar />
|
||||||
</Box>
|
<List>
|
||||||
</>
|
{sidebarItems.map((item) => (
|
||||||
);
|
<Tooltip title={item.title} placement="left" arrow>
|
||||||
|
<ListItem
|
||||||
|
key={item.title}
|
||||||
|
component={Link}
|
||||||
|
to={item.href}
|
||||||
|
target={item.openInNewTab ? "_blank" : "_self"}
|
||||||
|
disablePadding
|
||||||
|
>
|
||||||
|
<ListItemButton>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Icon component={Icons[item.icon]} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={item.title} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Drawer>
|
||||||
|
<Toolbar />
|
||||||
|
<Box
|
||||||
|
component="main"
|
||||||
|
sx={{ p: 2, marginLeft: isOpen ? "240px" : "56px" }}
|
||||||
|
>
|
||||||
|
{error && <ErrorPage />}
|
||||||
|
{localStorage.getItem("key") ? <Outlet /> : <Login />}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default function ToggleColorMode() {
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
61
src/pages/container/index.tsx
Normal file
61
src/pages/container/index.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { useLoaderData, useRevalidator } from "react-router";
|
||||||
|
import { InfoData } from "../../types";
|
||||||
|
import { Button, ButtonGroup, Paper, Typography } from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import ContainerInfo from "../../components/ContainerInfo";
|
||||||
|
|
||||||
|
export default function ContainerPage() {
|
||||||
|
let container = useLoaderData();
|
||||||
|
const revalidator = useRevalidator();
|
||||||
|
const [isPowerStateLocked, setPowerStateLocked] = useState<boolean>();
|
||||||
|
|
||||||
|
if (!container.data) {
|
||||||
|
return "Container not found";
|
||||||
|
}
|
||||||
|
|
||||||
|
container = container.data as InfoData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper square sx={{ padding: 1 }}>
|
||||||
|
<Paper sx={{ display: "flex" }} variant="outlined">
|
||||||
|
<Typography variant="h6" sx={{ flexGrow: 1 }}>
|
||||||
|
State: {container.state.Status}
|
||||||
|
</Typography>
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button
|
||||||
|
loading={isPowerStateLocked}
|
||||||
|
onClick={async () => {
|
||||||
|
await switchPowerState(
|
||||||
|
container.state.Status === "running" ? "stop" : "start",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Power {container.state.Status === "running" ? "off" : "on"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={async () => await switchPowerState("restart")}
|
||||||
|
loading={isPowerStateLocked}
|
||||||
|
disabled={container.state.Status === "exited"}
|
||||||
|
>
|
||||||
|
Restart
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Paper>
|
||||||
|
<ContainerInfo />
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
|
||||||
|
async function switchPowerState(state: string) {
|
||||||
|
setPowerStateLocked(true);
|
||||||
|
|
||||||
|
const res = await fetch(`https://api.ssh.surf/${state}`, {
|
||||||
|
// @ts-ignore
|
||||||
|
headers: { "x-ssh-auth": localStorage.getItem("key") },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
setPowerStateLocked(false);
|
||||||
|
revalidator.revalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
src/pages/container/processes.tsx
Normal file
6
src/pages/container/processes.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import ProcessesTable from "../../components/ProcessesTable";
|
||||||
|
|
||||||
|
export default function ProcessesPage() {
|
||||||
|
const containerName = localStorage["hostname"];
|
||||||
|
return <ProcessesTable container={containerName} />;
|
||||||
|
}
|
25
src/pages/container/terminal.tsx
Normal file
25
src/pages/container/terminal.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useLoaderData } from "react-router";
|
||||||
|
import { InfoData } from "../../types";
|
||||||
|
import { Paper, Typography } from "@mui/material";
|
||||||
|
import WebTerminal from "../../components/WebTerminal";
|
||||||
|
|
||||||
|
export default function TerminalPage() {
|
||||||
|
let container = useLoaderData();
|
||||||
|
|
||||||
|
if (!container.data) {
|
||||||
|
return "Container not found";
|
||||||
|
}
|
||||||
|
|
||||||
|
container = container.data as InfoData;
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,73 +0,0 @@
|
|||||||
import { useLoaderData, useRevalidator } from "react-router";
|
|
||||||
import { Container } from "../../../types";
|
|
||||||
import { Button, ButtonGroup, Paper, Typography } from "@mui/material";
|
|
||||||
import { useState } from "react";
|
|
||||||
import ContainerStats from "../../../components/ContainerStats";
|
|
||||||
|
|
||||||
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 ContainerPage() {
|
|
||||||
const container = useLoaderData() as Container & { statusCode: number };
|
|
||||||
|
|
||||||
if (container.statusCode === 404) {
|
|
||||||
return "Container not found";
|
|
||||||
}
|
|
||||||
|
|
||||||
const revalidator = useRevalidator();
|
|
||||||
const [isPowerStateLocked, setPowerStateLocked] = useState<boolean>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper square sx={{ padding: 1 }}>
|
|
||||||
<Paper sx={{ display: "flex" }} variant="outlined">
|
|
||||||
<Typography variant="h6" sx={{ flexGrow: 1 }}>
|
|
||||||
Managing: {container.name}
|
|
||||||
</Typography>
|
|
||||||
<ButtonGroup>
|
|
||||||
<Button href={`/containers/${container.name}/terminal`}>
|
|
||||||
Terminal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
loading={isPowerStateLocked}
|
|
||||||
onClick={async () => {
|
|
||||||
await switchPowerState(
|
|
||||||
container.status === "running" ? "stop" : "start",
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Power {container.status === "running" ? "off" : "on"}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={async () => await switchPowerState("restart")}
|
|
||||||
loading={isPowerStateLocked}
|
|
||||||
disabled={container.status === "exited"}
|
|
||||||
>
|
|
||||||
Restart
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
</Paper>
|
|
||||||
<ContainerStats id={container.id} />
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
|
|
||||||
async function switchPowerState(state: string) {
|
|
||||||
setPowerStateLocked(true);
|
|
||||||
|
|
||||||
const res = await fetch(`/api/containers/${container.name}/${state}`, {
|
|
||||||
method: "PUT",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
setPowerStateLocked(false);
|
|
||||||
revalidator.revalidate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
import { useLoaderData } from "react-router";
|
|
||||||
import { Container } from "../../types";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
CardActions,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
Grid2 as Grid,
|
|
||||||
} from "@mui/material";
|
|
||||||
|
|
||||||
export async function Loader() {
|
|
||||||
const containers = await fetch("/api/containers");
|
|
||||||
|
|
||||||
const data = await containers.json();
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Containers() {
|
|
||||||
const containers = useLoaderData() as Container[];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
{containers.map((container) => (
|
|
||||||
<Card key={container.id}>
|
|
||||||
<CardHeader
|
|
||||||
title={container.name}
|
|
||||||
sx={{
|
|
||||||
textOverflow: "elipsis",
|
|
||||||
overflow: "hidden",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<CardContent>
|
|
||||||
Image: {container.image}
|
|
||||||
<br />
|
|
||||||
Status: {container.status}
|
|
||||||
</CardContent>
|
|
||||||
<CardActions>
|
|
||||||
<Button href={`/containers/${container.name}`}>Manage</Button>
|
|
||||||
<Button
|
|
||||||
href={`//${container.name}.${document.location.host}`}
|
|
||||||
target="_blank"
|
|
||||||
disabled={container.status === "exited"}
|
|
||||||
>
|
|
||||||
Open container
|
|
||||||
</Button>
|
|
||||||
</CardActions>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
40
src/types.ts
40
src/types.ts
@ -1,7 +1,35 @@
|
|||||||
export interface Container {
|
export interface Info {
|
||||||
id: string;
|
success: boolean;
|
||||||
name: string;
|
data: InfoData;
|
||||||
image: string;
|
}
|
||||||
status: string;
|
|
||||||
ip: string;
|
export interface InfoData {
|
||||||
|
name: string;
|
||||||
|
IPAddress: string;
|
||||||
|
MacAddress: string;
|
||||||
|
memory: string;
|
||||||
|
cpus: string;
|
||||||
|
restartPolicy: { Name: string; MaximumRetryCount: number };
|
||||||
|
restarts: number;
|
||||||
|
state: {
|
||||||
|
Status: string;
|
||||||
|
Running: boolean;
|
||||||
|
Paused: boolean;
|
||||||
|
Restarting: boolean;
|
||||||
|
OOMKilled: boolean;
|
||||||
|
Dead: boolean;
|
||||||
|
Pid: number;
|
||||||
|
ExitCode: number;
|
||||||
|
Error: string;
|
||||||
|
StartedAt: string;
|
||||||
|
FinishedAt: string;
|
||||||
|
};
|
||||||
|
created: string;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Process {
|
||||||
|
pid: number;
|
||||||
|
user: string;
|
||||||
|
command: string;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user