(feature) Update UI & Resource Availability
This commit is contained in:
parent
11d8229eb5
commit
929193d272
44 changed files with 4747 additions and 27 deletions
12
src/MCL.jsx
12
src/MCL.jsx
|
@ -1,16 +1,20 @@
|
|||
// Imports
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { SettingsProvider } from "@mcl/settings";
|
||||
import Viewport from "./nav/Viewport.jsx";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
// Create a query client for the app
|
||||
const queryClient = new QueryClient();
|
||||
// Export Minecluster
|
||||
export default function MCL() {
|
||||
return (
|
||||
<div className="mcl">
|
||||
<div className="minecluster">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<h1>Welcome to Minecluster!</h1>
|
||||
</BrowserRouter>
|
||||
<SettingsProvider>
|
||||
<BrowserRouter>
|
||||
<Viewport />
|
||||
</BrowserRouter>
|
||||
</SettingsProvider>
|
||||
</QueryClientProvider>
|
||||
</div>
|
||||
);
|
||||
|
|
62
src/ctx/SettingsContext.jsx
Normal file
62
src/ctx/SettingsContext.jsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import React, { useReducer, createContext, useMemo } from "react";
|
||||
const SettingsContext = createContext();
|
||||
|
||||
const ACTIONS = {
|
||||
UPDATE: "u",
|
||||
};
|
||||
|
||||
const localSettings = localStorage.getItem("settings");
|
||||
const defaultSettings = {
|
||||
simplifiedControls: false,
|
||||
logAppDetails: true,
|
||||
defaultPage: "home",
|
||||
};
|
||||
|
||||
const settings = localSettings ? JSON.parse(localSettings) : defaultSettings;
|
||||
const settingsKeys = Object.keys(defaultSettings);
|
||||
|
||||
const initialState = {
|
||||
pages: ["home"],
|
||||
...settings,
|
||||
};
|
||||
|
||||
const settingsUpdater = (oldState, settingsUpdate) => {
|
||||
const settingsToUpdate = {};
|
||||
for (var k of settingsKeys) {
|
||||
settingsToUpdate[k] = oldState[k];
|
||||
if (settingsUpdate[k] === undefined) continue;
|
||||
settingsToUpdate[k] = settingsUpdate[k];
|
||||
}
|
||||
localStorage.setItem("settings", JSON.stringify(settingsToUpdate));
|
||||
};
|
||||
|
||||
const reducer = (state, action) => {
|
||||
const { settings } = action;
|
||||
// Actions
|
||||
switch (action.type) {
|
||||
case ACTIONS.UPDATE:
|
||||
settingsUpdater(state, settings);
|
||||
return { ...state, ...settings };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const SettingsProvider = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
const context = {
|
||||
state,
|
||||
dispatch,
|
||||
updateSettings: (settings) => dispatch({ type: ACTIONS.UPDATE, settings }),
|
||||
};
|
||||
const contextValue = useMemo(() => context, [state, dispatch]);
|
||||
|
||||
return (
|
||||
<SettingsContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsContext;
|
85
src/nav/MCLMenu.jsx
Normal file
85
src/nav/MCLMenu.jsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
// React imports
|
||||
import { useState } from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
|
||||
// Internal Imports
|
||||
import pages from "./MCLPages.jsx";
|
||||
|
||||
// Materialui
|
||||
import AppBar from "@mui/material/AppBar";
|
||||
import Box from "@mui/material/Box";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import Drawer from "@mui/material/Drawer";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import List from "@mui/material/List";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
|
||||
const drawerWidth = 250;
|
||||
export default function MCLMenu() {
|
||||
const location = useLocation();
|
||||
const [drawerOpen, setDrawer] = useState(false);
|
||||
|
||||
const toggleDrawer = () => setDrawer(!drawerOpen);
|
||||
const closeDrawer = () => setDrawer(false);
|
||||
|
||||
const navHeader = () => {
|
||||
const name = location.pathname.split("/").pop();
|
||||
const pathStr = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
return pathStr;
|
||||
};
|
||||
|
||||
const drawerIndex = (isDrawer) => (theme) =>
|
||||
theme.zIndex.modal + 2 - (isDrawer ? 1 : 0);
|
||||
|
||||
return (
|
||||
<AppBar position="fixed" sx={{ bgcolor: "black", zIndex: drawerIndex() }}>
|
||||
<Box sx={{ flexGrow: 1, margin: "0 20px" }}>
|
||||
<Toolbar disableGutters>
|
||||
<IconButton
|
||||
size="large"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
sx={{ mr: 2 }}
|
||||
onClick={toggleDrawer}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Drawer
|
||||
open={drawerOpen}
|
||||
onClose={closeDrawer}
|
||||
sx={{ zIndex: drawerIndex(true) }}
|
||||
>
|
||||
<Toolbar />
|
||||
<Box
|
||||
sx={{ width: drawerWidth, overflow: "auto" }}
|
||||
role="presentation"
|
||||
>
|
||||
<List>
|
||||
{pages.map((page, index) => (
|
||||
<ListItemButton
|
||||
key={index}
|
||||
component={Link}
|
||||
to={page.path}
|
||||
selected={location.pathname === page.path}
|
||||
onClick={closeDrawer}
|
||||
>
|
||||
<ListItemIcon>{page.icon}</ListItemIcon>
|
||||
<ListItemText primary={page.name} />
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</Drawer>
|
||||
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
||||
{navHeader()}
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</Box>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
20
src/nav/MCLPages.jsx
Normal file
20
src/nav/MCLPages.jsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import Home from "@mcl/pages/Home.jsx";
|
||||
import Create from "@mcl/pages/Create.jsx";
|
||||
// Go To https://mui.com/material-ui/material-icons/ for more!
|
||||
import HomeIcon from "@mui/icons-material/Home";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
|
||||
export default [
|
||||
{
|
||||
name: "Home",
|
||||
path: "/mcl/home",
|
||||
icon: <HomeIcon />,
|
||||
component: <Home />,
|
||||
},
|
||||
{
|
||||
name: "Create",
|
||||
path: "/mcl/create",
|
||||
icon: <AddIcon />,
|
||||
component: <Create />,
|
||||
},
|
||||
];
|
18
src/nav/MCLPortal.jsx
Normal file
18
src/nav/MCLPortal.jsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
// Import React
|
||||
import { Routes, Route, Navigate } from "react-router-dom";
|
||||
import pages from "./MCLPages.jsx";
|
||||
|
||||
const defaultPage = pages[0].path;
|
||||
|
||||
export default function MCLPortal() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route exact path="/mcl/" element={<Navigate to={defaultPage} />} />
|
||||
<Route exact path="/" element={<Navigate to={defaultPage} />} />
|
||||
{pages.map((p, i) => (
|
||||
<Route key={i} path={p.path} element={p.component} />
|
||||
))}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
16
src/nav/Viewport.jsx
Normal file
16
src/nav/Viewport.jsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import Box from "@mui/material/Box";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import MCLPortal from "./MCLPortal.jsx";
|
||||
// Import Navbar
|
||||
/*import Navbar from "./Navbar.jsx";*/
|
||||
import MCLMenu from "./MCLMenu.jsx";
|
||||
|
||||
export default function Views() {
|
||||
return (
|
||||
<div className="view">
|
||||
<MCLMenu />
|
||||
<Toolbar />
|
||||
<MCLPortal />
|
||||
</div>
|
||||
);
|
||||
}
|
23
src/overview/Overview.jsx
Normal file
23
src/overview/Overview.jsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { useSystemAvailable } from "@mcl/queries";
|
||||
import Box from "@mui/material/Box";
|
||||
import OverviewVisual from "./OverviewVisual.jsx";
|
||||
export default function Overview(props) {
|
||||
const [memory, setMemory] = useState(100);
|
||||
const [cpu, setCpu] = useState(100);
|
||||
const { isLoading: systemLoading, data: systemAvailable } =
|
||||
useSystemAvailable();
|
||||
|
||||
useEffect(() => {
|
||||
if (systemLoading || !props.clusterMetrics) return;
|
||||
setCpu((props.clusterMetrics.cpu / systemAvailable.cpu) * 100);
|
||||
setMemory((props.clusterMetrics.memory / systemAvailable.memory) * 100);
|
||||
}, [systemAvailable, props.clusterMetrics]);
|
||||
|
||||
return (
|
||||
<Box className="overview-toolbar">
|
||||
<OverviewVisual value={cpu} color="warning" label="CPU" />
|
||||
<OverviewVisual value={memory} color="success" label="MEMORY" />
|
||||
</Box>
|
||||
);
|
||||
}
|
44
src/overview/OverviewVisual.jsx
Normal file
44
src/overview/OverviewVisual.jsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Box from "@mui/material/Box";
|
||||
|
||||
export default function OverviewVisual(props) {
|
||||
const { value, color, label } = props;
|
||||
return (
|
||||
<Box className="overview-visual-wrapper">
|
||||
<Box sx={{ position: "relative", display: "inline-flex" }}>
|
||||
<CircularProgress
|
||||
variant="determinate"
|
||||
value={value}
|
||||
color={color}
|
||||
size="6.25rem"
|
||||
className="overview-visual-display"
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
position: "absolute",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4" component="div">
|
||||
{`${Math.round(value)}%`}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="h5"
|
||||
component="div"
|
||||
className="overview-visual-label"
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
152
src/pages/Create.jsx
Normal file
152
src/pages/Create.jsx
Normal file
|
@ -0,0 +1,152 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Select from "@mui/material/Select";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import InputLabel from "@mui/material/InputLabel";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import { useCreateServer, useVersionList } from "@mcl/queries";
|
||||
|
||||
const defaultServer = {
|
||||
version: "latest",
|
||||
name: "example",
|
||||
serverType: "VANILLA",
|
||||
difficulty: "easy",
|
||||
maxPlayers: "20",
|
||||
gamemode: "survival",
|
||||
memory: "1024",
|
||||
motd: "Minecluster Server Hosting",
|
||||
};
|
||||
|
||||
export default function Create() {
|
||||
const [spec, setSpec] = useState(defaultServer);
|
||||
const versionList = useVersionList();
|
||||
const [versions, setVersions] = useState(["latest"]);
|
||||
const createServer = useCreateServer(spec);
|
||||
const updateSpec = (attr, val) => {
|
||||
const s = { ...spec };
|
||||
s[attr] = val;
|
||||
setSpec(s);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!versionList.data) return;
|
||||
setVersions([
|
||||
"latest",
|
||||
...versionList.data.versions
|
||||
.filter(({ type: releaseType }) => releaseType === "release")
|
||||
.map(({ id }) => id),
|
||||
]);
|
||||
}, [versionList.data]);
|
||||
|
||||
const coreUpdate = (attr) => (e) => updateSpec(attr, e.target.value);
|
||||
|
||||
function upsertSpec() {
|
||||
if (validateSpec() !== "validated") return;
|
||||
createServer(spec);
|
||||
}
|
||||
|
||||
function validateSpec() {
|
||||
console.log("TODO CREATE VALIDATION");
|
||||
if (!spec.name) return alertValidationError("Name not included");
|
||||
if (!spec.version) return alertValidationError("Version cannot be blank");
|
||||
if (!spec.url) return alertValidationError("Url cannot be blank");
|
||||
return "validated";
|
||||
}
|
||||
|
||||
function alertValidationError(reason) {
|
||||
alert(`Could not validate spec because: ${reason}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="create">
|
||||
<FormControl fullWidth>
|
||||
<TextField
|
||||
label="Name"
|
||||
onChange={coreUpdate("name")}
|
||||
defaultValue={spec.name}
|
||||
required
|
||||
/>
|
||||
<TextField label="URL" onChange={coreUpdate("url")} required />
|
||||
<TextField
|
||||
label="Version"
|
||||
onChange={coreUpdate("version")}
|
||||
value={spec.version}
|
||||
select
|
||||
required
|
||||
SelectProps={{ MenuProps: { sx: { maxHeight: "12rem" } } }}
|
||||
>
|
||||
{versions.map((v, k) => (
|
||||
<MenuItem value={v} key={k}>
|
||||
{v}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField
|
||||
label="Server Type"
|
||||
onChange={coreUpdate("serverType")}
|
||||
value={spec.serverType}
|
||||
select
|
||||
required
|
||||
>
|
||||
<MenuItem value={"VANILLA"}>Vanilla</MenuItem>
|
||||
<MenuItem value={"FABRIC"}>Fabric</MenuItem>
|
||||
<MenuItem value={"PAPER"}>Paper</MenuItem>
|
||||
<MenuItem value={"SPIGOT"}>Spigot</MenuItem>
|
||||
</TextField>
|
||||
<TextField
|
||||
label="Difficulty"
|
||||
onChange={coreUpdate("difficulty")}
|
||||
value={spec.difficulty}
|
||||
select
|
||||
required
|
||||
>
|
||||
<MenuItem value={"peaceful"}>Peaceful</MenuItem>
|
||||
<MenuItem value={"easy"}>Easy</MenuItem>
|
||||
<MenuItem value={"medium"}>Medium</MenuItem>
|
||||
<MenuItem value={"hard"}>Hard</MenuItem>
|
||||
</TextField>
|
||||
|
||||
<TextField label="Whitelist" onChange={coreUpdate("whitelist")} />
|
||||
<TextField label="Ops" onChange={coreUpdate("ops")} />
|
||||
<TextField label="Icon" onChange={coreUpdate("icon")} required />
|
||||
<TextField
|
||||
label="Max Players"
|
||||
onChange={coreUpdate("maxPlayers")}
|
||||
defaultValue={spec.maxPlayers}
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
label="Gamemode"
|
||||
onChange={coreUpdate("gamemode")}
|
||||
value={spec.gamemode}
|
||||
select
|
||||
required
|
||||
>
|
||||
<MenuItem value={"survival"}>Survival</MenuItem>
|
||||
<MenuItem value={"creative"}>Creative</MenuItem>
|
||||
<MenuItem value={"adventure"}>Adventure</MenuItem>
|
||||
<MenuItem value={"spectator"}>Spectator</MenuItem>
|
||||
</TextField>
|
||||
<TextField label="Seed" onChange={coreUpdate("seed")} />
|
||||
<TextField label="Modpack" onChange={coreUpdate("modpack")} />
|
||||
<TextField
|
||||
label="Memory"
|
||||
onChange={coreUpdate("memory")}
|
||||
defaultValue={spec.memory}
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
label="MOTD"
|
||||
onChange={coreUpdate("motd")}
|
||||
defaultValue={spec.motd}
|
||||
required
|
||||
/>
|
||||
<Button onClick={upsertSpec} variant="contained">
|
||||
Create
|
||||
</Button>
|
||||
</FormControl>
|
||||
</Box>
|
||||
);
|
||||
}
|
51
src/pages/Home.jsx
Normal file
51
src/pages/Home.jsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import ServerCard from "../servers/ServerCard.jsx";
|
||||
import RconDialog, { useRconDialog } from "../servers/RconDialog.jsx";
|
||||
import Overview from "../overview/Overview.jsx";
|
||||
import "@mcl/css/server-card.css";
|
||||
import "@mcl/css/overview.css";
|
||||
import { useServerInstances } from "@mcl/queries";
|
||||
|
||||
export default function Home() {
|
||||
const [server, setServer] = useState();
|
||||
const [servers, setServers] = useState([]);
|
||||
const [rdOpen, rconToggle] = useRconDialog();
|
||||
const { isLoading, data: serversData } = useServerInstances();
|
||||
const { servers: serverInstances, clusterMetrics } = serversData ?? {};
|
||||
useEffect(() => {
|
||||
if (!serverInstances) return;
|
||||
serverInstances.sort((a, b) => a.name.localeCompare(b.name));
|
||||
setServers(serverInstances);
|
||||
}, [serverInstances]);
|
||||
|
||||
const openRcon = (s) => () => {
|
||||
setServer(s);
|
||||
rconToggle();
|
||||
};
|
||||
return (
|
||||
<Box className="home">
|
||||
<Overview clusterMetrics={clusterMetrics} />
|
||||
{!isLoading && servers.length === 0 && (
|
||||
<Box display="flex" alignItems="center" justifyContent="center">
|
||||
<Typography variant="h4" sx={{ textAlign: "center" }}>
|
||||
No servers found!
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<Box className="servers">
|
||||
{!isLoading &&
|
||||
servers.map((s, k) => (
|
||||
<ServerCard key={k} server={s} openRcon={openRcon(s.name)} />
|
||||
))}
|
||||
</Box>
|
||||
<RconDialog
|
||||
keepMounted
|
||||
open={rdOpen}
|
||||
dialogToggle={rconToggle}
|
||||
serverName={server}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
45
src/servers/RconDialog.jsx
Normal file
45
src/servers/RconDialog.jsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { useState } from "react";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Button from "@mui/material/Button";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import RconView from "./RconView.jsx";
|
||||
|
||||
export function useRconDialog(isOpen = false) {
|
||||
const [open, setOpen] = useState(isOpen);
|
||||
const dialogToggle = () => setOpen(!open);
|
||||
return [open, dialogToggle];
|
||||
}
|
||||
|
||||
export default function RconDialog(props) {
|
||||
const { serverName, open, dialogToggle } = props;
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
return (
|
||||
<Dialog
|
||||
sx={
|
||||
fullScreen
|
||||
? {}
|
||||
: { "& .MuiDialog-paper": { width: "80%", maxHeight: 525 } }
|
||||
}
|
||||
maxWidth="xs"
|
||||
open={open}
|
||||
fullScreen={fullScreen}
|
||||
>
|
||||
<Toolbar sx={{ display: { sm: "none" } }} />
|
||||
<DialogTitle>RCON - {serverName}</DialogTitle>
|
||||
<DialogContent>
|
||||
<RconView serverName={serverName} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={dialogToggle}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
27
src/servers/RconSocket.js
Normal file
27
src/servers/RconSocket.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { io } from "socket.io-client";
|
||||
export default class RconSocket {
|
||||
constructor(logUpdate, serverName) {
|
||||
(this.sk = io("/", { query: { serverName } })), (this.logs = []);
|
||||
this.logUpdate = logUpdate;
|
||||
this.sk.on("push", this.onPush.bind(this));
|
||||
this.sk.on("connect", this.onConnect.bind(this));
|
||||
}
|
||||
|
||||
onPush(p) {
|
||||
this.logs = [...this.logs, p];
|
||||
this.logUpdate(this.logs);
|
||||
}
|
||||
|
||||
send(m) {
|
||||
this.sk.emit("msg", m);
|
||||
}
|
||||
|
||||
onConnect() {
|
||||
this.logs = [];
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (!this.sk) return;
|
||||
this.sk.disconnect();
|
||||
}
|
||||
}
|
53
src/servers/RconView.jsx
Normal file
53
src/servers/RconView.jsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { useState, useEffect, useRef } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import RconSocket from "./RconSocket.js";
|
||||
import "@mcl/css/rcon.css";
|
||||
|
||||
export default function RconView(props) {
|
||||
const { serverName } = props;
|
||||
const logsRef = useRef(0);
|
||||
const [cmd, setCmd] = useState("");
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [rcon, setRcon] = useState({});
|
||||
const updateCmd = (e) => setCmd(e.target.value);
|
||||
useEffect(function () {
|
||||
setRcon(new RconSocket(setLogs, serverName));
|
||||
return () => {
|
||||
if (rcon && typeof rcon.disconnect === "function") rcon.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
logsRef.current.scrollTo(0, logsRef.current.scrollHeight);
|
||||
}, [rcon.logs]);
|
||||
|
||||
function sendCommand() {
|
||||
rcon.send(cmd);
|
||||
setCmd("");
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<div className="rconLogsWrapper" ref={logsRef}>
|
||||
{logs.map((v, k) => (
|
||||
<Box key={k}>
|
||||
{v}
|
||||
<br />
|
||||
</Box>
|
||||
))}
|
||||
</div>
|
||||
<Box className="rconActions">
|
||||
<TextField
|
||||
id="outlined-basic"
|
||||
label="Command"
|
||||
variant="outlined"
|
||||
value={cmd}
|
||||
onChange={updateCmd}
|
||||
/>
|
||||
<Button onClick={sendCommand}>Send</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
112
src/servers/ServerCard.jsx
Normal file
112
src/servers/ServerCard.jsx
Normal file
|
@ -0,0 +1,112 @@
|
|||
import React from "react";
|
||||
import { useStartServer, useStopServer, useDeleteServer } from "@mcl/queries";
|
||||
import Box from "@mui/material/Box";
|
||||
import Card from "@mui/material/Card";
|
||||
import CardActions from "@mui/material/CardActions";
|
||||
import CardContent from "@mui/material/CardContent";
|
||||
import Chip from "@mui/material/Chip";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import StopIcon from "@mui/icons-material/Stop";
|
||||
import TerminalIcon from "@mui/icons-material/Terminal";
|
||||
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||
import PendingIcon from "@mui/icons-material/Pending";
|
||||
import DeleteForeverIcon from "@mui/icons-material/DeleteForever";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
|
||||
export default function ServerCard(props) {
|
||||
const { server, openRcon } = props;
|
||||
const { name, metrics, started } = server;
|
||||
const startServer = useStartServer(name);
|
||||
const stopServer = useStopServer(name);
|
||||
const deleteServer = useDeleteServer(name);
|
||||
function toggleRcon() {
|
||||
if (!started) return;
|
||||
openRcon();
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="server-card">
|
||||
<CardContent className="server-card-header server-card-element">
|
||||
<Typography
|
||||
gutterBottom
|
||||
variant="h5"
|
||||
component="div"
|
||||
className="server-card-title"
|
||||
>
|
||||
{name}
|
||||
</Typography>
|
||||
{metrics && (
|
||||
<Box className="server-card-metrics">
|
||||
<Typography
|
||||
gutterBottom
|
||||
variant="body2"
|
||||
component="div"
|
||||
className="server-card-metrics-info"
|
||||
>
|
||||
CPU: {metrics.cpu}
|
||||
</Typography>
|
||||
<Typography
|
||||
gutterBottom
|
||||
variant="body2"
|
||||
component="div"
|
||||
className="server-card-metrics-info"
|
||||
>
|
||||
MEM: {metrics.memory}MB
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<Chip
|
||||
label={started ? "Online" : "Offline"}
|
||||
color={started ? "success" : "error"}
|
||||
className="server-card-status-indicator"
|
||||
/>
|
||||
</CardContent>
|
||||
<div className="server-card-actions-wrapper">
|
||||
<CardActions className="server-card-actions server-card-element">
|
||||
{started && (
|
||||
<IconButton
|
||||
color="error"
|
||||
aria-label="Stop Server"
|
||||
onClick={stopServer}
|
||||
size="large"
|
||||
>
|
||||
<StopIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
{!started && (
|
||||
<IconButton
|
||||
color="success"
|
||||
aria-label="Start Server"
|
||||
onClick={startServer}
|
||||
size="large"
|
||||
>
|
||||
<PlayArrowIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton
|
||||
color="primary"
|
||||
aria-label="RCON"
|
||||
onClick={toggleRcon}
|
||||
size="large"
|
||||
disabled={!started}
|
||||
>
|
||||
<TerminalIcon />
|
||||
</IconButton>
|
||||
<IconButton color="primary" aria-label="Edit" size="large">
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="error"
|
||||
aria-label="Delete Server"
|
||||
onClick={deleteServer}
|
||||
size="large"
|
||||
>
|
||||
<DeleteForeverIcon />
|
||||
</IconButton>
|
||||
</CardActions>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
60
src/util/queries.js
Normal file
60
src/util/queries.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
const fetchApi = (subPath) => async () =>
|
||||
fetch(`/api${subPath}`).then((res) => res.json());
|
||||
|
||||
const fetchApiPost = (subPath, json) => async () =>
|
||||
fetch(`/api${subPath}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(json),
|
||||
}).then((res) => res.json());
|
||||
|
||||
export const useServerStatus = (server) =>
|
||||
useQuery(
|
||||
[`server-status-${server}`],
|
||||
fetchApiPost("/server/status", { name: server })
|
||||
);
|
||||
export const useServerMetrics = (server) =>
|
||||
useQuery(
|
||||
[`server-metrics-${server}`],
|
||||
fetchApiPost("/server/metrics", { name: server }),
|
||||
{ refetchInterval: 10000 }
|
||||
);
|
||||
export const useStartServer = (server) =>
|
||||
postJsonApi("/server/start", { name: server }, "server-instances");
|
||||
export const useStopServer = (server) =>
|
||||
postJsonApi("/server/stop", { name: server }, "server-instances");
|
||||
export const useDeleteServer = (server) =>
|
||||
postJsonApi("/server/delete", { name: server }, "server-instances", "DELETE");
|
||||
export const useCreateServer = (spec) =>
|
||||
postJsonApi("/server/create", spec, "server-list");
|
||||
export const useServerList = () =>
|
||||
useQuery(["server-list"], fetchApi("/server/list"));
|
||||
export const useServerInstances = () =>
|
||||
useQuery(["server-instances"], fetchApi("/server/instances"), {
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
export const useSystemAvailable = () =>
|
||||
useQuery(["system-available"], fetchApi("/system/available"));
|
||||
export const useVersionList = () =>
|
||||
useQuery(["minecraft-versions"], () =>
|
||||
fetch("https://piston-meta.mojang.com/mc/game/version_manifest.json").then(
|
||||
(r) => r.json()
|
||||
)
|
||||
);
|
||||
|
||||
const postJsonApi = (subPath, body, invalidate, method = "POST") => {
|
||||
const qc = useQueryClient();
|
||||
return async () => {
|
||||
const res = await fetch(`/api${subPath}`, {
|
||||
method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
qc.invalidateQueries([invalidate]);
|
||||
};
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue