[FEATURE] Basic System with file manager (#4)
Co-authored-by: dunemask <dunemask@gmail.com> Co-authored-by: Dunemask <dunemask@gmail.com> Reviewed-on: https://gitea.dunemask.dev/elysium/minecluster/pulls/4
This commit is contained in:
parent
8fb5b34c77
commit
4f19cf19d9
62 changed files with 5910 additions and 1190 deletions
10
src/MCL.jsx
10
src/MCL.jsx
|
@ -1,4 +1,6 @@
|
|||
// Imports
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import mclTheme from "./util/theme.js";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { SettingsProvider } from "@mcl/settings";
|
||||
import Viewport from "./nav/Viewport.jsx";
|
||||
|
@ -11,9 +13,11 @@ export default function MCL() {
|
|||
<div className="minecluster">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SettingsProvider>
|
||||
<BrowserRouter>
|
||||
<Viewport />
|
||||
</BrowserRouter>
|
||||
<ThemeProvider theme={mclTheme}>
|
||||
<BrowserRouter>
|
||||
<Viewport />
|
||||
</BrowserRouter>
|
||||
</ThemeProvider>
|
||||
</SettingsProvider>
|
||||
</QueryClientProvider>
|
||||
</div>
|
||||
|
|
42
src/components/files/ChonkyStyledFileBrowser.jsx
Normal file
42
src/components/files/ChonkyStyledFileBrowser.jsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
// ChonkyFullFileBrowser.tsx
|
||||
import { forwardRef, memo } from "react";
|
||||
import {
|
||||
StylesProvider,
|
||||
createGenerateClassName,
|
||||
} from "@material-ui/core/styles";
|
||||
|
||||
import {
|
||||
FileBrowser,
|
||||
FileList,
|
||||
FileContextMenu,
|
||||
FileNavbar,
|
||||
FileToolbar,
|
||||
setChonkyDefaults,
|
||||
FileBrowserHandle,
|
||||
FileBrowserProps,
|
||||
} from "chonky";
|
||||
|
||||
import { ChonkyIconFA } from "chonky-icon-fontawesome";
|
||||
|
||||
setChonkyDefaults({ iconComponent: ChonkyIconFA });
|
||||
|
||||
const muiJSSClassNameGenerator = createGenerateClassName({
|
||||
// Seed property is used to add a prefix classes generated by material ui.
|
||||
seed: "chonky",
|
||||
});
|
||||
|
||||
export default memo(
|
||||
forwardRef((props, ref) => {
|
||||
const { onScroll } = props;
|
||||
return (
|
||||
<StylesProvider generateClassName={muiJSSClassNameGenerator}>
|
||||
<FileBrowser ref={ref} {...props}>
|
||||
<FileNavbar />
|
||||
<FileToolbar />
|
||||
<FileList onScroll={onScroll} />
|
||||
<FileContextMenu />
|
||||
</FileBrowser>
|
||||
</StylesProvider>
|
||||
);
|
||||
}),
|
||||
);
|
146
src/components/files/MineclusterFiles.jsx
Normal file
146
src/components/files/MineclusterFiles.jsx
Normal file
|
@ -0,0 +1,146 @@
|
|||
import { useState, useEffect, useMemo, useRef } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import {
|
||||
FileBrowser,
|
||||
FileContextMenu,
|
||||
FileList,
|
||||
FileNavbar,
|
||||
FileToolbar,
|
||||
setChonkyDefaults,
|
||||
ChonkyActions,
|
||||
} from "chonky";
|
||||
import { ChonkyIconFA } from "chonky-icon-fontawesome";
|
||||
|
||||
import {
|
||||
getServerFiles,
|
||||
createServerFolder,
|
||||
deleteServerItem,
|
||||
getServerItem,
|
||||
} from "@mcl/queries";
|
||||
|
||||
import "@mcl/css/header.css";
|
||||
|
||||
export default function MineclusterFiles(props) {
|
||||
// Chonky configuration
|
||||
setChonkyDefaults({ iconComponent: ChonkyIconFA });
|
||||
const fileActions = useMemo(
|
||||
() => [
|
||||
ChonkyActions.CreateFolder,
|
||||
ChonkyActions.UploadFiles,
|
||||
ChonkyActions.DownloadFiles,
|
||||
ChonkyActions.CopyFiles,
|
||||
ChonkyActions.DeleteFiles,
|
||||
],
|
||||
[],
|
||||
);
|
||||
const { server: serverName } = props;
|
||||
const inputRef = useRef(null);
|
||||
const [dirStack, setDirStack] = useState(["."]);
|
||||
const [files, setFiles] = useState([]);
|
||||
|
||||
const updateFiles = () =>
|
||||
getServerFiles(serverName, dirStack.join("/")).then((f) =>
|
||||
setFiles(f ?? []),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateFiles();
|
||||
}, [dirStack]);
|
||||
|
||||
const getFolderChain = () => {
|
||||
if (dirStack.length === 1) return [{ id: "home", name: "/", isDir: true }];
|
||||
return dirStack.map((d, i) => ({ id: `${d}-${i}`, name: d, isDir: true }));
|
||||
};
|
||||
|
||||
const openParentFolder = () => setDirStack(dirStack.slice(0, -1));
|
||||
|
||||
function openFolder(payload) {
|
||||
const { targetFile: file } = payload;
|
||||
if (!file || !file.isDir) return;
|
||||
setDirStack([...dirStack, file.name]);
|
||||
}
|
||||
|
||||
function createFolder() {
|
||||
const name = prompt("What is the name of the new folder?");
|
||||
const path = [...dirStack, name].join("/");
|
||||
createServerFolder(serverName, path).then(updateFiles);
|
||||
}
|
||||
|
||||
function deleteItems(files) {
|
||||
Promise.all(
|
||||
files.map((f) =>
|
||||
deleteServerItem(serverName, [...dirStack, f.name].join("/"), f.isDir),
|
||||
),
|
||||
)
|
||||
.catch((e) => console.error("Error deleting some files!", e))
|
||||
.then(updateFiles);
|
||||
}
|
||||
|
||||
function uploadFileSelection(e) {
|
||||
if (!e.target.files || e.target.files.length === 0) return;
|
||||
const { files } = e.target;
|
||||
Promise.all([...files].map((f) => uploadFile(f)))
|
||||
.catch((e) => console.log("Error uploading a file", e))
|
||||
.then(updateFiles);
|
||||
}
|
||||
|
||||
async function uploadFile(file) {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("name", serverName);
|
||||
formData.append("path", [...dirStack, name].join("/"));
|
||||
await fetch("/api/files/upload", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadFiles(files) {
|
||||
Promise.all(
|
||||
files.map((f) =>
|
||||
getServerItem(serverName, f.name, [...dirStack, f.name].join("/")),
|
||||
),
|
||||
)
|
||||
.then(() => console.log("Done"))
|
||||
.catch((e) => console.error("Error Downloading files!", e));
|
||||
}
|
||||
|
||||
function fileClick(chonkyEvent) {
|
||||
const { id: clickEvent, payload } = chonkyEvent;
|
||||
console.log(chonkyEvent);
|
||||
if (clickEvent === "open_parent_folder") return openParentFolder();
|
||||
if (clickEvent === "create_folder") return createFolder();
|
||||
if (clickEvent === "upload_files") return inputRef.current.click();
|
||||
if (clickEvent === "download_files")
|
||||
return downloadFiles(chonkyEvent.state.selectedFilesForAction);
|
||||
if (clickEvent === "delete_files")
|
||||
return deleteItems(chonkyEvent.state.selectedFilesForAction);
|
||||
if (clickEvent !== "open_files") return console.log(clickEvent);
|
||||
openFolder(payload);
|
||||
}
|
||||
return (
|
||||
<Box className="minecluster-files" sx={{ height: "calc(100vh - 6rem)" }}>
|
||||
<input
|
||||
type="file"
|
||||
id="file"
|
||||
ref={inputRef}
|
||||
style={{ display: "none" }}
|
||||
onChange={uploadFileSelection}
|
||||
multiple
|
||||
/>
|
||||
|
||||
<FileBrowser
|
||||
files={files}
|
||||
folderChain={getFolderChain()}
|
||||
onFileAction={fileClick}
|
||||
fileActions={fileActions}
|
||||
darkMode={true}
|
||||
>
|
||||
<FileNavbar />
|
||||
<FileToolbar />
|
||||
<FileList />
|
||||
<FileContextMenu />
|
||||
</FileBrowser>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -14,6 +14,8 @@ 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";
|
||||
import FolderIcon from "@mui/icons-material/Folder";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export default function ServerCard(props) {
|
||||
const { server, openRcon } = props;
|
||||
|
@ -94,9 +96,24 @@ export default function ServerCard(props) {
|
|||
>
|
||||
<TerminalIcon />
|
||||
</IconButton>
|
||||
<IconButton color="primary" aria-label="Edit" size="large">
|
||||
<IconButton
|
||||
color="primary"
|
||||
aria-label="Edit"
|
||||
size="large"
|
||||
component={Link}
|
||||
to={`/mcl/edit?server=${name}`}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="primary"
|
||||
aria-label="Files"
|
||||
size="large"
|
||||
component={Link}
|
||||
to={`/mcl/files?server=${name}`}
|
||||
>
|
||||
<FolderIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="error"
|
||||
aria-label="Delete Server"
|
31
src/css/header.css
Normal file
31
src/css/header.css
Normal file
|
@ -0,0 +1,31 @@
|
|||
.appbar-items {
|
||||
font-size: 1.25rem;
|
||||
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
|
||||
font-weight: 500;
|
||||
line-height: 1.6;
|
||||
letter-spacing: 0.0075em;
|
||||
}
|
||||
|
||||
.view > header {
|
||||
transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
|
||||
box-shadow:
|
||||
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
|
||||
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
|
||||
0px 1px 10px 0px rgba(0, 0, 0, 0.12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: auto;
|
||||
right: 0;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
z-index: 1302;
|
||||
background-color: black;
|
||||
}
|
||||
.view > header > div > div > a {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
20
src/css/overview.css
Normal file
20
src/css/overview.css
Normal file
|
@ -0,0 +1,20 @@
|
|||
.overview-toolbar {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
height: 230px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.overview-visual-display {
|
||||
display: flex;
|
||||
background-color: rgba(223, 223, 223, 0.5);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.overview-visual-label {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.overview-visual-wrapper {
|
||||
margin: 1rem;
|
||||
}
|
9
src/css/rcon.css
Normal file
9
src/css/rcon.css
Normal file
|
@ -0,0 +1,9 @@
|
|||
.rconLogsWrapper {
|
||||
overflow-y: scroll;
|
||||
max-height: 20rem;
|
||||
word-wrap: break-word;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.rconActions {
|
||||
display: inline-flex;
|
||||
}
|
82
src/css/server-card.css
Normal file
82
src/css/server-card.css
Normal file
|
@ -0,0 +1,82 @@
|
|||
.servers {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.server-card {
|
||||
width: 400px;
|
||||
min-height: 228px;
|
||||
min-width: 250px;
|
||||
max-width: 400px;
|
||||
margin: 15px;
|
||||
background-image: url("/images/server-backdrop.png");
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.server-card-header {
|
||||
padding: 0px;
|
||||
display: inline-flex;
|
||||
max-height: 32px;
|
||||
height: 100%;
|
||||
font-weight: bold;
|
||||
text-transform: capitalize;
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
rgba(255, 255, 255, 1),
|
||||
rgba(255, 255, 255, 0.1)
|
||||
);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.server-card-title {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.server-card-status-indicator {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.server-card-actions-wrapper {
|
||||
margin-top: auto;
|
||||
justify-content: end;
|
||||
width: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.server-card-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.server-card-action {
|
||||
position: relative;
|
||||
height: 20%;
|
||||
}
|
||||
|
||||
.server-card-element {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.server-card-metrics {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.server-card-metrics-info {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
|
@ -13,7 +13,7 @@ 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 HomeIcon from "@mui/icons-material/Home";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import List from "@mui/material/List";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
|
@ -36,48 +36,23 @@ export default function MCLMenu() {
|
|||
theme.zIndex.modal + 2 - (isDrawer ? 1 : 0);
|
||||
|
||||
return (
|
||||
<AppBar position="fixed" sx={{ bgcolor: "black", zIndex: drawerIndex() }}>
|
||||
<Box sx={{ flexGrow: 1, margin: "0 20px" }}>
|
||||
<AppBar
|
||||
position="fixed"
|
||||
color="primary"
|
||||
sx={{ zIndex: drawerIndex(), bgcolor: "black" }}
|
||||
enableColorOnDark={true}
|
||||
>
|
||||
<Box
|
||||
sx={{ flexGrow: 1, margin: "0 20px", color: "white" }}
|
||||
className="appbar-items"
|
||||
>
|
||||
<Toolbar disableGutters>
|
||||
<IconButton
|
||||
size="large"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
sx={{ mr: 2 }}
|
||||
onClick={toggleDrawer}
|
||||
>
|
||||
<MenuIcon />
|
||||
<IconButton component={Link} to="/" color="inherit">
|
||||
<HomeIcon />
|
||||
</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 }}>
|
||||
<span style={{ margin: "auto 0", color: "inherit" }}>
|
||||
{navHeader()}
|
||||
</Typography>
|
||||
</span>
|
||||
</Toolbar>
|
||||
</Box>
|
||||
</AppBar>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Home from "@mcl/pages/Home.jsx";
|
||||
import Create from "@mcl/pages/Create.jsx";
|
||||
import Files from "@mcl/pages/Files.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";
|
||||
|
@ -17,4 +18,10 @@ export default [
|
|||
icon: <AddIcon />,
|
||||
component: <Create />,
|
||||
},
|
||||
{
|
||||
name: "Edit",
|
||||
path: "/mcl/files",
|
||||
icon: <AddIcon />,
|
||||
component: <Files />,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import Box from "@mui/material/Box";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import MCLPortal from "./MCLPortal.jsx";
|
||||
import Button from "@mui/material/Button";
|
||||
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
|
||||
// Import Navbar
|
||||
/*import Navbar from "./Navbar.jsx";*/
|
||||
import MCLMenu from "./MCLMenu.jsx";
|
||||
|
|
|
@ -1,152 +1,12 @@
|
|||
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",
|
||||
};
|
||||
|
||||
import CreateOptions from "./CreateOptions.jsx";
|
||||
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>
|
||||
{/*<CreateMenu />*/}
|
||||
<Box className="create-wrapper" sx={{ display: "flex" }}>
|
||||
<CreateOptions />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
248
src/pages/CreateOptions.jsx
Normal file
248
src/pages/CreateOptions.jsx
Normal file
|
@ -0,0 +1,248 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Chip from "@mui/material/Chip";
|
||||
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",
|
||||
serverType: "VANILLA",
|
||||
difficulty: "easy",
|
||||
maxPlayers: "5",
|
||||
gamemode: "survival",
|
||||
memory: "512",
|
||||
motd: `\\u00A7e\\u00A7ka\\u00A7l\\u00A7aMine\\u00A76Cluster\\u00A7r\\u00A78\\u00A7b\\u00A7ka`,
|
||||
};
|
||||
|
||||
export default function Create() {
|
||||
const [wl, setWl] = useState([]);
|
||||
const [ops, setOps] = useState([]);
|
||||
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);
|
||||
console.log(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 opsAdd(e) {
|
||||
const opEntry = e.target.innerHTML ?? e.target.value;
|
||||
if (!opEntry) return;
|
||||
const newOps = [...ops, opEntry];
|
||||
setOps(newOps);
|
||||
updateSpec("ops", newOps.join(","));
|
||||
}
|
||||
|
||||
function whitelistAdd(e) {
|
||||
const wlEntry = e.target.value;
|
||||
if (!wlEntry) return;
|
||||
const newWl = [...wl, wlEntry];
|
||||
setWl(newWl);
|
||||
updateSpec("whitelist", newWl.join(","));
|
||||
}
|
||||
|
||||
const opsRemove =
|
||||
(name, { onDelete: updateAutoComplete }) =>
|
||||
(e) => {
|
||||
updateAutoComplete(e);
|
||||
const newOps = [...ops];
|
||||
const entryIndex = newOps.indexOf(name);
|
||||
if (entryIndex === -1) return;
|
||||
newOps.splice(entryIndex, 1);
|
||||
setOps(newOps);
|
||||
updateSpec("ops", newOps.join(","));
|
||||
};
|
||||
|
||||
const whitelistRemove =
|
||||
(name, { onDelete: updateAutocomplete }) =>
|
||||
(e) => {
|
||||
updateAutocomplete(e);
|
||||
const newWl = [...wl];
|
||||
const entryIndex = newWl.indexOf(name);
|
||||
if (entryIndex === -1) return;
|
||||
newWl.splice(entryIndex, 1);
|
||||
setWl(newWl);
|
||||
updateSpec("whitelist", newWl.join(","));
|
||||
};
|
||||
|
||||
const opUpdate = (e) => alert("Op not implimented");
|
||||
|
||||
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.host) return alertValidationError("Host cannot be blank");
|
||||
return "validated";
|
||||
}
|
||||
|
||||
function alertValidationError(reason) {
|
||||
alert(`Could not validate spec because: ${reason}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="create-options"
|
||||
sx={{ width: "100%", maxWidth: "600px", margin: "auto" }}
|
||||
>
|
||||
<FormControl fullWidth sx={{ mt: "2rem", display: "flex", gap: ".5rem" }}>
|
||||
<TextField
|
||||
label="Name"
|
||||
onChange={coreUpdate("name")}
|
||||
helperText="Example: My Survival World"
|
||||
defaultValue={spec.name}
|
||||
FormHelperTextProps={{ sx: { ml: 0 } }}
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
label="Host"
|
||||
onChange={coreUpdate("host")}
|
||||
helperText="Example: host.mc.example.com"
|
||||
FormHelperTextProps={{ sx: { ml: 0 } }}
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
label="Version"
|
||||
onChange={coreUpdate("version")}
|
||||
value={spec.version}
|
||||
select
|
||||
required
|
||||
SelectProps={{ MenuProps: { sx: { maxHeight: "20rem" } } }}
|
||||
>
|
||||
{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>
|
||||
|
||||
<Autocomplete
|
||||
multiple
|
||||
id="whitelist-autocomplete"
|
||||
options={[]}
|
||||
onChange={whitelistAdd}
|
||||
freeSolo
|
||||
renderInput={(p) => <TextField {...p} label="Whitelist" />}
|
||||
renderTags={(value, getTagProps) =>
|
||||
value.map((option, index) => {
|
||||
const defaultChipProps = getTagProps({ index });
|
||||
return (
|
||||
<Chip
|
||||
label={option}
|
||||
{...defaultChipProps}
|
||||
onDelete={whitelistRemove(option, defaultChipProps)}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Autocomplete
|
||||
filterSelectedOptions={true}
|
||||
multiple
|
||||
id="ops-autocomplete"
|
||||
options={wl}
|
||||
onChange={opsAdd}
|
||||
renderInput={(p) => <TextField {...p} label="Ops" />}
|
||||
renderTags={(value, getTagProps) =>
|
||||
value.map((option, index) => {
|
||||
const defaultChipProps = getTagProps({ index });
|
||||
return (
|
||||
<Chip
|
||||
label={option}
|
||||
{...defaultChipProps}
|
||||
onDelete={opsRemove(option, defaultChipProps)}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
/>
|
||||
{/*<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>
|
||||
);
|
||||
}
|
20
src/pages/Files.jsx
Normal file
20
src/pages/Files.jsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { useEffect } from "react";
|
||||
import { useSearchParams, useNavigate } from "react-router-dom";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import MineclusterFiles from "@mcl/components/files/MineclusterFiles.jsx";
|
||||
|
||||
export default function Files() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const currentServer = searchParams.get("server");
|
||||
const nav = useNavigate();
|
||||
useEffect(() => {
|
||||
if (!currentServer) nav("/");
|
||||
}, [currentServer]);
|
||||
return (
|
||||
<Box className="edit" sx={{ height: "100%" }}>
|
||||
<MineclusterFiles server={currentServer} />
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -1,19 +1,25 @@
|
|||
import { Link } from "react-router-dom";
|
||||
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 ServerCard from "@mcl/components/servers/ServerCard.jsx";
|
||||
import RconDialog, {
|
||||
useRconDialog,
|
||||
} from "@mcl/components/servers/RconDialog.jsx";
|
||||
import Overview from "@mcl/components/overview/Overview.jsx";
|
||||
import Button from "@mui/material/Button";
|
||||
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
|
||||
import "@mcl/css/server-card.css";
|
||||
import "@mcl/css/overview.css";
|
||||
import { useServerInstances } from "@mcl/queries";
|
||||
|
||||
export default function Home() {
|
||||
const clusterMetrics = { cpu: 0, memory: 0 };
|
||||
const [server, setServer] = useState();
|
||||
const [servers, setServers] = useState([]);
|
||||
const [rdOpen, rconToggle] = useRconDialog();
|
||||
const { isLoading, data: serversData } = useServerInstances();
|
||||
const { servers: serverInstances, clusterMetrics } = serversData ?? {};
|
||||
const serverInstances = serversData ?? [];
|
||||
useEffect(() => {
|
||||
if (!serverInstances) return;
|
||||
serverInstances.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
@ -53,6 +59,23 @@ export default function Home() {
|
|||
dialogToggle={rconToggle}
|
||||
serverName={server}
|
||||
/>
|
||||
<Button
|
||||
component={Link}
|
||||
to="/mcl/create"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
padding: "1rem",
|
||||
borderRadius: "100%",
|
||||
height: "4rem",
|
||||
width: "4rem",
|
||||
}}
|
||||
>
|
||||
<SpeedDialIcon />
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,15 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|||
const fetchApi = (subPath) => async () =>
|
||||
fetch(`/api${subPath}`).then((res) => res.json());
|
||||
|
||||
const fetchApiCore = async (subPath, json, method = "POST", jsonify = false) =>
|
||||
fetch(`/api${subPath}`, {
|
||||
method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(json),
|
||||
}).then((res) => (jsonify ? res.json() : res));
|
||||
|
||||
const fetchApiPost = (subPath, json) => async () =>
|
||||
fetch(`/api${subPath}`, {
|
||||
method: "POST",
|
||||
|
@ -12,16 +21,16 @@ const fetchApiPost = (subPath, json) => async () =>
|
|||
}).then((res) => res.json());
|
||||
|
||||
export const useServerStatus = (server) =>
|
||||
useQuery(
|
||||
[`server-status-${server}`],
|
||||
fetchApiPost("/server/status", { name: server })
|
||||
);
|
||||
useQuery({
|
||||
queryKey: [`server-status-${server}`],
|
||||
queryFn: fetchApiPost("/server/status", { name: server }),
|
||||
});
|
||||
export const useServerMetrics = (server) =>
|
||||
useQuery(
|
||||
[`server-metrics-${server}`],
|
||||
fetchApiPost("/server/metrics", { name: server }),
|
||||
{ refetchInterval: 10000 }
|
||||
);
|
||||
useQuery({
|
||||
queryKey: [`server-metrics-${server}`],
|
||||
queryFn: fetchApiPost("/server/metrics", { name: server }),
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
export const useStartServer = (server) =>
|
||||
postJsonApi("/server/start", { name: server }, "server-instances");
|
||||
export const useStopServer = (server) =>
|
||||
|
@ -30,20 +39,61 @@ export const useDeleteServer = (server) =>
|
|||
postJsonApi("/server/delete", { name: server }, "server-instances", "DELETE");
|
||||
export const useCreateServer = (spec) =>
|
||||
postJsonApi("/server/create", spec, "server-list");
|
||||
|
||||
export const getServerFiles = async (server, path) =>
|
||||
fetchApiCore("/files/list", { name: server, path }, "POST", true);
|
||||
export const createServerFolder = async (server, path) =>
|
||||
fetchApiCore("/files/folder", {
|
||||
name: server,
|
||||
path,
|
||||
}); /*postJsonApi("/files/folder", {name: server, path});*/
|
||||
export const deleteServerItem = async (server, path, isDir) =>
|
||||
fetchApiCore("/files/item", { name: server, path, isDir }, "DELETE");
|
||||
|
||||
export const getServerItem = async (server, name, path) =>
|
||||
fetchApiCore("/files/item", { name: server, path })
|
||||
.then((resp) =>
|
||||
resp.status === 200
|
||||
? resp.blob()
|
||||
: Promise.reject("something went wrong"),
|
||||
)
|
||||
.then((blob) => {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.style.display = "none";
|
||||
a.href = url;
|
||||
a.download = name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
export const useInvalidator = () => {
|
||||
const qc = useQueryClient();
|
||||
return (q) => qc.invalidateQueries([q]);
|
||||
};
|
||||
|
||||
export const useServerList = () =>
|
||||
useQuery(["server-list"], fetchApi("/server/list"));
|
||||
useQuery({ queryKey: ["server-list"], queryFn: fetchApi("/server/list") });
|
||||
export const useServerInstances = () =>
|
||||
useQuery(["server-instances"], fetchApi("/server/instances"), {
|
||||
useQuery({
|
||||
queryKey: ["server-instances"],
|
||||
queryFn: fetchApi("/server/instances"),
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
export const useSystemAvailable = () =>
|
||||
useQuery(["system-available"], fetchApi("/system/available"));
|
||||
useQuery({
|
||||
queryKey: ["system-available"],
|
||||
queryFn: fetchApi("/system/available"),
|
||||
});
|
||||
export const useVersionList = () =>
|
||||
useQuery(["minecraft-versions"], () =>
|
||||
fetch("https://piston-meta.mojang.com/mc/game/version_manifest.json").then(
|
||||
(r) => r.json()
|
||||
)
|
||||
);
|
||||
useQuery({
|
||||
queryKey: ["minecraft-versions"],
|
||||
queryFn: () =>
|
||||
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();
|
||||
|
@ -55,6 +105,7 @@ const postJsonApi = (subPath, body, invalidate, method = "POST") => {
|
|||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
qc.invalidateQueries([invalidate]);
|
||||
if (invalidate) qc.invalidateQueries([invalidate]);
|
||||
return res.json();
|
||||
};
|
||||
};
|
||||
|
|
16
src/util/theme.js
Normal file
16
src/util/theme.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
// Generated using https://zenoo.github.io/mui-theme-creator/
|
||||
import { createTheme } from "@mui/material/styles";
|
||||
|
||||
const themeOptions = {
|
||||
palette: {
|
||||
mode: "light",
|
||||
primary: {
|
||||
main: "rgba(109,216,144,255)",
|
||||
},
|
||||
secondary: {
|
||||
main: "#f50057",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default createTheme(themeOptions);
|
Loading…
Add table
Add a link
Reference in a new issue