[FEATURE] Several QOL Updates (#18)

Co-authored-by: Dunemask <dunemask@gmail.com>
Reviewed-on: https://gitea.dunemask.dev/elysium/minecluster/pulls/18
This commit is contained in:
dunemask 2024-02-11 03:57:01 +00:00
parent 4959d6c1fe
commit 0a0f9c8463
16 changed files with 432 additions and 828 deletions

View file

@ -1,42 +0,0 @@
// 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>
);
}),
);

View file

@ -1,4 +1,4 @@
import { useState, useEffect, memo } 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";
@ -10,7 +10,17 @@ import Toolbar from "@mui/material/Toolbar";
import TextEditor from "./TextEditor.jsx";
import { cairoAuthHeader } from "@mcl/util/auth.js";
const textFileTypes = ["properties", "txt", "yaml", "yml", "json", "env"];
const textFileTypes = [
"properties",
"txt",
"yaml",
"yml",
"json",
"env",
"toml",
"tml",
"text",
];
const imageFileTypes = ["png", "jpeg", "jpg"];
export const supportedFileTypes = [...textFileTypes, ...imageFileTypes];
@ -44,6 +54,7 @@ export default function FilePreview(props) {
}
async function onSave() {
if (!isTextFile) return;
const formData = new FormData();
const blob = new Blob([modifiedText], { type: "plain/text" });
formData.append("file", blob, name);
@ -77,7 +88,7 @@ export default function FilePreview(props) {
<Toolbar sx={{ display: { sm: "none" } }} />
<DialogTitle>{name}</DialogTitle>
<DialogContent>
<TextEditor text={fileText} onChange={editorChange} />
{isTextFile && <TextEditor text={fileText} onChange={editorChange} />}
</DialogContent>
<DialogActions>
<Button autoFocus onClick={dialogToggle}>

View file

@ -1,5 +1,7 @@
import { useState, useEffect, useMemo, useRef } from "react";
import Box from "@mui/material/Box";
import Dropzone from "react-dropzone";
import {
FileBrowser,
FileContextMenu,
@ -16,8 +18,9 @@ import {
createServerFolder,
deleteServerItem,
getServerItem,
moveServerItems,
previewServerItem,
} from "@mcl/queries";
import { previewServerItem } from "../../util/queries";
import { cairoAuthHeader } from "@mcl/util/auth.js";
import { supportedFileTypes } from "./FilePreview.jsx";
@ -32,6 +35,7 @@ export default function MineclusterFiles(props) {
ChonkyActions.DownloadFiles,
ChonkyActions.CopyFiles,
ChonkyActions.DeleteFiles,
ChonkyActions.MoveFiles,
],
[],
);
@ -97,6 +101,10 @@ export default function MineclusterFiles(props) {
function uploadFileSelection(e) {
if (!e.target.files || e.target.files.length === 0) return;
const { files } = e.target;
uploadMultipleFiles(files);
}
function uploadMultipleFiles(files) {
Promise.all([...files].map((f) => uploadFile(f)))
.catch((e) => console.log("Error uploading a file", e))
.then(updateFiles);
@ -132,6 +140,15 @@ export default function MineclusterFiles(props) {
);
}
function moveFile(movePayload) {
const { files: filePayload, destination: destinationPayload } = movePayload;
if (!destinationPayload.isDir || filePayload.length === 0) return;
const files = filePayload.map((f) => f.name);
const dest = destinationPayload.id;
const origin = dirStack.join("/");
moveServerItems(serverId, files, dest, origin).then(updateFiles);
}
function fileClick(chonkyEvent) {
const { id: clickEvent, payload } = chonkyEvent;
if (clickEvent === "open_parent_folder") return openParentFolder();
@ -141,32 +158,41 @@ export default function MineclusterFiles(props) {
return downloadFiles(chonkyEvent.state.selectedFilesForAction);
if (clickEvent === "delete_files")
return deleteItems(chonkyEvent.state.selectedFilesForAction);
if (clickEvent === "move_files") return moveFile(payload);
if (clickEvent !== "open_files") return; // console.log(clickEvent);
openItem(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>
return (
<Dropzone onDrop={uploadMultipleFiles}>
{({ getRootProps }) => (
<Box
className="minecluster-files"
sx={{ height: "calc(100vh - 6rem)" }}
onDrop={getRootProps().onDrop}
>
<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>
)}
</Dropzone>
);
}

View file

@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState } from "react";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTheme } from "@mui/material/styles";
import Button from "@mui/material/Button";
@ -19,22 +19,19 @@ export default function RconDialog(props) {
const { server, open, dialogToggle } = props;
const { name: serverName, id: serverId } = server ?? {};
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
const fullScreen = useMediaQuery(theme.breakpoints.down("md"));
return (
<Dialog
sx={
fullScreen
? {}
: { "& .mcl-MuiDialog-paper": { width: "80%", maxHeight: 555 } }
}
maxWidth="xs"
fullWidth
maxWidth="lg"
open={open}
fullScreen={fullScreen}
PaperProps={!fullScreen ? { sx: { height: "60%" } } : undefined}
>
<Toolbar sx={{ display: { sm: "none" } }} />
<Toolbar sx={{ display: { md: "none" } }} />
<DialogTitle>RCON - {serverName}</DialogTitle>
<DialogContent>
<DialogContent sx={{ height: "100%" }}>
<RconView serverId={serverId} />
</DialogContent>
<DialogActions>

View file

@ -2,9 +2,21 @@ 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 Skeleton from "@mui/material/Skeleton";
import Typography from "@mui/material/Typography";
import RconSocket from "./RconSocket.js";
import "@mcl/css/rcon.css";
function RconLogSkeleton() {
return (
<Skeleton
variant="text"
width="100%"
sx={{ backgroundColor: "rgba(255,255,255,.25)" }}
/>
);
}
export default function RconView(props) {
const { serverId } = props;
const logsRef = useRef(0);
@ -39,16 +51,31 @@ export default function RconView(props) {
}
return (
<Box>
<div className="rconLogsWrapper" ref={logsRef}>
{logs.map((v, k) => (
<Box key={k}>
{v}
<br />
</Box>
))}
</div>
<Box className="rconActions">
<Box sx={{ height: "100%", display: "flex", flexWrap: "wrap" }}>
<Box
className="rconLogsWrapper"
ref={logsRef}
style={{
padding: "1rem",
backgroundColor: "rgba(0,0,0,.815)",
color: "white",
borderRadius: "4px",
width: "100%",
}}
>
{logs.length === 0 &&
[...Array(20).keys()].map((_v, i) => <RconLogSkeleton key={i} />)}
{logs.length > 0 &&
logs.map((v, k) => (
<Box key={k}>
<Typography variant="subtitle2">{v}</Typography>
</Box>
))}
</Box>
<Box
className="rconActions"
sx={{ marginTop: "auto", paddingTop: "1rem", width: "100%" }}
>
<TextField
id="outlined-basic"
label="Command"
@ -56,9 +83,12 @@ export default function RconView(props) {
value={cmd}
onChange={updateCmd}
disabled={!(rcon && rcon.rconLive && !rcon.rconError)}
sx={{ width: "100%" }}
/>
{rcon && rcon.rconLive && !rcon.rconError && (
<Button onClick={sendCommand}>Send</Button>
<Button onClick={sendCommand} sx={{ padding: "0 2rem" }}>
Send
</Button>
)}
{!(rcon && rcon.rconLive && !rcon.rconError) && (
<Button color="secondary">Not Connected</Button>

View file

@ -1,8 +1,7 @@
.rconLogsWrapper {
overflow-y: scroll;
max-height: 20rem;
max-height: calc(100% - 6rem);
word-wrap: break-word;
margin-bottom: 10px;
}
.rconActions {
display: inline-flex;

View file

@ -64,6 +64,13 @@ export const createServerFolder = async (serverId, path) =>
export const deleteServerItem = async (serverId, path, isDir) =>
fetchApiCore("/files/item", { id: serverId, path, isDir }, "DELETE");
export const moveServerItems = async (serverId, files, destination, origin) =>
fetchApiCore(
"/files/move",
{ id: serverId, files, destination, origin },
"POST",
);
export async function previewServerItem(serverId, path) {
const resp = await fetchApiCore("/files/item", { id: serverId, path });
if (resp.status !== 200) return console.log("AHHHH");