Compare commits
9 commits
master
...
ep/Feb10-2
Author | SHA1 | Date | |
---|---|---|---|
cf291a6112 | |||
15aae89e6d | |||
c9361e6771 | |||
39369fe41a | |||
9b7539664e | |||
99c735a6fd | |||
beca266eef | |||
1bdc10c81e | |||
a804a6e98b |
16 changed files with 432 additions and 828 deletions
|
@ -4,6 +4,7 @@ import {
|
||||||
listServerFiles,
|
listServerFiles,
|
||||||
removeServerItem,
|
removeServerItem,
|
||||||
uploadServerItem,
|
uploadServerItem,
|
||||||
|
moveServerItems,
|
||||||
} from "../k8s/server-files.js";
|
} from "../k8s/server-files.js";
|
||||||
import { sendError } from "../util/ExpressClientError.js";
|
import { sendError } from "../util/ExpressClientError.js";
|
||||||
import { checkAuthorization } from "../database/queries/server-queries.js";
|
import { checkAuthorization } from "../database/queries/server-queries.js";
|
||||||
|
@ -79,3 +80,18 @@ export async function getItem(req, res) {
|
||||||
})
|
})
|
||||||
.catch(sendError(res));
|
.catch(sendError(res));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function moveItems(req, res) {
|
||||||
|
const serverSpec = req.body;
|
||||||
|
if (!serverSpec.id) return res.status(400).send("Server id missing!");
|
||||||
|
if (!serverSpec.destination)
|
||||||
|
return res.status(400).send("Destination required!");
|
||||||
|
if (!serverSpec.origin) return res.status(400).send("Origin required!");
|
||||||
|
if (!serverSpec.files || !Array.isArray(serverSpec.files))
|
||||||
|
return res.status(400).send("Files required!");
|
||||||
|
const authorized = await checkAuthorization(serverSpec.id, req.cairoId);
|
||||||
|
if (!authorized) return res.sendStatus(403);
|
||||||
|
moveServerItems(serverSpec)
|
||||||
|
.then(() => res.sendStatus(200))
|
||||||
|
.catch(sendError(res));
|
||||||
|
}
|
||||||
|
|
|
@ -26,9 +26,13 @@ export async function webConsoleLogs(socket) {
|
||||||
|
|
||||||
const log = new k8s.Log(kc);
|
const log = new k8s.Log(kc);
|
||||||
const logStream = new stream.PassThrough();
|
const logStream = new stream.PassThrough();
|
||||||
logStream.on("data", (chunk) =>
|
var logstreamBuffer = "";
|
||||||
socket.emit("push", Buffer.from(chunk).toString()),
|
logStream.on("data", (chunk) => {
|
||||||
);
|
const bufferString = Buffer.from(chunk).toString();
|
||||||
|
if (!bufferString.includes("\n")) return (logstreamBuffer += bufferString);
|
||||||
|
const clientChunks = `${logstreamBuffer}${bufferString}`.split("\n");
|
||||||
|
for (var c of clientChunks) socket.emit("push", c);
|
||||||
|
});
|
||||||
log
|
log
|
||||||
.log(namespace, mcsPods[0], containerName, logStream, {
|
.log(namespace, mcsPods[0], containerName, logStream, {
|
||||||
follow: true,
|
follow: true,
|
||||||
|
|
|
@ -67,7 +67,7 @@ function createBackupSecret(serverSpec) {
|
||||||
`endpoint = ${backupHost}`,
|
`endpoint = ${backupHost}`,
|
||||||
`acl = private`,
|
`acl = private`,
|
||||||
`no_check_bucket = true`,
|
`no_check_bucket = true`,
|
||||||
`no_check_container = true`
|
`no_check_container = true`,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
backupYaml.data["rclone.conf"] = Buffer.from(rcloneConfig).toString("base64");
|
backupYaml.data["rclone.conf"] = Buffer.from(rcloneConfig).toString("base64");
|
||||||
return backupYaml;
|
return backupYaml;
|
||||||
|
|
|
@ -86,12 +86,22 @@ export async function uploadServerItem(serverSpec, file) {
|
||||||
}).catch(handleError);
|
}).catch(handleError);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getServerItem(serverSpec, writableStream) {
|
export async function getServerItem(serverSpec) {
|
||||||
const { path } = serverSpec;
|
const { path } = serverSpec;
|
||||||
const ds = new Transform({ transform: (c, e, cb) => cb(null, c) });
|
const ds = new Transform({ transform: (c, _e, cb) => cb(null, c) });
|
||||||
pathSecurityCheck(path);
|
pathSecurityCheck(path);
|
||||||
const ftpTransfer = useServerFtp(serverSpec, async (c) => {
|
const ftpTransfer = useServerFtp(serverSpec, async (c) => {
|
||||||
await c.downloadTo(ds, path);
|
await c.downloadTo(ds, path);
|
||||||
}).catch(handleError);
|
}).catch(handleError);
|
||||||
return { ds, ftpTransfer };
|
return { ds, ftpTransfer };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function moveServerItems(serverSpec) {
|
||||||
|
const { destination, origin, files } = serverSpec;
|
||||||
|
useServerFtp(serverSpec, async (c) =>
|
||||||
|
Promise.all(
|
||||||
|
files.map((f) => c.rename(`${origin}/${f}`, `${destination}/${f}`)),
|
||||||
|
),
|
||||||
|
).catch(handleError);
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
listFiles,
|
listFiles,
|
||||||
uploadItem,
|
uploadItem,
|
||||||
getItem,
|
getItem,
|
||||||
|
moveItems,
|
||||||
} from "../controllers/file-controller.js";
|
} from "../controllers/file-controller.js";
|
||||||
|
|
||||||
import cairoAuthMiddleware from "./middlewares/auth-middleware.js";
|
import cairoAuthMiddleware from "./middlewares/auth-middleware.js";
|
||||||
|
@ -18,6 +19,7 @@ router.post("/list", listFiles);
|
||||||
router.post("/folder", createFolder);
|
router.post("/folder", createFolder);
|
||||||
router.delete("/item", deleteItem);
|
router.delete("/item", deleteItem);
|
||||||
router.post("/item", getItem);
|
router.post("/item", getItem);
|
||||||
|
router.post("/move", moveItems);
|
||||||
router.post("/upload", multerMiddleware.single("file"), uploadItem);
|
router.post("/upload", multerMiddleware.single("file"), uploadItem);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -17,7 +17,6 @@ const cairoAuthenticate = async (token) => {
|
||||||
// Middleware
|
// Middleware
|
||||||
const cairoAuthHandler = (req, res, next) => {
|
const cairoAuthHandler = (req, res, next) => {
|
||||||
if (!req.token) return res.status(401).send("Cairo auth required!");
|
if (!req.token) return res.status(401).send("Cairo auth required!");
|
||||||
VERB("AUTH", `${MCL_CAIRO_URL}/api/user/info`);
|
|
||||||
cairoAuthenticate(req.token)
|
cairoAuthenticate(req.token)
|
||||||
.then((authData) => (req.cairoId = authData.id))
|
.then((authData) => (req.cairoId = authData.id))
|
||||||
.then(() => next())
|
.then(() => next())
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
import multer from "multer";
|
|
||||||
import multerS3 from "multer-s3";
|
|
||||||
import AWS from "aws-sdk";
|
|
||||||
|
|
||||||
// Environment Variables
|
|
||||||
const {
|
|
||||||
MCL_S3_ENDPOINT: s3Endpoint,
|
|
||||||
MCL_S3_ACCESS_KEY_ID: s3KeyId,
|
|
||||||
MCL_S3_ACCESS_KEY: s3Key,
|
|
||||||
} = process.env;
|
|
||||||
|
|
||||||
export const mcl = "mcl";
|
|
||||||
|
|
||||||
export const s3 = new AWS.S3({
|
|
||||||
endpoint: s3Endpoint,
|
|
||||||
accessKeyId: s3KeyId,
|
|
||||||
secretAccessKey: s3Key,
|
|
||||||
sslEnabled: true,
|
|
||||||
s3ForcePathStyle: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const storage = multerS3({
|
|
||||||
s3,
|
|
||||||
bucket,
|
|
||||||
contentType: multerS3.AUTO_CONTENT_TYPE,
|
|
||||||
metadata: (req, file, cb) => {
|
|
||||||
cb(null, { fieldName: file.fieldname });
|
|
||||||
},
|
|
||||||
key: (req, file, cb) => {
|
|
||||||
cb(null, Date.now().toString());
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const upload = multer({ storage });
|
|
954
package-lock.json
generated
954
package-lock.json
generated
File diff suppressed because it is too large
Load diff
15
package.json
15
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "minecluster",
|
"name": "minecluster",
|
||||||
"version": "0.0.1-alpha.0",
|
"version": "0.0.1-alpha.1",
|
||||||
"description": "Minecraft Server management using Kubernetes",
|
"description": "Minecraft Server management using Kubernetes",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
"start": "node dist/app.js",
|
"start": "node dist/app.js",
|
||||||
"dev:server": "nodemon dist/app.js",
|
"dev:server": "nodemon dist/app.js",
|
||||||
"dev:react": "vite",
|
"dev:react": "vite",
|
||||||
"kub": "nodemon lib/k8s.js",
|
"lint": "npx prettier -w src lib vite.config.js",
|
||||||
"start:dev": "concurrently -k \"MCL_DEV_PORT=52025 npm run dev:server\" \" MCL_VITE_DEV_PORT=52000 MCL_VITE_BACKEND_URL=http://localhost:52025 npm run dev:react\" -n s,v -p -c green,yellow",
|
"start:dev": "concurrently -k \"MCL_DEV_PORT=52025 npm run dev:server\" \" MCL_VITE_DEV_PORT=52000 MCL_VITE_BACKEND_URL=http://localhost:52025 npm run dev:react\" -n s,v -p -c green,yellow",
|
||||||
"start:dev:garden": "concurrently -k \"npm run dev:server\" \"npm run dev:react\" -n s,v -p -c green,yellow"
|
"start:dev:garden": "concurrently -k \"npm run dev:server\" \"npm run dev:react\" -n s,v -p -c green,yellow"
|
||||||
},
|
},
|
||||||
|
@ -24,9 +24,9 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@emotion/react": "^11.11.3",
|
"@emotion/react": "^11.11.3",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@mui/icons-material": "^5.15.7",
|
"@mui/icons-material": "^5.15.9",
|
||||||
"@mui/material": "^5.15.7",
|
"@mui/material": "^5.15.9",
|
||||||
"@tanstack/react-query": "^5.18.1",
|
"@tanstack/react-query": "^5.20.1",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"chonky": "^2.3.2",
|
"chonky": "^2.3.2",
|
||||||
"chonky-icon-fontawesome": "^2.3.2",
|
"chonky-icon-fontawesome": "^2.3.2",
|
||||||
|
@ -39,11 +39,11 @@
|
||||||
"react-router-dom": "^6.22.0",
|
"react-router-dom": "^6.22.0",
|
||||||
"react-toastify": "^10.0.4",
|
"react-toastify": "^10.0.4",
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.7.4",
|
||||||
"vite": "^5.0.12"
|
"vite": "^5.1.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kubernetes/client-node": "^0.20.0",
|
"@kubernetes/client-node": "^0.20.0",
|
||||||
"aws-sdk": "^2.1550.0",
|
"aws-sdk": "^2.1555.0",
|
||||||
"basic-ftp": "^5.0.4",
|
"basic-ftp": "^5.0.4",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
|
@ -57,6 +57,7 @@
|
||||||
"pg-promise": "^11.5.4",
|
"pg-promise": "^11.5.4",
|
||||||
"postgres-migrations": "^5.3.0",
|
"postgres-migrations": "^5.3.0",
|
||||||
"rcon-client": "^4.2.4",
|
"rcon-client": "^4.2.4",
|
||||||
|
"react-dropzone": "^14.2.3",
|
||||||
"socket.io": "^4.7.4",
|
"socket.io": "^4.7.4",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
|
@ -10,7 +10,17 @@ import Toolbar from "@mui/material/Toolbar";
|
||||||
import TextEditor from "./TextEditor.jsx";
|
import TextEditor from "./TextEditor.jsx";
|
||||||
import { cairoAuthHeader } from "@mcl/util/auth.js";
|
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"];
|
const imageFileTypes = ["png", "jpeg", "jpg"];
|
||||||
|
|
||||||
export const supportedFileTypes = [...textFileTypes, ...imageFileTypes];
|
export const supportedFileTypes = [...textFileTypes, ...imageFileTypes];
|
||||||
|
@ -44,6 +54,7 @@ export default function FilePreview(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSave() {
|
async function onSave() {
|
||||||
|
if (!isTextFile) return;
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
const blob = new Blob([modifiedText], { type: "plain/text" });
|
const blob = new Blob([modifiedText], { type: "plain/text" });
|
||||||
formData.append("file", blob, name);
|
formData.append("file", blob, name);
|
||||||
|
@ -77,7 +88,7 @@ export default function FilePreview(props) {
|
||||||
<Toolbar sx={{ display: { sm: "none" } }} />
|
<Toolbar sx={{ display: { sm: "none" } }} />
|
||||||
<DialogTitle>{name}</DialogTitle>
|
<DialogTitle>{name}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<TextEditor text={fileText} onChange={editorChange} />
|
{isTextFile && <TextEditor text={fileText} onChange={editorChange} />}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button autoFocus onClick={dialogToggle}>
|
<Button autoFocus onClick={dialogToggle}>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { useState, useEffect, useMemo, useRef } from "react";
|
import { useState, useEffect, useMemo, useRef } from "react";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
|
import Dropzone from "react-dropzone";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FileBrowser,
|
FileBrowser,
|
||||||
FileContextMenu,
|
FileContextMenu,
|
||||||
|
@ -16,8 +18,9 @@ import {
|
||||||
createServerFolder,
|
createServerFolder,
|
||||||
deleteServerItem,
|
deleteServerItem,
|
||||||
getServerItem,
|
getServerItem,
|
||||||
|
moveServerItems,
|
||||||
|
previewServerItem,
|
||||||
} from "@mcl/queries";
|
} from "@mcl/queries";
|
||||||
import { previewServerItem } from "../../util/queries";
|
|
||||||
import { cairoAuthHeader } from "@mcl/util/auth.js";
|
import { cairoAuthHeader } from "@mcl/util/auth.js";
|
||||||
|
|
||||||
import { supportedFileTypes } from "./FilePreview.jsx";
|
import { supportedFileTypes } from "./FilePreview.jsx";
|
||||||
|
@ -32,6 +35,7 @@ export default function MineclusterFiles(props) {
|
||||||
ChonkyActions.DownloadFiles,
|
ChonkyActions.DownloadFiles,
|
||||||
ChonkyActions.CopyFiles,
|
ChonkyActions.CopyFiles,
|
||||||
ChonkyActions.DeleteFiles,
|
ChonkyActions.DeleteFiles,
|
||||||
|
ChonkyActions.MoveFiles,
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
@ -97,6 +101,10 @@ export default function MineclusterFiles(props) {
|
||||||
function uploadFileSelection(e) {
|
function uploadFileSelection(e) {
|
||||||
if (!e.target.files || e.target.files.length === 0) return;
|
if (!e.target.files || e.target.files.length === 0) return;
|
||||||
const { files } = e.target;
|
const { files } = e.target;
|
||||||
|
uploadMultipleFiles(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadMultipleFiles(files) {
|
||||||
Promise.all([...files].map((f) => uploadFile(f)))
|
Promise.all([...files].map((f) => uploadFile(f)))
|
||||||
.catch((e) => console.log("Error uploading a file", e))
|
.catch((e) => console.log("Error uploading a file", e))
|
||||||
.then(updateFiles);
|
.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) {
|
function fileClick(chonkyEvent) {
|
||||||
const { id: clickEvent, payload } = chonkyEvent;
|
const { id: clickEvent, payload } = chonkyEvent;
|
||||||
if (clickEvent === "open_parent_folder") return openParentFolder();
|
if (clickEvent === "open_parent_folder") return openParentFolder();
|
||||||
|
@ -141,11 +158,19 @@ export default function MineclusterFiles(props) {
|
||||||
return downloadFiles(chonkyEvent.state.selectedFilesForAction);
|
return downloadFiles(chonkyEvent.state.selectedFilesForAction);
|
||||||
if (clickEvent === "delete_files")
|
if (clickEvent === "delete_files")
|
||||||
return deleteItems(chonkyEvent.state.selectedFilesForAction);
|
return deleteItems(chonkyEvent.state.selectedFilesForAction);
|
||||||
|
if (clickEvent === "move_files") return moveFile(payload);
|
||||||
if (clickEvent !== "open_files") return; // console.log(clickEvent);
|
if (clickEvent !== "open_files") return; // console.log(clickEvent);
|
||||||
openItem(payload);
|
openItem(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="minecluster-files" sx={{ height: "calc(100vh - 6rem)" }}>
|
<Dropzone onDrop={uploadMultipleFiles}>
|
||||||
|
{({ getRootProps }) => (
|
||||||
|
<Box
|
||||||
|
className="minecluster-files"
|
||||||
|
sx={{ height: "calc(100vh - 6rem)" }}
|
||||||
|
onDrop={getRootProps().onDrop}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
id="file"
|
id="file"
|
||||||
|
@ -163,10 +188,11 @@ export default function MineclusterFiles(props) {
|
||||||
>
|
>
|
||||||
<FileNavbar />
|
<FileNavbar />
|
||||||
<FileToolbar />
|
<FileToolbar />
|
||||||
|
|
||||||
<FileList />
|
<FileList />
|
||||||
<FileContextMenu />
|
<FileContextMenu />
|
||||||
</FileBrowser>
|
</FileBrowser>
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
|
</Dropzone>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState } from "react";
|
||||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
|
@ -19,22 +19,19 @@ export default function RconDialog(props) {
|
||||||
const { server, open, dialogToggle } = props;
|
const { server, open, dialogToggle } = props;
|
||||||
const { name: serverName, id: serverId } = server ?? {};
|
const { name: serverName, id: serverId } = server ?? {};
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
const fullScreen = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
sx={
|
fullWidth
|
||||||
fullScreen
|
maxWidth="lg"
|
||||||
? {}
|
|
||||||
: { "& .mcl-MuiDialog-paper": { width: "80%", maxHeight: 555 } }
|
|
||||||
}
|
|
||||||
maxWidth="xs"
|
|
||||||
open={open}
|
open={open}
|
||||||
fullScreen={fullScreen}
|
fullScreen={fullScreen}
|
||||||
|
PaperProps={!fullScreen ? { sx: { height: "60%" } } : undefined}
|
||||||
>
|
>
|
||||||
<Toolbar sx={{ display: { sm: "none" } }} />
|
<Toolbar sx={{ display: { md: "none" } }} />
|
||||||
<DialogTitle>RCON - {serverName}</DialogTitle>
|
<DialogTitle>RCON - {serverName}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent sx={{ height: "100%" }}>
|
||||||
<RconView serverId={serverId} />
|
<RconView serverId={serverId} />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
|
|
|
@ -2,9 +2,21 @@ import { useState, useEffect, useRef } from "react";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
|
import Skeleton from "@mui/material/Skeleton";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
import RconSocket from "./RconSocket.js";
|
import RconSocket from "./RconSocket.js";
|
||||||
import "@mcl/css/rcon.css";
|
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) {
|
export default function RconView(props) {
|
||||||
const { serverId } = props;
|
const { serverId } = props;
|
||||||
const logsRef = useRef(0);
|
const logsRef = useRef(0);
|
||||||
|
@ -39,16 +51,31 @@ export default function RconView(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box sx={{ height: "100%", display: "flex", flexWrap: "wrap" }}>
|
||||||
<div className="rconLogsWrapper" ref={logsRef}>
|
<Box
|
||||||
{logs.map((v, k) => (
|
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}>
|
<Box key={k}>
|
||||||
{v}
|
<Typography variant="subtitle2">{v}</Typography>
|
||||||
<br />
|
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Box>
|
||||||
<Box className="rconActions">
|
<Box
|
||||||
|
className="rconActions"
|
||||||
|
sx={{ marginTop: "auto", paddingTop: "1rem", width: "100%" }}
|
||||||
|
>
|
||||||
<TextField
|
<TextField
|
||||||
id="outlined-basic"
|
id="outlined-basic"
|
||||||
label="Command"
|
label="Command"
|
||||||
|
@ -56,9 +83,12 @@ export default function RconView(props) {
|
||||||
value={cmd}
|
value={cmd}
|
||||||
onChange={updateCmd}
|
onChange={updateCmd}
|
||||||
disabled={!(rcon && rcon.rconLive && !rcon.rconError)}
|
disabled={!(rcon && rcon.rconLive && !rcon.rconError)}
|
||||||
|
sx={{ width: "100%" }}
|
||||||
/>
|
/>
|
||||||
{rcon && rcon.rconLive && !rcon.rconError && (
|
{rcon && rcon.rconLive && !rcon.rconError && (
|
||||||
<Button onClick={sendCommand}>Send</Button>
|
<Button onClick={sendCommand} sx={{ padding: "0 2rem" }}>
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
{!(rcon && rcon.rconLive && !rcon.rconError) && (
|
{!(rcon && rcon.rconLive && !rcon.rconError) && (
|
||||||
<Button color="secondary">Not Connected</Button>
|
<Button color="secondary">Not Connected</Button>
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
.rconLogsWrapper {
|
.rconLogsWrapper {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
max-height: 20rem;
|
max-height: calc(100% - 6rem);
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
.rconActions {
|
.rconActions {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
|
@ -64,6 +64,13 @@ export const createServerFolder = async (serverId, path) =>
|
||||||
export const deleteServerItem = async (serverId, path, isDir) =>
|
export const deleteServerItem = async (serverId, path, isDir) =>
|
||||||
fetchApiCore("/files/item", { id: serverId, path, isDir }, "DELETE");
|
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) {
|
export async function previewServerItem(serverId, path) {
|
||||||
const resp = await fetchApiCore("/files/item", { id: 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");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue