[FEATURE] Migrated to new loading sequence (#6)
Co-authored-by: Dunemask <dunemask@gmail.com> Reviewed-on: https://gitea.dunemask.dev/elysium/minecluster/pulls/6
This commit is contained in:
parent
fb57c03ba7
commit
6eb4ed3e95
53 changed files with 1349 additions and 449 deletions
74
src/components/files/FilePreview.jsx
Normal file
74
src/components/files/FilePreview.jsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { useState, useEffect } 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";
|
||||
|
||||
const textFileTypes = ["properties", "txt", "yaml", "yml", "json", "env"];
|
||||
const imageFileTypes = ["png", "jpeg", "jpg"];
|
||||
|
||||
export const supportedFileTypes = [...textFileTypes, ...imageFileTypes];
|
||||
|
||||
export function useFilePreview(isOpen = false) {
|
||||
const [open, setOpen] = useState(isOpen);
|
||||
const dialogToggle = () => setOpen(!open);
|
||||
return [open, dialogToggle];
|
||||
}
|
||||
|
||||
function TextPreview(props) {
|
||||
const { fileText } = props;
|
||||
return <div style={{ whiteSpace: "break-spaces" }}>{fileText}</div>;
|
||||
}
|
||||
|
||||
export default function FilePreview(props) {
|
||||
const [fileText, setFileText] = useState();
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down("md"));
|
||||
|
||||
const { previewData, open, dialogToggle } = props;
|
||||
const { fileData, name } = previewData ?? {};
|
||||
const ext = name ? name.split(".").pop() : null;
|
||||
const isTextFile = textFileTypes.includes(ext);
|
||||
|
||||
async function onPreviewChange() {
|
||||
if (isTextFile) setFileText(await fileData.text());
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
onPreviewChange();
|
||||
}, [fileData]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
sx={
|
||||
fullScreen
|
||||
? {}
|
||||
: {
|
||||
"& .mcl-MuiDialog-paper": {
|
||||
width: "100%",
|
||||
maxHeight: 525,
|
||||
maxWidth: "80%",
|
||||
},
|
||||
}
|
||||
}
|
||||
maxWidth="xs"
|
||||
open={open}
|
||||
fullScreen={fullScreen}
|
||||
>
|
||||
<Toolbar sx={{ display: { sm: "none" } }} />
|
||||
<DialogTitle>{name}</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextPreview fileText={fileText} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={dialogToggle}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
|
@ -17,8 +17,9 @@ import {
|
|||
deleteServerItem,
|
||||
getServerItem,
|
||||
} from "@mcl/queries";
|
||||
import { previewServerItem } from "../../util/queries";
|
||||
|
||||
import "@mcl/css/header.css";
|
||||
import { supportedFileTypes } from "./FilePreview.jsx";
|
||||
|
||||
export default function MineclusterFiles(props) {
|
||||
// Chonky configuration
|
||||
|
@ -33,17 +34,23 @@ export default function MineclusterFiles(props) {
|
|||
],
|
||||
[],
|
||||
);
|
||||
const { server: serverName } = props;
|
||||
const { server: serverId, changePreview } = props;
|
||||
const inputRef = useRef(null);
|
||||
const [dirStack, setDirStack] = useState(["."]);
|
||||
const [files, setFiles] = useState([]);
|
||||
|
||||
const updateFiles = () => {
|
||||
const dir = dirStack.join("/");
|
||||
getServerFiles(serverName, dir).then((f) => {
|
||||
const files = f.map((fi) => ({ ...fi, id: `${dir}/${fi.name}` }));
|
||||
setFiles(files ?? []);
|
||||
});
|
||||
getServerFiles(serverId, dir)
|
||||
.then((f) => {
|
||||
const files = f.map((fi) => ({ ...fi, id: `${dir}/${fi.name}` }));
|
||||
setFiles(files ?? []);
|
||||
})
|
||||
.catch(() =>
|
||||
console.error(
|
||||
"Couldn't update files, server likely hasn't started yet",
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -61,22 +68,25 @@ export default function MineclusterFiles(props) {
|
|||
|
||||
const openParentFolder = () => setDirStack(dirStack.slice(0, -1));
|
||||
|
||||
function openFolder(payload) {
|
||||
function openItem(payload) {
|
||||
const { targetFile: file } = payload;
|
||||
if (file && file.isDir) return setDirStack(file.id.split("/"));
|
||||
if (file && !file.isDir) return downloadFiles([file]);
|
||||
if (!file || file.isDir) return; // Ensure file exists or is dir
|
||||
if (supportedFileTypes.includes(file.name.split(".").pop()))
|
||||
return previewFile(file);
|
||||
return downloadFiles([file]);
|
||||
}
|
||||
|
||||
function createFolder() {
|
||||
const name = prompt("What is the name of the new folder?");
|
||||
const path = [...dirStack, name].join("/");
|
||||
createServerFolder(serverName, path).then(updateFiles);
|
||||
createServerFolder(serverId, path).then(updateFiles);
|
||||
}
|
||||
|
||||
function deleteItems(files) {
|
||||
Promise.all(
|
||||
files.map((f) =>
|
||||
deleteServerItem(serverName, [...dirStack, f.name].join("/"), f.isDir),
|
||||
deleteServerItem(serverId, [...dirStack, f.name].join("/"), f.isDir),
|
||||
),
|
||||
)
|
||||
.catch((e) => console.error("Error deleting some files!", e))
|
||||
|
@ -94,7 +104,7 @@ export default function MineclusterFiles(props) {
|
|||
async function uploadFile(file) {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("name", serverName);
|
||||
formData.append("id", serverId);
|
||||
formData.append("path", [...dirStack, name].join("/"));
|
||||
await fetch("/api/files/upload", {
|
||||
method: "POST",
|
||||
|
@ -105,13 +115,20 @@ export default function MineclusterFiles(props) {
|
|||
async function downloadFiles(files) {
|
||||
Promise.all(
|
||||
files.map((f) =>
|
||||
getServerItem(serverName, f.name, [...dirStack, f.name].join("/")),
|
||||
getServerItem(serverId, f.name, [...dirStack, f.name].join("/")),
|
||||
),
|
||||
)
|
||||
.then(() => console.log("Done downloading files!"))
|
||||
.catch((e) => console.error("Error Downloading files!", e));
|
||||
}
|
||||
|
||||
function previewFile(file) {
|
||||
const { name } = file;
|
||||
previewServerItem(serverId, [...dirStack, name].join("/")).then(
|
||||
(fileData) => changePreview(name, fileData),
|
||||
);
|
||||
}
|
||||
|
||||
function fileClick(chonkyEvent) {
|
||||
const { id: clickEvent, payload } = chonkyEvent;
|
||||
if (clickEvent === "open_parent_folder") return openParentFolder();
|
||||
|
@ -122,7 +139,7 @@ export default function MineclusterFiles(props) {
|
|||
if (clickEvent === "delete_files")
|
||||
return deleteItems(chonkyEvent.state.selectedFilesForAction);
|
||||
if (clickEvent !== "open_files") return; // console.log(clickEvent);
|
||||
openFolder(payload);
|
||||
openItem(payload);
|
||||
}
|
||||
return (
|
||||
<Box className="minecluster-files" sx={{ height: "calc(100vh - 6rem)" }}>
|
||||
|
@ -134,7 +151,6 @@ export default function MineclusterFiles(props) {
|
|||
onChange={uploadFileSelection}
|
||||
multiple
|
||||
/>
|
||||
|
||||
<FileBrowser
|
||||
files={files}
|
||||
folderChain={getFolderChain()}
|
||||
|
@ -144,6 +160,7 @@ export default function MineclusterFiles(props) {
|
|||
>
|
||||
<FileNavbar />
|
||||
<FileToolbar />
|
||||
|
||||
<FileList />
|
||||
<FileContextMenu />
|
||||
</FileBrowser>
|
||||
|
|
15
src/components/server-options/BackupBucketOption.jsx
Normal file
15
src/components/server-options/BackupBucketOption.jsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import TextField from "@mui/material/TextField";
|
||||
export default function BackupBucketOption(props) {
|
||||
const { value, onChange } = props;
|
||||
|
||||
return (
|
||||
<TextField
|
||||
label="Bucket Path"
|
||||
onChange={onChange}
|
||||
defaultValue={value}
|
||||
helperText="Example: /minecraft-backups/example-backups"
|
||||
FormHelperTextProps={{ sx: { ml: 0 } }}
|
||||
required
|
||||
/>
|
||||
);
|
||||
}
|
14
src/components/server-options/BackupHostOption.jsx
Normal file
14
src/components/server-options/BackupHostOption.jsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import TextField from "@mui/material/TextField";
|
||||
export default function BackupHostOption(props) {
|
||||
const { onChange } = props;
|
||||
|
||||
return (
|
||||
<TextField
|
||||
label="Backup Host"
|
||||
onChange={onChange}
|
||||
helperText="Example: s3.mydomain.com"
|
||||
FormHelperTextProps={{ sx: { ml: 0 } }}
|
||||
required
|
||||
/>
|
||||
);
|
||||
}
|
14
src/components/server-options/BackupIdOption.jsx
Normal file
14
src/components/server-options/BackupIdOption.jsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import TextField from "@mui/material/TextField";
|
||||
export default function BackupIdOption(props) {
|
||||
const { onChange } = props;
|
||||
|
||||
return (
|
||||
<TextField
|
||||
label="S3 Access Key ID"
|
||||
onChange={onChange}
|
||||
helperText="Example: s3-access-key-id"
|
||||
FormHelperTextProps={{ sx: { ml: 0 } }}
|
||||
required
|
||||
/>
|
||||
);
|
||||
}
|
55
src/components/server-options/BackupIntervalOption.jsx
Normal file
55
src/components/server-options/BackupIntervalOption.jsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { useState } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import TextField from "@mui/material/TextField";
|
||||
|
||||
const backupIntervalStepDisplay = ["Minutes", "Hours", "Days"];
|
||||
export const backupIntervalDefault = "1d";
|
||||
export const backupIntervalStepOptions = ["m", "h", "d"];
|
||||
export default function BackupIntervalOption(props) {
|
||||
const { onChange } = props;
|
||||
const [interval, setInterval] = useState(1);
|
||||
const [intervalStep, setIntervalStep] = useState(
|
||||
backupIntervalStepOptions[2],
|
||||
);
|
||||
|
||||
const changeStep = (e) => {
|
||||
setIntervalStep(e.target.value);
|
||||
onChange({ target: { value: `${interval}${e.target.value}` } });
|
||||
};
|
||||
|
||||
const changeInterval = (e) => {
|
||||
setInterval(e.target.value);
|
||||
onChange({ target: { value: `${e.target.value}${intervalStep}` } });
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<TextField
|
||||
label="Backup Interval"
|
||||
sx={{ width: "70%" }}
|
||||
value={interval}
|
||||
onChange={changeInterval}
|
||||
helperText="Examples: 1m, 3h, 3.5d"
|
||||
FormHelperTextProps={{ sx: { ml: 0 } }}
|
||||
type="number"
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
label="Step"
|
||||
sx={{ width: "30%", minWidth: "4rem" }}
|
||||
onChange={onChange}
|
||||
value={intervalStep}
|
||||
select
|
||||
required
|
||||
SelectProps={{ MenuProps: { sx: { maxHeight: "20rem" } } }}
|
||||
>
|
||||
{backupIntervalStepOptions.map((o, i) => (
|
||||
<MenuItem value={o} key={i}>
|
||||
{backupIntervalStepDisplay[i]}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Box>
|
||||
);
|
||||
}
|
14
src/components/server-options/BackupKeyOption.jsx
Normal file
14
src/components/server-options/BackupKeyOption.jsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import TextField from "@mui/material/TextField";
|
||||
export default function BackupKeyOption(props) {
|
||||
const { onChange } = props;
|
||||
|
||||
return (
|
||||
<TextField
|
||||
label="S3 Access Key"
|
||||
onChange={onChange}
|
||||
helperText="Example: s3-access-key"
|
||||
FormHelperTextProps={{ sx: { ml: 0 } }}
|
||||
required
|
||||
/>
|
||||
);
|
||||
}
|
26
src/components/server-options/CpuOption.jsx
Normal file
26
src/components/server-options/CpuOption.jsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import TextField from "@mui/material/TextField";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
|
||||
const maxCpuSupported = 8;
|
||||
export const cpuOptions = new Array(2 * maxCpuSupported)
|
||||
.fill(0)
|
||||
.map((v, i) => (i + 1) * 0.5);
|
||||
|
||||
export default function CpuOption(props) {
|
||||
const { value, onChange } = props;
|
||||
return (
|
||||
<TextField
|
||||
label="CPU"
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
select
|
||||
required
|
||||
SelectProps={{ MenuProps: { sx: { maxHeight: "20rem" } } }}
|
||||
disabled // TODO: Enable on backend support
|
||||
>
|
||||
{cpuOptions.map((o, i) => (
|
||||
<MenuItem value={o} key={i}>{`${o} CPU`}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
);
|
||||
}
|
14
src/components/server-options/HostOption.jsx
Normal file
14
src/components/server-options/HostOption.jsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import TextField from "@mui/material/TextField";
|
||||
export default function HostOption(props) {
|
||||
const { onChange } = props;
|
||||
|
||||
return (
|
||||
<TextField
|
||||
label="Host"
|
||||
onChange={onChange}
|
||||
helperText="Example: host.mydomain.com"
|
||||
FormHelperTextProps={{ sx: { ml: 0 } }}
|
||||
required
|
||||
/>
|
||||
);
|
||||
}
|
24
src/components/server-options/MemoryOption.jsx
Normal file
24
src/components/server-options/MemoryOption.jsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import TextField from "@mui/material/TextField";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
const maxMemSupported = 10;
|
||||
export const memoryOptions = new Array(2 * maxMemSupported)
|
||||
.fill(0)
|
||||
.map((v, i) => (i + 1) * 512);
|
||||
|
||||
export default function Option(props) {
|
||||
const { value, onChange } = props;
|
||||
return (
|
||||
<TextField
|
||||
label="Memory"
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
select
|
||||
required
|
||||
SelectProps={{ MenuProps: { sx: { maxHeight: "20rem" } } }}
|
||||
>
|
||||
{memoryOptions.map((o, i) => (
|
||||
<MenuItem value={o} key={i}>{`${o / 1024} Gi`}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
);
|
||||
}
|
14
src/components/server-options/NameOption.jsx
Normal file
14
src/components/server-options/NameOption.jsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import TextField from "@mui/material/TextField";
|
||||
export default function NameOption(props) {
|
||||
const { onChange } = props;
|
||||
|
||||
return (
|
||||
<TextField
|
||||
label="Name"
|
||||
onChange={onChange}
|
||||
helperText="Example: My Survival World"
|
||||
FormHelperTextProps={{ sx: { ml: 0 } }}
|
||||
required
|
||||
/>
|
||||
);
|
||||
}
|
25
src/components/server-options/ServerTypeOption.jsx
Normal file
25
src/components/server-options/ServerTypeOption.jsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import TextField from "@mui/material/TextField";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
|
||||
const displayOption = (o) => o.charAt(0) + o.toLowerCase().slice(1);
|
||||
|
||||
export const serverTypeOptions = ["VANILLA", "FABRIC", "PAPER", "SPIGOT"];
|
||||
export default function ServerTypeOption(props) {
|
||||
const { value, onChange } = props;
|
||||
return (
|
||||
<TextField
|
||||
label="Memory"
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
select
|
||||
required
|
||||
SelectProps={{ MenuProps: { sx: { maxHeight: "20rem" } } }}
|
||||
>
|
||||
{serverTypeOptions.map((o, i) => (
|
||||
<MenuItem value={o} key={i}>
|
||||
{displayOption(o)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
);
|
||||
}
|
37
src/components/server-options/VersionOption.jsx
Normal file
37
src/components/server-options/VersionOption.jsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { useVersionList } from "@mcl/queries";
|
||||
|
||||
export default function VersionOption(props) {
|
||||
const { value, onChange } = props;
|
||||
const versionList = useVersionList();
|
||||
const [versions, setVersions] = useState(["latest"]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!versionList.data) return;
|
||||
setVersions([
|
||||
"latest",
|
||||
...versionList.data.versions
|
||||
.filter(({ type: releaseType }) => releaseType === "release")
|
||||
.map(({ id }) => id),
|
||||
]);
|
||||
}, [versionList.data]);
|
||||
|
||||
return (
|
||||
<TextField
|
||||
label="Version"
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
select
|
||||
required
|
||||
SelectProps={{ MenuProps: { sx: { maxHeight: "20rem" } } }}
|
||||
>
|
||||
{versions.map((v, k) => (
|
||||
<MenuItem value={v} key={k}>
|
||||
{v}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Button from "@mui/material/Button";
|
||||
|
@ -16,15 +16,17 @@ export function useRconDialog(isOpen = false) {
|
|||
}
|
||||
|
||||
export default function RconDialog(props) {
|
||||
const { serverName, open, dialogToggle } = props;
|
||||
const { server, open, dialogToggle } = props;
|
||||
const { name: serverName, id: serverId } = server ?? {};
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
sx={
|
||||
fullScreen
|
||||
? {}
|
||||
: { "& .MuiDialog-paper": { width: "80%", maxHeight: 525 } }
|
||||
: { "& .mcl-MuiDialog-paper": { width: "80%", maxHeight: 555 } }
|
||||
}
|
||||
maxWidth="xs"
|
||||
open={open}
|
||||
|
@ -33,7 +35,7 @@ export default function RconDialog(props) {
|
|||
<Toolbar sx={{ display: { sm: "none" } }} />
|
||||
<DialogTitle>RCON - {serverName}</DialogTitle>
|
||||
<DialogContent>
|
||||
<RconView serverName={serverName} />
|
||||
<RconView serverId={serverId} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={dialogToggle}>
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import { io } from "socket.io-client";
|
||||
export default class RconSocket {
|
||||
constructor(logUpdate, serverName) {
|
||||
(this.sk = io("/", { query: { serverName } })), (this.logs = []);
|
||||
constructor(logUpdate, serverId) {
|
||||
(this.sk = io("/", { query: { serverId } })), (this.logs = []);
|
||||
this.logUpdate = logUpdate;
|
||||
this.sk.on("push", this.onPush.bind(this));
|
||||
this.sk.on("connect", this.onConnect.bind(this));
|
||||
this.sk.on("rcon-error", this.onRconError.bind(this));
|
||||
this.sk.on("error", () => console.log("WHOOSPSIE I GUESS?"));
|
||||
this.rconLive = false;
|
||||
}
|
||||
|
||||
onPush(p) {
|
||||
this.rconLive = true;
|
||||
this.logs = [...this.logs, p];
|
||||
this.logUpdate(this.logs);
|
||||
}
|
||||
|
@ -16,7 +20,13 @@ export default class RconSocket {
|
|||
this.sk.emit("msg", m);
|
||||
}
|
||||
|
||||
onRconError(v) {
|
||||
this.rconLive = false;
|
||||
console.log("Server sent" + v);
|
||||
}
|
||||
|
||||
onConnect() {
|
||||
this.sk.readyState = 1;
|
||||
this.logs = [];
|
||||
}
|
||||
|
||||
|
|
|
@ -6,22 +6,32 @@ import RconSocket from "./RconSocket.js";
|
|||
import "@mcl/css/rcon.css";
|
||||
|
||||
export default function RconView(props) {
|
||||
const { serverName } = props;
|
||||
const { serverId } = props;
|
||||
const logsRef = useRef(0);
|
||||
const [cmd, setCmd] = useState("");
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [rcon, setRcon] = 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]);
|
||||
const disconnectRcon = () => {
|
||||
if (!rcon || typeof rcon.disconnect !== "function") return;
|
||||
rcon.disconnect();
|
||||
};
|
||||
|
||||
useEffect(
|
||||
function () {
|
||||
if (!serverId) return;
|
||||
const rs = new RconSocket(setLogs, serverId);
|
||||
setRcon(rs);
|
||||
return disconnectRcon;
|
||||
},
|
||||
[serverId],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => logsRef.current.scrollTo(0, logsRef.current.scrollHeight),
|
||||
[(rcon ?? {}).logs],
|
||||
);
|
||||
|
||||
function sendCommand() {
|
||||
rcon.send(cmd);
|
||||
|
@ -45,8 +55,12 @@ export default function RconView(props) {
|
|||
variant="outlined"
|
||||
value={cmd}
|
||||
onChange={updateCmd}
|
||||
disabled={!(rcon && rcon.rconLive)}
|
||||
/>
|
||||
<Button onClick={sendCommand}>Send</Button>
|
||||
{rcon && rcon.rconLive && <Button onClick={sendCommand}>Send</Button>}
|
||||
{!(rcon && rcon.rconLive) && (
|
||||
<Button color="secondary">Not Connected</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
@ -11,7 +11,6 @@ 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";
|
||||
import FolderIcon from "@mui/icons-material/Folder";
|
||||
|
@ -19,10 +18,10 @@ import { Link } from "react-router-dom";
|
|||
|
||||
export default function ServerCard(props) {
|
||||
const { server, openRcon } = props;
|
||||
const { name, metrics, ftpAvailable, serverAvailable, services } = server;
|
||||
const startServer = useStartServer(name);
|
||||
const stopServer = useStopServer(name);
|
||||
const deleteServer = useDeleteServer(name);
|
||||
const { name, id, metrics, ftpAvailable, serverAvailable, services } = server;
|
||||
const startServer = useStartServer(id);
|
||||
const stopServer = useStopServer(id);
|
||||
const deleteServer = useDeleteServer(id);
|
||||
function toggleRcon() {
|
||||
if (!services.includes("server")) return;
|
||||
openRcon();
|
||||
|
@ -113,7 +112,7 @@ export default function ServerCard(props) {
|
|||
aria-label="Edit"
|
||||
size="large"
|
||||
component={Link}
|
||||
to={`/mcl/edit?server=${name}`}
|
||||
to={`/mcl/edit?server=${id}`}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
|
@ -122,8 +121,8 @@ export default function ServerCard(props) {
|
|||
aria-label="Files"
|
||||
size="large"
|
||||
component={Link}
|
||||
to={`/mcl/files?server=${name}`}
|
||||
disabled={!services.includes("ftp")}
|
||||
to={`/mcl/files?server=${id}`}
|
||||
disabled={!ftpAvailable}
|
||||
>
|
||||
<FolderIcon />
|
||||
</IconButton>
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
.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: #29985c;
|
||||
}
|
||||
.view > header > div > div > a {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
|
@ -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 HomeIcon from "@mui/icons-material/Home";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import List from "@mui/material/List";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
|
@ -36,18 +36,52 @@ export default function MCLMenu() {
|
|||
theme.zIndex.modal + 2 - (isDrawer ? 1 : 0);
|
||||
|
||||
return (
|
||||
<AppBar position="fixed" color="primary" sx={{ zIndex: drawerIndex() }}>
|
||||
<Box
|
||||
sx={{ flexGrow: 1, margin: "0 20px", color: "white" }}
|
||||
className="appbar-items"
|
||||
>
|
||||
<AppBar position="fixed" sx={{ zIndex: drawerIndex() }}>
|
||||
<Box sx={{ flexGrow: 1, margin: "0 20px" }} className="appbar-items">
|
||||
<Toolbar disableGutters>
|
||||
<IconButton component={Link} to="/" color="inherit">
|
||||
<HomeIcon />
|
||||
<IconButton
|
||||
size="large"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
sx={{ mr: 2 }}
|
||||
onClick={toggleDrawer}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<span style={{ margin: "auto 0", color: "inherit" }}>
|
||||
<Drawer
|
||||
open={drawerOpen}
|
||||
onClose={closeDrawer}
|
||||
sx={{ zIndex: drawerIndex(true) }}
|
||||
className="mcl-menu-drawer"
|
||||
>
|
||||
<Toolbar />
|
||||
<Box
|
||||
sx={{ width: drawerWidth, overflow: "auto" }}
|
||||
role="presentation"
|
||||
>
|
||||
<List>
|
||||
{pages.map(
|
||||
(page, index) =>
|
||||
page.visible && (
|
||||
<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()}
|
||||
</span>
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</Box>
|
||||
</AppBar>
|
||||
|
|
|
@ -11,17 +11,20 @@ export default [
|
|||
path: "/mcl/home",
|
||||
icon: <HomeIcon />,
|
||||
component: <Home />,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
name: "Create",
|
||||
path: "/mcl/create",
|
||||
icon: <AddIcon />,
|
||||
component: <Create />,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
name: "Edit",
|
||||
path: "/mcl/files",
|
||||
icon: <AddIcon />,
|
||||
component: <Files />,
|
||||
visible: false,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import Box from "@mui/material/Box";
|
||||
import CreateOptions from "./CreateOptions.jsx";
|
||||
import CreateCoreOptions from "./CreateCoreOptions.jsx";
|
||||
export default function Create() {
|
||||
return (
|
||||
<Box className="create">
|
||||
{/*<CreateMenu />*/}
|
||||
<Box className="create-wrapper" sx={{ display: "flex" }}>
|
||||
<CreateOptions />
|
||||
<CreateCoreOptions />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
|
145
src/pages/CreateCoreOptions.jsx
Normal file
145
src/pages/CreateCoreOptions.jsx
Normal file
|
@ -0,0 +1,145 @@
|
|||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useCreateServer } from "@mcl/queries";
|
||||
|
||||
// Core Options
|
||||
import NameOption from "@mcl/components/server-options/NameOption.jsx";
|
||||
import HostOption from "@mcl/components/server-options/HostOption.jsx";
|
||||
import VersionOption from "@mcl/components/server-options/VersionOption.jsx";
|
||||
import ServerTypeOption, {
|
||||
serverTypeOptions,
|
||||
} from "@mcl/components/server-options/ServerTypeOption.jsx";
|
||||
import CpuOption, {
|
||||
cpuOptions,
|
||||
} from "@mcl/components/server-options/CpuOption.jsx";
|
||||
import MemoryOption, {
|
||||
memoryOptions,
|
||||
} from "@mcl/components/server-options/MemoryOption.jsx";
|
||||
|
||||
import BackupHostOption from "@mcl/components/server-options/BackupHostOption.jsx";
|
||||
import BackupBucketOption from "@mcl/components/server-options/BackupBucketOption.jsx";
|
||||
import BackupIdOption from "@mcl/components/server-options/BackupIdOption.jsx";
|
||||
import BackupKeyOption from "@mcl/components/server-options/BackupKeyOption.jsx";
|
||||
import BackupIntervalOption, {
|
||||
backupIntervalDefault,
|
||||
} from "@mcl/components/server-options/BackupIntervalOption.jsx";
|
||||
|
||||
const defaultServer = {
|
||||
version: "latest",
|
||||
serverType: serverTypeOptions[0],
|
||||
cpu: cpuOptions[0],
|
||||
memory: memoryOptions[2], // 1.5GB
|
||||
};
|
||||
|
||||
export default function CreateCoreOptions() {
|
||||
const [backupEnabled, setBackupEnabled] = useState(false);
|
||||
const [spec, setSpec] = useState(defaultServer);
|
||||
const nav = useNavigate();
|
||||
const createServer = useCreateServer(spec);
|
||||
|
||||
const updateSpec = (attr, val) => {
|
||||
const s = { ...spec };
|
||||
s[attr] = val;
|
||||
setSpec(s);
|
||||
};
|
||||
|
||||
const coreUpdate = (attr) => (e) => updateSpec(attr, e.target.value);
|
||||
|
||||
async function upsertSpec() {
|
||||
if (validateSpec() !== "validated") return;
|
||||
createServer(spec)
|
||||
// .then(() => nav("/"))
|
||||
.catch(alert);
|
||||
}
|
||||
|
||||
function validateSpec() {
|
||||
console.log("TODO CREATE VALIDATION");
|
||||
if (!spec.host) return alertValidationError("Host cannot be blank");
|
||||
if (!spec.name) return alertValidationError("Name not included");
|
||||
if (!spec.version) return alertValidationError("Version cannot be blank");
|
||||
return "validated";
|
||||
}
|
||||
|
||||
function alertValidationError(reason) {
|
||||
alert(`Could not validate spec because: ${reason}`);
|
||||
}
|
||||
|
||||
const toggleBackupEnabled = () => {
|
||||
const s = { ...spec };
|
||||
if (!backupEnabled) {
|
||||
(s.backupInterval = backupIntervalDefault),
|
||||
(s.backupBucket = `/mcl/server-backups/${(
|
||||
s.name ?? "my-server"
|
||||
).toLowerCase()}`);
|
||||
} else for (var k in s) if (k.startsWith("backup")) delete s[k];
|
||||
setSpec(s);
|
||||
console.log(s);
|
||||
setBackupEnabled(!backupEnabled);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="create-options"
|
||||
sx={{ width: "100%", maxWidth: "600px", margin: "auto" }}
|
||||
>
|
||||
<FormControl fullWidth sx={{ mt: "2rem", display: "flex", gap: ".5rem" }}>
|
||||
<NameOption onChange={coreUpdate("name")} />
|
||||
<HostOption onChange={coreUpdate("host")} />
|
||||
<VersionOption value={spec.version} onChange={coreUpdate("version")} />
|
||||
<ServerTypeOption
|
||||
value={spec.serverType}
|
||||
onChange={coreUpdate("serverType")}
|
||||
/>
|
||||
<CpuOption value={spec.cpu} onChange={coreUpdate("cpu")} />
|
||||
<MemoryOption value={spec.memory} onChange={coreUpdate("memory")} />
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={backupEnabled}
|
||||
onChange={toggleBackupEnabled}
|
||||
inputProps={{ "aria-label": "controlled" }}
|
||||
/>
|
||||
}
|
||||
label="Enable Backups?"
|
||||
labelPlacement="start"
|
||||
sx={{ mr: "auto" }}
|
||||
/>
|
||||
{backupEnabled && (
|
||||
<FormControl
|
||||
fullWidth
|
||||
sx={{ mt: "2rem", display: "flex", gap: ".5rem" }}
|
||||
>
|
||||
<Typography variant="h6">Backups</Typography>
|
||||
<BackupHostOption
|
||||
value={spec.backupHost}
|
||||
onChange={coreUpdate("backupHost")}
|
||||
/>
|
||||
<BackupBucketOption
|
||||
value={spec.backupBucket}
|
||||
onChange={coreUpdate("backupBucket")}
|
||||
/>
|
||||
<BackupIdOption
|
||||
value={spec.backupId}
|
||||
onChange={coreUpdate("backupId")}
|
||||
/>
|
||||
<BackupKeyOption
|
||||
value={spec.backupKey}
|
||||
onChange={coreUpdate("backupKey")}
|
||||
/>
|
||||
<BackupIntervalOption onChange={coreUpdate("backupInterval")} />
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<Button onClick={upsertSpec} variant="contained">
|
||||
Create
|
||||
</Button>
|
||||
</FormControl>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -1,20 +1,36 @@
|
|||
import { useEffect } from "react";
|
||||
import { useState, 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 FilePreview, {
|
||||
useFilePreview,
|
||||
} from "@mcl/components/files/FilePreview.jsx";
|
||||
import MineclusterFiles from "@mcl/components/files/MineclusterFiles.jsx";
|
||||
|
||||
export default function Files() {
|
||||
const [open, dialogToggle] = useFilePreview();
|
||||
const [previewData, setPreviewData] = useState();
|
||||
const [searchParams] = useSearchParams();
|
||||
const currentServer = searchParams.get("server");
|
||||
const nav = useNavigate();
|
||||
useEffect(() => {
|
||||
if (!currentServer) nav("/");
|
||||
}, [currentServer]);
|
||||
|
||||
function changePreview(name, fileData) {
|
||||
setPreviewData({ name, fileData });
|
||||
dialogToggle();
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="edit" sx={{ height: "100%" }}>
|
||||
<MineclusterFiles server={currentServer} />
|
||||
<FilePreview
|
||||
open={open}
|
||||
dialogToggle={dialogToggle}
|
||||
previewData={previewData}
|
||||
/>
|
||||
<MineclusterFiles server={currentServer} changePreview={changePreview} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ export default function Home() {
|
|||
setServer(s);
|
||||
rconToggle();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="home">
|
||||
<Overview clusterMetrics={clusterMetrics} />
|
||||
|
@ -50,10 +51,10 @@ export default function Home() {
|
|||
<Box className="servers">
|
||||
{!isLoading &&
|
||||
servers.map((s, k) => (
|
||||
<ServerCard key={k} server={s} openRcon={openRcon(s.name)} />
|
||||
<ServerCard key={k} server={s} openRcon={openRcon(s)} />
|
||||
))}
|
||||
</Box>
|
||||
<RconDialog open={rdOpen} dialogToggle={rconToggle} serverName={server} />
|
||||
<RconDialog open={rdOpen} dialogToggle={rconToggle} server={server} />
|
||||
<Button
|
||||
component={Link}
|
||||
to="/mcl/create"
|
||||
|
|
|
@ -20,38 +20,45 @@ const fetchApiPost = (subPath, json) => async () =>
|
|||
body: JSON.stringify(json),
|
||||
}).then((res) => res.json());
|
||||
|
||||
export const useServerStatus = (server) =>
|
||||
export const useServerStatus = (serverId) =>
|
||||
useQuery({
|
||||
queryKey: [`server-status-${server}`],
|
||||
queryFn: fetchApiPost("/server/status", { name: server }),
|
||||
queryKey: [`server-status-${serverId}`],
|
||||
queryFn: fetchApiPost("/server/status", { id: serverId }),
|
||||
});
|
||||
export const useServerMetrics = (server) =>
|
||||
export const useServerMetrics = (serverId) =>
|
||||
useQuery({
|
||||
queryKey: [`server-metrics-${server}`],
|
||||
queryFn: fetchApiPost("/server/metrics", { name: server }),
|
||||
queryKey: [`server-metrics-${serverId}`],
|
||||
queryFn: fetchApiPost("/server/metrics", { id: serverId }),
|
||||
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 useStartServer = (serverId) =>
|
||||
postJsonApi("/server/start", { id: serverId }, "server-instances");
|
||||
export const useStopServer = (serverId) =>
|
||||
postJsonApi("/server/stop", { id: serverId }, "server-instances");
|
||||
export const useDeleteServer = (serverId) =>
|
||||
postJsonApi("/server/delete", { id: serverId }, "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) =>
|
||||
export const getServerFiles = async (serverId, path) =>
|
||||
fetchApiCore("/files/list", { id: serverId, path }, "POST", true);
|
||||
export const createServerFolder = async (serverId, path) =>
|
||||
fetchApiCore("/files/folder", {
|
||||
name: server,
|
||||
id: serverId,
|
||||
path,
|
||||
});
|
||||
export const deleteServerItem = async (server, path, isDir) =>
|
||||
fetchApiCore("/files/item", { name: server, path, isDir }, "DELETE");
|
||||
export const deleteServerItem = async (serverId, path, isDir) =>
|
||||
fetchApiCore("/files/item", { id: serverId, path, isDir }, "DELETE");
|
||||
|
||||
export const getServerItem = async (server, name, path) =>
|
||||
fetchApiCore("/files/item", { name: server, path })
|
||||
export async function previewServerItem(serverId, path) {
|
||||
const resp = await fetchApiCore("/files/item", { id: serverId, path });
|
||||
if (!resp.status === 200) return console.log("AHHHH");
|
||||
const blob = await resp.blob();
|
||||
return blob;
|
||||
}
|
||||
|
||||
export const getServerItem = async (serverId, name, path) =>
|
||||
fetchApiCore("/files/item", { id: serverId, path })
|
||||
.then((resp) =>
|
||||
resp.status === 200
|
||||
? resp.blob()
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
// Generated using https://zenoo.github.io/mui-theme-creator/
|
||||
import { createTheme } from "@mui/material/styles";
|
||||
import { unstable_ClassNameGenerator as ClassNameGenerator } from "@mui/material/className";
|
||||
// This fixes style clashing with Chonky which has not been updated to Material 5
|
||||
// see https://github.com/TimboKZ/Chonky/issues/101#issuecomment-1362949314
|
||||
ClassNameGenerator.configure((componentName) => `mcl-${componentName}`);
|
||||
|
||||
const themeOptions = {
|
||||
palette: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue