[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:
parent
4f19cf19d9
commit
fb57c03ba7
9 changed files with 63 additions and 45 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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();
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue