[FEATURE] Quality of Life Improvements for Management (#7)
Co-authored-by: Dunemask <dunemask@gmail.com> Reviewed-on: https://gitea.dunemask.dev/elysium/minecluster/pulls/7
This commit is contained in:
parent
6eb4ed3e95
commit
23efaafe1d
18 changed files with 479 additions and 39 deletions
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, memo } from "react";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Button from "@mui/material/Button";
|
||||
|
@ -8,6 +8,8 @@ import DialogActions from "@mui/material/DialogActions";
|
|||
import Dialog from "@mui/material/Dialog";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
|
||||
import TextEditor from "./TextEditor.jsx";
|
||||
|
||||
const textFileTypes = ["properties", "txt", "yaml", "yml", "json", "env"];
|
||||
const imageFileTypes = ["png", "jpeg", "jpg"];
|
||||
|
||||
|
@ -19,28 +21,40 @@ export function useFilePreview(isOpen = false) {
|
|||
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 [modifiedText, setModifiedText] = useState();
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down("md"));
|
||||
|
||||
const { previewData, open, dialogToggle } = props;
|
||||
const { fileData, name } = previewData ?? {};
|
||||
const { previewData, open, dialogToggle, server: serverId } = props;
|
||||
const { fileData, name, filePath } = 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]);
|
||||
const editorChange = (v) => setModifiedText(v);
|
||||
|
||||
async function onPreviewChange() {
|
||||
if (!isTextFile) return;
|
||||
const text = await fileData.text();
|
||||
setFileText(text);
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
const formData = new FormData();
|
||||
const blob = new Blob([modifiedText], { type: "plain/text" });
|
||||
formData.append("file", blob, name);
|
||||
formData.append("id", serverId);
|
||||
formData.append("path", filePath);
|
||||
await fetch("/api/files/upload", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
dialogToggle();
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
|
@ -62,12 +76,15 @@ export default function FilePreview(props) {
|
|||
<Toolbar sx={{ display: { sm: "none" } }} />
|
||||
<DialogTitle>{name}</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextPreview fileText={fileText} />
|
||||
<TextEditor text={fileText} onChange={editorChange} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={dialogToggle}>
|
||||
Close
|
||||
</Button>
|
||||
<Button variant="contained" autoFocus onClick={onSave}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
|
|
@ -105,7 +105,7 @@ export default function MineclusterFiles(props) {
|
|||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("id", serverId);
|
||||
formData.append("path", [...dirStack, name].join("/"));
|
||||
formData.append("path", [...dirStack, file.name].join("/"));
|
||||
await fetch("/api/files/upload", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
|
@ -125,7 +125,8 @@ export default function MineclusterFiles(props) {
|
|||
function previewFile(file) {
|
||||
const { name } = file;
|
||||
previewServerItem(serverId, [...dirStack, name].join("/")).then(
|
||||
(fileData) => changePreview(name, fileData),
|
||||
(fileData) =>
|
||||
changePreview(name, fileData, [...dirStack, name].join("/")),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
21
src/components/files/TextEditor.jsx
Normal file
21
src/components/files/TextEditor.jsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import ReactQuill from "react-quill";
|
||||
import { useState, useEffect, useMemo, memo } from "react";
|
||||
import "react-quill/dist/quill.snow.css";
|
||||
|
||||
const buildDelta = (t) => {
|
||||
if (!t) return;
|
||||
const ops = t.split("\n").map((l) => ({ insert: `${l}\n` }));
|
||||
return { ops };
|
||||
};
|
||||
|
||||
function TextEditor(props) {
|
||||
const { text, onChange } = props;
|
||||
const [delta, setDelta] = useState();
|
||||
const constructDelta = useMemo(() => buildDelta(text), [text]);
|
||||
useEffect(() => setDelta(constructDelta), [text]);
|
||||
|
||||
const onEditorChange = (c, d, s, editor) => onChange(editor.getText());
|
||||
|
||||
return <ReactQuill theme="snow" value={delta} onChange={onEditorChange} />;
|
||||
}
|
||||
export default memo(TextEditor, (a, b) => a.text === b.text);
|
41
src/components/server-options/ExtraPortsOption.jsx
Normal file
41
src/components/server-options/ExtraPortsOption.jsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { useState } from "react";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Chip from "@mui/material/Chip";
|
||||
|
||||
const validatePort = (p) => p !== "25565" && p !== "25575" && p.length < 6;
|
||||
|
||||
export default function ExtraPortsOption(props) {
|
||||
const [extraPorts, setExtraPorts] = useState([]);
|
||||
const { onChange } = props;
|
||||
|
||||
function portChange(e, val, optionType, changedValue) {
|
||||
if (optionType === "clear") {
|
||||
setExtraPorts([]);
|
||||
onChange("extraPorts", []);
|
||||
return;
|
||||
}
|
||||
if (!validatePort(changedValue.option))
|
||||
return alert("That port cannot be added/removed as an extra port!");
|
||||
setExtraPorts(val);
|
||||
onChange("extraPorts", val);
|
||||
}
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
multiple
|
||||
id="extra-ports-autocomplete"
|
||||
options={[]}
|
||||
value={extraPorts}
|
||||
onChange={portChange}
|
||||
freeSolo
|
||||
renderInput={(p) => <TextField {...p} label="Extra Ports" />}
|
||||
renderTags={(value, getTagProps) =>
|
||||
value.map((option, index) => {
|
||||
const defaultChipProps = getTagProps({ index });
|
||||
return <Chip label={option} {...defaultChipProps} />;
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -21,6 +21,7 @@ import CpuOption, {
|
|||
import MemoryOption, {
|
||||
memoryOptions,
|
||||
} from "@mcl/components/server-options/MemoryOption.jsx";
|
||||
import ExtraPortsOption from "@mcl/components/server-options/ExtraPortsOption.jsx";
|
||||
|
||||
import BackupHostOption from "@mcl/components/server-options/BackupHostOption.jsx";
|
||||
import BackupBucketOption from "@mcl/components/server-options/BackupBucketOption.jsx";
|
||||
|
@ -35,6 +36,7 @@ const defaultServer = {
|
|||
serverType: serverTypeOptions[0],
|
||||
cpu: cpuOptions[0],
|
||||
memory: memoryOptions[2], // 1.5GB
|
||||
extraPorts: [],
|
||||
};
|
||||
|
||||
export default function CreateCoreOptions() {
|
||||
|
@ -79,7 +81,6 @@ export default function CreateCoreOptions() {
|
|||
).toLowerCase()}`);
|
||||
} else for (var k in s) if (k.startsWith("backup")) delete s[k];
|
||||
setSpec(s);
|
||||
console.log(s);
|
||||
setBackupEnabled(!backupEnabled);
|
||||
};
|
||||
|
||||
|
@ -98,6 +99,7 @@ export default function CreateCoreOptions() {
|
|||
/>
|
||||
<CpuOption value={spec.cpu} onChange={coreUpdate("cpu")} />
|
||||
<MemoryOption value={spec.memory} onChange={coreUpdate("memory")} />
|
||||
<ExtraPortsOption onChange={updateSpec} />
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
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";
|
||||
|
@ -18,14 +16,15 @@ export default function Files() {
|
|||
if (!currentServer) nav("/");
|
||||
}, [currentServer]);
|
||||
|
||||
function changePreview(name, fileData) {
|
||||
setPreviewData({ name, fileData });
|
||||
function changePreview(name, fileData, filePath) {
|
||||
setPreviewData({ name, fileData, filePath });
|
||||
dialogToggle();
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="edit" sx={{ height: "100%" }}>
|
||||
<FilePreview
|
||||
server={currentServer}
|
||||
open={open}
|
||||
dialogToggle={dialogToggle}
|
||||
previewData={previewData}
|
||||
|
|
|
@ -52,7 +52,7 @@ export const deleteServerItem = async (serverId, path, isDir) =>
|
|||
|
||||
export async function previewServerItem(serverId, path) {
|
||||
const resp = await fetchApiCore("/files/item", { id: serverId, path });
|
||||
if (!resp.status === 200) return console.log("AHHHH");
|
||||
if (resp.status !== 200) return console.log("AHHHH");
|
||||
const blob = await resp.blob();
|
||||
return blob;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue