mirror of
https://github.com/CyberL1/dlinux-dashboard.git
synced 2025-01-21 17:09:19 -05:00
feat: connecting to the dlinux api
This commit is contained in:
parent
81bf5fb6af
commit
e47e87199f
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Code containers</title>
|
||||
<title>DLinux</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
25
src/components/ContainerInfo.tsx
Normal file
25
src/components/ContainerInfo.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
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`, {
|
||||
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>);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
40
src/components/Login.tsx
Normal file
40
src/components/Login.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
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`, {
|
||||
headers: { "x-ssh-auth": data.key },
|
||||
});
|
||||
|
||||
const json = await hello.json();
|
||||
|
||||
if (json.message.startsWith("Hello")) {
|
||||
localStorage.setItem("key", data.key.toString());
|
||||
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>
|
||||
);
|
||||
}
|
@ -4,10 +4,12 @@ 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";
|
||||
import { InfoData } from "../types";
|
||||
|
||||
export default function WebTerminal() {
|
||||
const container = useLoaderData() as Container;
|
||||
let container = useLoaderData();
|
||||
container = container.data as InfoData;
|
||||
|
||||
const terminalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
31
src/main.tsx
31
src/main.tsx
@ -3,13 +3,17 @@ import ReactDOM from "react-dom/client";
|
||||
import App from "./pages/App.tsx";
|
||||
import "./index.css";
|
||||
import { createBrowserRouter, RouterProvider } from "react-router";
|
||||
import Containers, {
|
||||
Loader as ContainersLoader,
|
||||
} from "./pages/containers/index.tsx";
|
||||
import ContainerPage, {
|
||||
Loader as ContainerDataLoader,
|
||||
} from "./pages/containers/[name]/index.tsx";
|
||||
import TerminalPage from "./pages/containers/[name]/terminal.tsx";
|
||||
import ContainerPage from "./pages/container/index.tsx";
|
||||
import TerminalPage from "./pages/container/terminal.tsx";
|
||||
|
||||
async function loader() {
|
||||
const info = await fetch(`https://api.ssh.surf/info`, {
|
||||
headers: { "x-ssh-auth": localStorage.getItem("key") },
|
||||
});
|
||||
|
||||
const data = await info.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@ -18,19 +22,14 @@ const router = createBrowserRouter([
|
||||
errorElement: <App error />,
|
||||
children: [
|
||||
{
|
||||
path: "/containers",
|
||||
element: <Containers />,
|
||||
loader: ContainersLoader,
|
||||
},
|
||||
{
|
||||
path: "/containers/:name",
|
||||
path: "/container",
|
||||
element: <ContainerPage />,
|
||||
loader: ContainerDataLoader,
|
||||
loader,
|
||||
},
|
||||
{
|
||||
path: "/containers/:name/terminal",
|
||||
path: "/container/terminal",
|
||||
element: <TerminalPage />,
|
||||
loader: ContainerDataLoader,
|
||||
loader,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -20,6 +20,7 @@ import * as Icons from "@mui/icons-material";
|
||||
import { useState } from "react";
|
||||
import { Link, Outlet } from "react-router";
|
||||
import ErrorPage from "./Error";
|
||||
import Login from "../components/Login";
|
||||
|
||||
interface Item {
|
||||
title: string;
|
||||
@ -28,7 +29,8 @@ interface Item {
|
||||
}
|
||||
|
||||
const sidebarItems: Item[] = [
|
||||
{ title: "Containers", icon: "Storage", href: "/containers" },
|
||||
{ title: "Info", icon: "Info", href: "/container" },
|
||||
{ title: "Terminal", icon: "Terminal", href: "/container/terminal" },
|
||||
];
|
||||
|
||||
const drawerWidth = 240;
|
||||
@ -52,7 +54,7 @@ function App({ error }: { error?: boolean }) {
|
||||
<Icons.Menu />
|
||||
</IconButton>
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
Code containers
|
||||
DLinux
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
@ -96,7 +98,7 @@ function App({ error }: { error?: boolean }) {
|
||||
sx={{ p: 2, marginLeft: isOpen ? "240px" : "56px" }}
|
||||
>
|
||||
{error && <ErrorPage />}
|
||||
<Outlet />
|
||||
{localStorage.getItem("key") ? <Outlet /> : <Login />}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
60
src/pages/container/index.tsx
Normal file
60
src/pages/container/index.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
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();
|
||||
if (!container.data) {
|
||||
return "Container not found";
|
||||
}
|
||||
|
||||
container = container.data as InfoData;
|
||||
|
||||
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
|
||||
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(`/api/containers/${container.name}/${state}`, {
|
||||
method: "PUT",
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setPowerStateLocked(false);
|
||||
revalidator.revalidate();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +1,14 @@
|
||||
import { useLoaderData } from "react-router";
|
||||
import { Container } from "../../../types";
|
||||
import { Container } from "../../types";
|
||||
import { Paper, Typography } from "@mui/material";
|
||||
import WebTerminal from "../../../components/WebTerminal";
|
||||
import WebTerminal from "../../components/WebTerminal";
|
||||
|
||||
interface Params {
|
||||
name: string;
|
||||
}
|
||||
export async function Loader() {
|
||||
const info = await fetch(`https://api.ssh.surf/info`, {
|
||||
headers: { "x-ssh-auth": localStorage["key"] },
|
||||
});
|
||||
|
||||
export async function Loader({ params }: { params: Params }) {
|
||||
const container = await fetch(`/api/containers/${params.name}`);
|
||||
|
||||
const data = await container.json();
|
||||
const data = await info.json();
|
||||
return data;
|
||||
}
|
||||
|
@ -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,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>
|
||||
</>
|
||||
);
|
||||
}
|
34
src/types.ts
34
src/types.ts
@ -1,7 +1,29 @@
|
||||
export interface Container {
|
||||
id: string;
|
||||
name: string;
|
||||
image: string;
|
||||
status: string;
|
||||
ip: string;
|
||||
export interface Info {
|
||||
success: boolean;
|
||||
data: InfoData;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user