diff --git a/index.html b/index.html index 09c76e3..903ad29 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Code containers + DLinux
diff --git a/src/components/ContainerInfo.tsx b/src/components/ContainerInfo.tsx new file mode 100644 index 0000000..808f06c --- /dev/null +++ b/src/components/ContainerInfo.tsx @@ -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(); + + 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 (Hostname: {info.name}); +} diff --git a/src/components/ContainerStats.tsx b/src/components/ContainerStats.tsx deleted file mode 100644 index 7926148..0000000 --- a/src/components/ContainerStats.tsx +++ /dev/null @@ -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(); - - 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 ( - CPU Usage: {(CPUPercentage || 0).toFixed(2) + "%"} - ); -} diff --git a/src/components/Login.tsx b/src/components/Login.tsx new file mode 100644 index 0000000..8d39ed6 --- /dev/null +++ b/src/components/Login.tsx @@ -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 ( + + + + + ); +} diff --git a/src/components/WebTerminal.tsx b/src/components/WebTerminal.tsx index 2b59551..af85c52 100644 --- a/src/components/WebTerminal.tsx +++ b/src/components/WebTerminal.tsx @@ -4,59 +4,61 @@ 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; - const terminalRef = useRef(null); + let container = useLoaderData(); + container = container.data as InfoData; - useEffect(() => { - document.title = `Terminal: ${container.name}`; + const terminalRef = useRef(null); - if (terminalRef.current) { - const terminal = new Terminal({ rows: 67 }); + useEffect(() => { + document.title = `Terminal: ${container.name}`; - const socket = new WebSocket( - `ws://127.0.0.1:3000/containers/${container.id}/terminal`, - ); + if (terminalRef.current) { + const terminal = new Terminal({ rows: 67 }); - const fitAddon = new FitAddon(); - terminal.loadAddon(fitAddon); + const socket = new WebSocket( + `ws://127.0.0.1:3000/containers/${container.id}/terminal`, + ); - terminal.open(terminalRef.current); - fitAddon.fit(); + const fitAddon = new FitAddon(); + terminal.loadAddon(fitAddon); - terminal.write("Connecting to the container\r\n"); + terminal.open(terminalRef.current); + fitAddon.fit(); - socket.onopen = () => { - const attachAddon = new AttachAddon(socket); - terminal.loadAddon(attachAddon); + terminal.write("Connecting to the container\r\n"); - terminal.clear(); - terminal.focus(); + socket.onopen = () => { + const attachAddon = new AttachAddon(socket); + terminal.loadAddon(attachAddon); - socket.send( - JSON.stringify({ - rows: terminal.rows, - cols: terminal.cols, - }), - ); - }; + terminal.clear(); + terminal.focus(); - window.addEventListener("resize", () => { - fitAddon.fit(); - }); + socket.send( + JSON.stringify({ + rows: terminal.rows, + cols: terminal.cols, + }), + ); + }; - terminal.onResize(({ rows, cols }) => { - socket.send(JSON.stringify({ rows, cols })); - }); + window.addEventListener("resize", () => { + fitAddon.fit(); + }); - return () => { - socket.close(); - terminal.dispose(); - }; - } - }, []); + terminal.onResize(({ rows, cols }) => { + socket.send(JSON.stringify({ rows, cols })); + }); - return
; + return () => { + socket.close(); + terminal.dispose(); + }; + } + }, []); + + return
; } diff --git a/src/main.tsx b/src/main.tsx index 6903389..f4ddaf4 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,41 +3,40 @@ 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([ - { - path: "/", - element: , - errorElement: , - children: [ - { - path: "/containers", - element: , - loader: ContainersLoader, - }, - { - path: "/containers/:name", - element: , - loader: ContainerDataLoader, - }, - { - path: "/containers/:name/terminal", - element: , - loader: ContainerDataLoader, - }, - ], - }, + { + path: "/", + element: , + errorElement: , + children: [ + { + path: "/container", + element: , + loader, + }, + { + path: "/container/terminal", + element: , + loader, + }, + ], + }, ]); ReactDOM.createRoot(document.getElementById("root")!).render( - - - , + + + , ); diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 2756db2..7b7cc3e 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -3,103 +3,105 @@ import "@fontsource/roboto/400.css"; import "@fontsource/roboto/500.css"; import "@fontsource/roboto/700.css"; import { - AppBar, - Box, - Drawer, - Icon, - IconButton, - List, - ListItem, - ListItemButton, - ListItemIcon, - ListItemText, - Toolbar, - Typography, + AppBar, + Box, + Drawer, + Icon, + IconButton, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Toolbar, + Typography, } from "@mui/material"; 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; - icon: keyof typeof Icons; - href: string; + title: string; + icon: keyof typeof Icons; + href: string; } 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; function App({ error }: { error?: boolean }) { - const [isOpen, setOpen] = useState(false); + const [isOpen, setOpen] = useState(false); - return ( - <> - theme.zIndex.drawer + 1 }} - > - - setOpen(!isOpen)} - size="large" - color="inherit" - sx={{ mr: 2 }} - > - - - - Code containers - - - - - theme.transitions.create("width", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.enteringScreen, - }), - }, - }} - > - - - {sidebarItems.map((item) => ( - - - - - - - - - ))} - - - - - {error && } - - - - ); + return ( + <> + theme.zIndex.drawer + 1 }} + > + + setOpen(!isOpen)} + size="large" + color="inherit" + sx={{ mr: 2 }} + > + + + + DLinux + + + + + theme.transitions.create("width", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + }, + }} + > + + + {sidebarItems.map((item) => ( + + + + + + + + + ))} + + + + + {error && } + {localStorage.getItem("key") ? : } + + + ); } export default App; diff --git a/src/pages/container/index.tsx b/src/pages/container/index.tsx new file mode 100644 index 0000000..1acb497 --- /dev/null +++ b/src/pages/container/index.tsx @@ -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(); + + return ( + + + + Managing: {container.name} + + + + + + + + + ); + + 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(); + } + } +} diff --git a/src/pages/containers/[name]/terminal.tsx b/src/pages/container/terminal.tsx similarity index 63% rename from src/pages/containers/[name]/terminal.tsx rename to src/pages/container/terminal.tsx index 4c36e93..2767903 100644 --- a/src/pages/containers/[name]/terminal.tsx +++ b/src/pages/container/terminal.tsx @@ -1,17 +1,15 @@ 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(); - return data; + const data = await info.json(); + return data; } export default function TerminalPage() { diff --git a/src/pages/containers/[name]/index.tsx b/src/pages/containers/[name]/index.tsx deleted file mode 100644 index 9bacede..0000000 --- a/src/pages/containers/[name]/index.tsx +++ /dev/null @@ -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(); - - return ( - - - - Managing: {container.name} - - - - - - - - - - ); - - 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(); - } - } -} diff --git a/src/pages/containers/index.tsx b/src/pages/containers/index.tsx deleted file mode 100644 index b7e43e7..0000000 --- a/src/pages/containers/index.tsx +++ /dev/null @@ -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 ( - <> - - {containers.map((container) => ( - - - - Image: {container.image} -
- Status: {container.status} -
- - - - -
- ))} -
- - ); -} diff --git a/src/types.ts b/src/types.ts index 0781da7..4e3786f 100644 --- a/src/types.ts +++ b/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; }