[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:
parent
4959d6c1fe
commit
0a0f9c8463
16 changed files with 432 additions and 828 deletions
|
@ -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>
|
||||
);
|
||||
}),
|
||||
);
|
|
@ -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}>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue