[FEATURE] Fixed file manager and adjusted starting display (#5)

Co-authored-by: Dunemask <dunemask@gmail.com>
Reviewed-on: https://gitea.dunemask.dev/elysium/minecluster/pulls/5
This commit is contained in:
dunemask 2023-12-22 18:30:48 +00:00
parent 4f19cf19d9
commit fb57c03ba7
9 changed files with 63 additions and 45 deletions

View file

@ -11,7 +11,6 @@ const kc = new k8s.KubeConfig();
kc.loadFromDefault(); kc.loadFromDefault();
const k8sMetrics = new k8s.Metrics(kc); const k8sMetrics = new k8s.Metrics(kc);
const k8sDeps = kc.makeApiClient(k8s.AppsV1Api);
const namespace = process.env.MCL_SERVER_NAMESPACE; const namespace = process.env.MCL_SERVER_NAMESPACE;
export async function startServerContainer(serverSpec) { export async function startServerContainer(serverSpec) {
@ -43,17 +42,26 @@ export async function stopServerContainer(serverSpec) {
export async function getInstances() { export async function getInstances() {
const serverDeployments = await getDeployments(); const serverDeployments = await getDeployments();
const podMetricsResponse = await k8sMetrics.getPodMetrics(namespace); const podMetricsResponse = await k8sMetrics.getPodMetrics(namespace);
var name, metrics, started; var name, metrics, services, serverAvailable, ftpAvailable;
const serverInstances = serverDeployments.map((s) => { const serverInstances = serverDeployments.map((s) => {
name = s.metadata.annotations["minecluster.dunemask.net/server-name"]; name = s.metadata.annotations["minecluster.dunemask.net/server-name"];
metrics = null; metrics = null;
started = !!s.spec.template.spec.containers.find((c) => const { containers } = s.spec.template.spec;
c.name.includes(`mcl-${name}-server`), services = containers.map(({ name }) => name.split("-").pop());
const serverStatusList = s.status.conditions.map(
({ type: statusType, status: sts }) => ({ statusType, sts }),
); );
const deploymentAvailable =
serverStatusList.find(
(ss) => ss.statusType === "Available" && ss.sts === "True",
) !== undefined;
serverAvailable = services.includes(`server`) && deploymentAvailable;
ftpAvailable = services.includes("ftp") && deploymentAvailable;
const pod = podMetricsResponse.items.find(({ metadata: md }) => { const pod = podMetricsResponse.items.find(({ metadata: md }) => {
return md.labels && md.labels.app && md.labels.app === `mcl-${name}-app`; return md.labels && md.labels.app && md.labels.app === `mcl-${name}-app`;
}); });
if (started && pod) { if (serverAvailable && pod) {
const podCpus = pod.containers.map( const podCpus = pod.containers.map(
({ usage }) => parseInt(usage.cpu) / 1_000_000, ({ usage }) => parseInt(usage.cpu) / 1_000_000,
); );
@ -65,7 +73,7 @@ export async function getInstances() {
memory: Math.ceil(podMems.reduce((a, b) => a + b)), memory: Math.ceil(podMems.reduce((a, b) => a + b)),
}; };
} }
return { name, metrics, started }; return { name, metrics, services, serverAvailable, ftpAvailable };
}); });
return serverInstances; return serverInstances;
} }

View file

@ -38,26 +38,33 @@ export default function MineclusterFiles(props) {
const [dirStack, setDirStack] = useState(["."]); const [dirStack, setDirStack] = useState(["."]);
const [files, setFiles] = useState([]); const [files, setFiles] = useState([]);
const updateFiles = () => const updateFiles = () => {
getServerFiles(serverName, dirStack.join("/")).then((f) => const dir = dirStack.join("/");
setFiles(f ?? []), getServerFiles(serverName, dir).then((f) => {
); const files = f.map((fi) => ({ ...fi, id: `${dir}/${fi.name}` }));
setFiles(files ?? []);
});
};
useEffect(() => { useEffect(() => {
updateFiles(); updateFiles();
}, [dirStack]); }, [dirStack]);
const getFolderChain = () => { const getFolderChain = () => {
if (dirStack.length === 1) return [{ id: "home", name: "/", isDir: true }]; if (dirStack.length === 1) return [{ id: "./", name: "Home", isDir: true }];
return dirStack.map((d, i) => ({ id: `${d}-${i}`, name: d, isDir: true })); return dirStack.map((d, i) => ({
id: `${dirStack.slice(0, i + 1).join("/")}`,
name: i === 0 ? "Home" : d,
isDir: true,
}));
}; };
const openParentFolder = () => setDirStack(dirStack.slice(0, -1)); const openParentFolder = () => setDirStack(dirStack.slice(0, -1));
function openFolder(payload) { function openFolder(payload) {
const { targetFile: file } = payload; const { targetFile: file } = payload;
if (!file || !file.isDir) return; if (file && file.isDir) return setDirStack(file.id.split("/"));
setDirStack([...dirStack, file.name]); if (file && !file.isDir) return downloadFiles([file]);
} }
function createFolder() { function createFolder() {
@ -101,13 +108,12 @@ export default function MineclusterFiles(props) {
getServerItem(serverName, f.name, [...dirStack, f.name].join("/")), getServerItem(serverName, f.name, [...dirStack, f.name].join("/")),
), ),
) )
.then(() => console.log("Done")) .then(() => console.log("Done downloading files!"))
.catch((e) => console.error("Error Downloading files!", e)); .catch((e) => console.error("Error Downloading files!", e));
} }
function fileClick(chonkyEvent) { function fileClick(chonkyEvent) {
const { id: clickEvent, payload } = chonkyEvent; const { id: clickEvent, payload } = chonkyEvent;
console.log(chonkyEvent);
if (clickEvent === "open_parent_folder") return openParentFolder(); if (clickEvent === "open_parent_folder") return openParentFolder();
if (clickEvent === "create_folder") return createFolder(); if (clickEvent === "create_folder") return createFolder();
if (clickEvent === "upload_files") return inputRef.current.click(); if (clickEvent === "upload_files") return inputRef.current.click();
@ -115,7 +121,7 @@ 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 !== "open_files") return console.log(clickEvent); if (clickEvent !== "open_files") return; // console.log(clickEvent);
openFolder(payload); openFolder(payload);
} }
return ( return (

View file

@ -19,12 +19,12 @@ import { Link } from "react-router-dom";
export default function ServerCard(props) { export default function ServerCard(props) {
const { server, openRcon } = props; const { server, openRcon } = props;
const { name, metrics, started } = server; const { name, metrics, ftpAvailable, serverAvailable, services } = server;
const startServer = useStartServer(name); const startServer = useStartServer(name);
const stopServer = useStopServer(name); const stopServer = useStopServer(name);
const deleteServer = useDeleteServer(name); const deleteServer = useDeleteServer(name);
function toggleRcon() { function toggleRcon() {
if (!started) return; if (!services.includes("server")) return;
openRcon(); openRcon();
} }
@ -60,14 +60,26 @@ export default function ServerCard(props) {
</Box> </Box>
)} )}
<Chip <Chip
label={started ? "Online" : "Offline"} label={
color={started ? "success" : "error"} services.includes("server")
? serverAvailable
? "Online"
: "Starting"
: "Offline"
}
color={
services.includes("server")
? serverAvailable
? "success"
: "info"
: "error"
}
className="server-card-status-indicator" className="server-card-status-indicator"
/> />
</CardContent> </CardContent>
<div className="server-card-actions-wrapper"> <div className="server-card-actions-wrapper">
<CardActions className="server-card-actions server-card-element"> <CardActions className="server-card-actions server-card-element">
{started && ( {services.includes("server") && (
<IconButton <IconButton
color="error" color="error"
aria-label="Stop Server" aria-label="Stop Server"
@ -77,7 +89,7 @@ export default function ServerCard(props) {
<StopIcon /> <StopIcon />
</IconButton> </IconButton>
)} )}
{!started && ( {!services.includes("server") && (
<IconButton <IconButton
color="success" color="success"
aria-label="Start Server" aria-label="Start Server"
@ -92,7 +104,7 @@ export default function ServerCard(props) {
aria-label="RCON" aria-label="RCON"
onClick={toggleRcon} onClick={toggleRcon}
size="large" size="large"
disabled={!started} disabled={!services.includes("server")}
> >
<TerminalIcon /> <TerminalIcon />
</IconButton> </IconButton>
@ -106,11 +118,12 @@ export default function ServerCard(props) {
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
<IconButton <IconButton
color="primary" color="info"
aria-label="Files" aria-label="Files"
size="large" size="large"
component={Link} component={Link}
to={`/mcl/files?server=${name}`} to={`/mcl/files?server=${name}`}
disabled={!services.includes("ftp")}
> >
<FolderIcon /> <FolderIcon />
</IconButton> </IconButton>

View file

@ -23,7 +23,7 @@
right: 0; right: 0;
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
z-index: 1302; z-index: 1302;
background-color: black; background-color: #29985c;
} }
.view > header > div > div > a { .view > header > div > div > a {
height: 40px; height: 40px;

View file

@ -36,12 +36,7 @@ export default function MCLMenu() {
theme.zIndex.modal + 2 - (isDrawer ? 1 : 0); theme.zIndex.modal + 2 - (isDrawer ? 1 : 0);
return ( return (
<AppBar <AppBar position="fixed" color="primary" sx={{ zIndex: drawerIndex() }}>
position="fixed"
color="primary"
sx={{ zIndex: drawerIndex(), bgcolor: "black" }}
enableColorOnDark={true}
>
<Box <Box
sx={{ flexGrow: 1, margin: "0 20px", color: "white" }} sx={{ flexGrow: 1, margin: "0 20px", color: "white" }}
className="appbar-items" className="appbar-items"

View file

@ -1,4 +1,5 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import Autocomplete from "@mui/material/Autocomplete"; import Autocomplete from "@mui/material/Autocomplete";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
@ -24,6 +25,7 @@ export default function Create() {
const [wl, setWl] = useState([]); const [wl, setWl] = useState([]);
const [ops, setOps] = useState([]); const [ops, setOps] = useState([]);
const [spec, setSpec] = useState(defaultServer); const [spec, setSpec] = useState(defaultServer);
const nav = useNavigate();
const versionList = useVersionList(); const versionList = useVersionList();
const [versions, setVersions] = useState(["latest"]); const [versions, setVersions] = useState(["latest"]);
const createServer = useCreateServer(spec); const createServer = useCreateServer(spec);
@ -86,11 +88,11 @@ export default function Create() {
updateSpec("whitelist", newWl.join(",")); updateSpec("whitelist", newWl.join(","));
}; };
const opUpdate = (e) => alert("Op not implimented"); async function upsertSpec() {
function upsertSpec() {
if (validateSpec() !== "validated") return; if (validateSpec() !== "validated") return;
createServer(spec); createServer(spec)
.then(() => nav("/"))
.catch(alert);
} }
function validateSpec() { function validateSpec() {

View file

@ -53,12 +53,7 @@ export default function Home() {
<ServerCard key={k} server={s} openRcon={openRcon(s.name)} /> <ServerCard key={k} server={s} openRcon={openRcon(s.name)} />
))} ))}
</Box> </Box>
<RconDialog <RconDialog open={rdOpen} dialogToggle={rconToggle} serverName={server} />
keepMounted
open={rdOpen}
dialogToggle={rconToggle}
serverName={server}
/>
<Button <Button
component={Link} component={Link}
to="/mcl/create" to="/mcl/create"

View file

@ -46,7 +46,7 @@ export const createServerFolder = async (server, path) =>
fetchApiCore("/files/folder", { fetchApiCore("/files/folder", {
name: server, name: server,
path, path,
}); /*postJsonApi("/files/folder", {name: server, path});*/ });
export const deleteServerItem = async (server, path, isDir) => export const deleteServerItem = async (server, path, isDir) =>
fetchApiCore("/files/item", { name: server, path, isDir }, "DELETE"); fetchApiCore("/files/item", { name: server, path, isDir }, "DELETE");
@ -106,6 +106,5 @@ const postJsonApi = (subPath, body, invalidate, method = "POST") => {
body: JSON.stringify(body), body: JSON.stringify(body),
}); });
if (invalidate) qc.invalidateQueries([invalidate]); if (invalidate) qc.invalidateQueries([invalidate]);
return res.json();
}; };
}; };

View file

@ -5,7 +5,7 @@ const themeOptions = {
palette: { palette: {
mode: "light", mode: "light",
primary: { primary: {
main: "rgba(109,216,144,255)", main: "#29985c",
}, },
secondary: { secondary: {
main: "#f50057", main: "#f50057",