Merge pull request #365 from etkecc/add-etke-server-commands
Add etke server commands panel inside server status page
This commit is contained in:
commit
4404840b16
@ -118,6 +118,7 @@ The following list contains such features - they are only available for [etke.cc
|
||||
|
||||
* 📊 [Server Status indicator and page](https://github.com/etkecc/synapse-admin/pull/182)
|
||||
* 📬 [Server Notifications indicator and page](https://github.com/etkecc/synapse-admin/pull/240)
|
||||
* 🛠️ [Server Commands panel](https://github.com/etkecc/synapse-admin/pull/365)
|
||||
|
||||
### Development
|
||||
|
||||
|
BIN
screenshots/etke.cc/server-commands/panel.webp
Normal file
BIN
screenshots/etke.cc/server-commands/panel.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
screenshots/etke.cc/server-status/indicator-sidebar.webp
Normal file
BIN
screenshots/etke.cc/server-status/indicator-sidebar.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
@ -1,4 +1,4 @@
|
||||
import { CheckForApplicationUpdate, AppBar, TitlePortal, InspectorButton, Confirm, Layout, Logout, Menu, useLogout, UserMenu } from "react-admin";
|
||||
import { CheckForApplicationUpdate, AppBar, TitlePortal, InspectorButton, Confirm, Layout, Logout, Menu, useLogout, UserMenu, useStore } from "react-admin";
|
||||
import { LoginMethod } from "../pages/LoginPage";
|
||||
import { useEffect, useState, Suspense } from "react";
|
||||
import { Icons, DefaultIcon } from "../utils/icons";
|
||||
@ -6,6 +6,8 @@ import { MenuItem, GetConfig, ClearConfig } from "../utils/config";
|
||||
import Footer from "./Footer";
|
||||
import ServerStatusBadge from "./etke.cc/ServerStatusBadge";
|
||||
import { ServerNotificationsBadge } from "./etke.cc/ServerNotificationsBadge";
|
||||
import { ServerProcessResponse, ServerStatusResponse } from "../synapse/dataProvider";
|
||||
import { ServerStatusStyledBadge } from "./etke.cc/ServerStatusBadge";
|
||||
|
||||
const AdminUserMenu = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
@ -59,9 +61,20 @@ const AdminAppBar = () => {
|
||||
const AdminMenu = (props) => {
|
||||
const [menu, setMenu] = useState([] as MenuItem[]);
|
||||
useEffect(() => setMenu(GetConfig().menu), []);
|
||||
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" });
|
||||
const [serverStatus, setServerStatus] = useStore<ServerStatusResponse>("serverStatus", { success: false, ok: false, host: "", results: [] });
|
||||
|
||||
return (
|
||||
<Menu {...props}>
|
||||
{menu && menu.length > 0 && <Menu.Item to="/server_status" leftIcon={
|
||||
<ServerStatusStyledBadge
|
||||
inSidebar={true}
|
||||
command={serverProcess.command}
|
||||
locked_at={serverProcess.locked_at}
|
||||
isOkay={serverStatus.ok} />
|
||||
}
|
||||
primaryText="Server Status" />
|
||||
}
|
||||
<Menu.ResourceItems />
|
||||
{menu && menu.map((item, index) => {
|
||||
const { url, icon, label } = item;
|
||||
|
@ -18,6 +18,10 @@ In the application bar the new monitoring icon is displayed that shows the curre
|
||||
* 🟡 (yellow) - the server is up and running, but there is a command in progress (likely [maintenance](https://etke.cc/help/extras/scheduler/#maintenance)), so some temporary issues may occur - that's totally fine
|
||||
* 🔴 (red) - there is at least 1 issue with one of the server's components
|
||||
|
||||

|
||||
|
||||
The same icon (and link to the [Server Status page](#server-status-page)) is displayed in the sidebar.
|
||||
|
||||
### Server Status page
|
||||
|
||||

|
||||
@ -29,6 +33,8 @@ Server Status page. This page contains the following information:
|
||||
* Details about the currently running command (if any)
|
||||
* Details about the server's components statuses (up/down with error details and suggested actions) by categories
|
||||
|
||||
This is [a Monitoring report](https://etke.cc/services/monitoring/)
|
||||
|
||||
### Server Notifications icon
|
||||
|
||||

|
||||
@ -40,3 +46,11 @@ In the application bar the new notifications icon is displayed that shows the nu
|
||||

|
||||
|
||||
When you click on a notification from the [Server Notifications icon](#server-notifications-icon)'s list in the application bar, you will be redirected to the Server Notifications page. This page contains the full text of all the notifications you have about your server.
|
||||
|
||||
### Server Commands Panel
|
||||
|
||||

|
||||
|
||||
When you open [Server Status page](#server-status-page), you will see the Server Commands panel. This panel contains all
|
||||
[the commands](https://etke.cc/help/extras/scheduler/#commands) you can run on your server in 1 click. Once command is finished, you will get a notification about the
|
||||
result.
|
||||
|
182
src/components/etke.cc/ServerCommandsPanel.tsx
Normal file
182
src/components/etke.cc/ServerCommandsPanel.tsx
Normal file
@ -0,0 +1,182 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Loading, useDataProvider, useCreatePath, useStore } from "react-admin";
|
||||
import { useAppContext } from "../../Context";
|
||||
import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Alert, TextField, Box } from "@mui/material";
|
||||
import { PlayArrow, CheckCircle } from "@mui/icons-material";
|
||||
import { Icons } from "../../utils/icons";
|
||||
import { ServerCommand, ServerProcessResponse } from "../../synapse/dataProvider";
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
|
||||
const renderIcon = (icon: string) => {
|
||||
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
|
||||
return IconComponent ? <IconComponent sx={{ verticalAlign: "middle", mr: 1 }} /> : null;
|
||||
}
|
||||
|
||||
const ServerCommandsPanel = () => {
|
||||
const { etkeccAdmin } = useAppContext();
|
||||
const createPath = useCreatePath();
|
||||
|
||||
const [ isLoading, setLoading ] = useState(true);
|
||||
const [serverCommands, setServerCommands] = useState<{ [key: string]: ServerCommand }>({});
|
||||
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" });
|
||||
const [commandIsRunning, setCommandIsRunning] = useState<boolean>(serverProcess.command !== "");
|
||||
const [commandResult, setCommandResult] = useState<any[]>([]);
|
||||
|
||||
const dataProvider = useDataProvider();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchIsAdmin = async () => {
|
||||
const serverCommandsResponse = await dataProvider.getServerCommands(etkeccAdmin);
|
||||
if (serverCommandsResponse) {
|
||||
const serverCommands = serverCommandsResponse;
|
||||
Object.keys(serverCommandsResponse).forEach((command: string) => {
|
||||
serverCommands[command].additionalArgs = "";
|
||||
});
|
||||
setServerCommands(serverCommands);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
fetchIsAdmin();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (serverProcess.command === "") {
|
||||
setCommandIsRunning(false);
|
||||
}
|
||||
}, [serverProcess]);
|
||||
|
||||
const setCommandAdditionalArgs = (command: string, additionalArgs: string) => {
|
||||
const updatedServerCommands = {...serverCommands};
|
||||
updatedServerCommands[command].additionalArgs = additionalArgs;
|
||||
setServerCommands(updatedServerCommands);
|
||||
}
|
||||
|
||||
const runCommand = async (command: string) => {
|
||||
setCommandResult([]);
|
||||
setCommandIsRunning(true);
|
||||
|
||||
try {
|
||||
const additionalArgs = serverCommands[command].additionalArgs || "";
|
||||
const requestParams = additionalArgs ? { args: additionalArgs } : {};
|
||||
|
||||
const response = await dataProvider.runServerCommand(etkeccAdmin, command, requestParams);
|
||||
|
||||
if (!response.success) {
|
||||
setCommandIsRunning(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update UI with success message
|
||||
const commandResults = buildCommandResultMessages(command, additionalArgs);
|
||||
setCommandResult(commandResults);
|
||||
|
||||
// Reset the additional args field
|
||||
resetCommandArgs(command);
|
||||
|
||||
// Update server process status
|
||||
await updateServerProcessStatus(serverCommands[command]);
|
||||
} catch (error) {
|
||||
setCommandIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const buildCommandResultMessages = (command: string, additionalArgs: string): React.ReactNode[] => {
|
||||
const results: React.ReactNode[] = [];
|
||||
|
||||
let commandScheduledText = `Command scheduled: ${command}`;
|
||||
if (additionalArgs) {
|
||||
commandScheduledText += `, with additional args: ${additionalArgs}`;
|
||||
}
|
||||
|
||||
results.push(<Box>{commandScheduledText}</Box>);
|
||||
results.push(
|
||||
<Box>
|
||||
Expect your result in the <Link to={createPath({ resource: "server_notifications", type: "list" })}>Notifications</Link> page soon.
|
||||
</Box>
|
||||
);
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
const resetCommandArgs = (command: string) => {
|
||||
const updatedServerCommands = {...serverCommands};
|
||||
updatedServerCommands[command].additionalArgs = "";
|
||||
setServerCommands(updatedServerCommands);
|
||||
};
|
||||
|
||||
const updateServerProcessStatus = async (command: ServerCommand) => {
|
||||
const commandIsLocking = command.with_lock;
|
||||
const serverProcess = await dataProvider.getServerRunningProcess(etkeccAdmin, true);
|
||||
if (!commandIsLocking && serverProcess.command === "") {
|
||||
// if command is not locking, we simulate the "lock" mechanism so notifications will be refetched
|
||||
serverProcess["command"] = command.name;
|
||||
serverProcess["locked_at"] = new Date().toISOString();
|
||||
}
|
||||
|
||||
setServerProcess({...serverProcess});
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />
|
||||
}
|
||||
|
||||
return (<>
|
||||
<h2>Server Commands</h2>
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 450 }} size="small" aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Command</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Object.entries(serverCommands).map(([command, { icon, args, description, additionalArgs }]) => (
|
||||
<TableRow
|
||||
key={command}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
<TableCell scope="row">
|
||||
<Box>
|
||||
{renderIcon(icon)}
|
||||
{command}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>{description}</TableCell>
|
||||
<TableCell>
|
||||
{args && <TextField
|
||||
size="small"
|
||||
variant="standard"
|
||||
onChange={(e) => {
|
||||
setCommandAdditionalArgs(command, e.target.value);
|
||||
}}
|
||||
value={additionalArgs}
|
||||
/>}
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
label="Run"
|
||||
startIcon={<PlayArrow />}
|
||||
onClick={() => { runCommand(command); }}
|
||||
disabled={commandIsRunning || (args && typeof additionalArgs === 'string' && additionalArgs.length === 0)}
|
||||
></Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{commandResult.length > 0 && <Alert icon={<CheckCircle fontSize="inherit" />} severity="success">
|
||||
{commandResult.map((result, index) => (
|
||||
<div key={index}>{result}</div>
|
||||
))}
|
||||
</Alert>}
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
export default ServerCommandsPanel;
|
@ -5,21 +5,28 @@ import { useDataProvider, useStore } from "react-admin";
|
||||
import { useNavigate } from "react-router";
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { useAppContext } from "../../Context";
|
||||
import { ServerNotificationsResponse } from "../../synapse/dataProvider";
|
||||
import { ServerNotificationsResponse, ServerProcessResponse } from "../../synapse/dataProvider";
|
||||
import { getTimeSince } from "../../utils/date";
|
||||
|
||||
// 5 minutes
|
||||
const SERVER_NOTIFICATIONS_INTERVAL_TIME = 300000;
|
||||
|
||||
const useServerNotifications = () => {
|
||||
const [serverNotifications, setServerNotifications] = useStore<ServerNotificationsResponse>("serverNotifications", { notifications: [], success: false });
|
||||
const [ serverProcess, setServerProcess ] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" });
|
||||
const { command, locked_at } = serverProcess;
|
||||
|
||||
const { etkeccAdmin } = useAppContext();
|
||||
const dataProvider = useDataProvider();
|
||||
const { notifications, success } = serverNotifications;
|
||||
|
||||
const fetchNotifications = async () => {
|
||||
const notificationsResponse: ServerNotificationsResponse = await dataProvider.getServerNotifications(etkeccAdmin);
|
||||
const notificationsResponse: ServerNotificationsResponse = await dataProvider.getServerNotifications(etkeccAdmin, command !== "");
|
||||
const serverNotifications = [...notificationsResponse.notifications];
|
||||
serverNotifications.reverse();
|
||||
setServerNotifications({
|
||||
...notificationsResponse,
|
||||
notifications: notificationsResponse.notifications,
|
||||
notifications: serverNotifications,
|
||||
success: notificationsResponse.success
|
||||
});
|
||||
};
|
||||
@ -35,21 +42,26 @@ const useServerNotifications = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let serverNotificationsInterval: NodeJS.Timeout;
|
||||
let serverNotificationsInterval: NodeJS.Timeout | null = null;
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
|
||||
if (etkeccAdmin) {
|
||||
fetchNotifications();
|
||||
setTimeout(() => {
|
||||
timeoutId = setTimeout(() => {
|
||||
// start the interval after the SERVER_NOTIFICATIONS_INTERVAL_TIME to avoid too many requests
|
||||
serverNotificationsInterval = setInterval(fetchNotifications, SERVER_NOTIFICATIONS_INTERVAL_TIME);
|
||||
}, SERVER_NOTIFICATIONS_INTERVAL_TIME);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
if (serverNotificationsInterval) {
|
||||
clearInterval(serverNotificationsInterval);
|
||||
}
|
||||
}
|
||||
}, [etkeccAdmin]);
|
||||
}, [etkeccAdmin, command, locked_at]);
|
||||
|
||||
return { success, notifications, deleteServerNotifications };
|
||||
};
|
||||
@ -108,6 +120,7 @@ export const ServerNotificationsBadge = () => {
|
||||
sx={{
|
||||
p: 1,
|
||||
maxHeight: "350px",
|
||||
paddingTop: 0,
|
||||
overflowY: "auto",
|
||||
minWidth: "300px",
|
||||
maxWidth: {
|
||||
@ -126,7 +139,6 @@ export const ServerNotificationsBadge = () => {
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
fontWeight: "bold",
|
||||
backgroundColor: "inherit",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">Notifications</Typography>
|
||||
@ -134,10 +146,14 @@ export const ServerNotificationsBadge = () => {
|
||||
</ListSubheader>
|
||||
<Divider />
|
||||
{notifications.map((notification, index) => {
|
||||
return (<Fragment key={notification.event_id ? notification.event_id : index }>
|
||||
return (<Fragment key={notification.event_id ? notification.event_id + index : index }>
|
||||
<ListItem
|
||||
onClick={() => handleSeeAllNotifications()}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
overflow: "hidden",
|
||||
"&:hover": {
|
||||
backgroundColor: "action.hover",
|
||||
cursor: "pointer"
|
||||
@ -151,12 +167,16 @@ export const ServerNotificationsBadge = () => {
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap"
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: notification.output.split("\n")[0] }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="body2" sx={{ color: theme.palette.text.secondary }}>{getTimeSince(notification.sent_at) + " ago"}</Typography>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
</Fragment>
|
||||
|
@ -4,9 +4,12 @@ import { useStore } from "react-admin"
|
||||
import dataProvider, { ServerNotificationsResponse } from "../../synapse/dataProvider"
|
||||
import { useAppContext } from "../../Context";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import { getTimeSince } from "../../utils/date";
|
||||
import { Tooltip } from "@mui/material";
|
||||
|
||||
const DisplayTime = ({ date }: { date: string }) => {
|
||||
const dateFromDateString = new Date(date);
|
||||
return <>{dateFromDateString.toLocaleString()}</>;
|
||||
const dateFromDateString = new Date(date.replace(" ", "T") + "Z");
|
||||
return <Tooltip title={dateFromDateString.toLocaleString()}>{<span>{getTimeSince(date) + " ago"}</span>}</Tooltip>;
|
||||
};
|
||||
|
||||
const ServerNotificationsPage = () => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Avatar, Badge, Theme, Tooltip } from "@mui/material";
|
||||
import { Avatar, Badge, Box, Theme, Tooltip } from "@mui/material";
|
||||
import { useEffect } from "react";
|
||||
import { useAppContext } from "../../Context";
|
||||
import { Button, useDataProvider, useStore } from "react-admin";
|
||||
@ -52,13 +52,15 @@ const SERVER_CURRENT_PROCCESS_INTERVAL_TIME = 5 * 1000;
|
||||
|
||||
const useServerStatus = () => {
|
||||
const [serverStatus, setServerStatus] = useStore<ServerStatusResponse>("serverStatus", { ok: false, success: false, host: "", results: [] });
|
||||
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" });
|
||||
const { command, locked_at } = serverProcess;
|
||||
const { etkeccAdmin } = useAppContext();
|
||||
const dataProvider = useDataProvider();
|
||||
const isOkay = serverStatus.ok;
|
||||
const successCheck = serverStatus.success;
|
||||
|
||||
const checkServerStatus = async () => {
|
||||
const serverStatus: ServerStatusResponse = await dataProvider.getServerStatus(etkeccAdmin);
|
||||
const serverStatus: ServerStatusResponse = await dataProvider.getServerStatus(etkeccAdmin, command !== "");
|
||||
setServerStatus({
|
||||
ok: serverStatus.ok,
|
||||
success: serverStatus.success,
|
||||
@ -68,10 +70,12 @@ const useServerStatus = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let serverStatusInterval: NodeJS.Timeout;
|
||||
let serverStatusInterval: NodeJS.Timeout | null = null;
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
|
||||
if (etkeccAdmin) {
|
||||
checkServerStatus();
|
||||
setTimeout(() => {
|
||||
timeoutId = setTimeout(() => {
|
||||
// start the interval after 10 seconds to avoid too many requests
|
||||
serverStatusInterval = setInterval(checkServerStatus, SERVER_STATUS_INTERVAL_TIME);
|
||||
}, 10000);
|
||||
@ -80,11 +84,14 @@ const useServerStatus = () => {
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
if (serverStatusInterval) {
|
||||
clearInterval(serverStatusInterval);
|
||||
}
|
||||
}
|
||||
}, [etkeccAdmin]);
|
||||
}, [etkeccAdmin, command]);
|
||||
|
||||
return { isOkay, successCheck };
|
||||
};
|
||||
@ -96,7 +103,7 @@ const useCurrentServerProcess = () => {
|
||||
const { command, locked_at } = serverProcess;
|
||||
|
||||
const checkServerRunningProcess = async () => {
|
||||
const serverProcess: ServerProcessResponse = await dataProvider.getServerRunningProcess(etkeccAdmin);
|
||||
const serverProcess: ServerProcessResponse = await dataProvider.getServerRunningProcess(etkeccAdmin, command !== "");
|
||||
setServerProcess({
|
||||
...serverProcess,
|
||||
command: serverProcess.command,
|
||||
@ -105,10 +112,12 @@ const useCurrentServerProcess = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let serverCheckInterval: NodeJS.Timeout;
|
||||
let serverCheckInterval: NodeJS.Timeout | null = null;
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
|
||||
if (etkeccAdmin) {
|
||||
checkServerRunningProcess();
|
||||
setTimeout(() => {
|
||||
timeoutId = setTimeout(() => {
|
||||
serverCheckInterval = setInterval(checkServerRunningProcess, SERVER_CURRENT_PROCCESS_INTERVAL_TIME);
|
||||
}, 5000);
|
||||
} else {
|
||||
@ -116,19 +125,48 @@ const useCurrentServerProcess = () => {
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
if (serverCheckInterval) {
|
||||
clearInterval(serverCheckInterval);
|
||||
}
|
||||
}
|
||||
}, [etkeccAdmin]);
|
||||
}, [etkeccAdmin, command]);
|
||||
|
||||
return { command, locked_at };
|
||||
};
|
||||
|
||||
export const ServerStatusStyledBadge = ({ command, locked_at, isOkay, inSidebar = false }: { command: string, locked_at: string, isOkay: boolean, inSidebar: boolean }) => {
|
||||
const theme = useTheme();
|
||||
let badgeBackgroundColor = isOkay ? theme.palette.success.light : theme.palette.error.main;
|
||||
let badgeColor = isOkay ? theme.palette.success.light : theme.palette.error.main;
|
||||
|
||||
if (command && locked_at) {
|
||||
badgeBackgroundColor = theme.palette.warning.main;
|
||||
badgeColor = theme.palette.warning.main;
|
||||
}
|
||||
let avatarBackgroundColor = theme.palette.mode === "dark" ? theme.palette.background.default : "#2196f3";
|
||||
if (inSidebar) {
|
||||
avatarBackgroundColor = theme.palette.grey[600];
|
||||
}
|
||||
|
||||
return <StyledBadge
|
||||
overlap="circular"
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
variant="dot"
|
||||
backgroundColor={badgeBackgroundColor}
|
||||
badgeColor={badgeColor}
|
||||
>
|
||||
<Avatar sx={{ height: 24, width: 24, background: avatarBackgroundColor }}>
|
||||
<MonitorHeartIcon sx={{ height: 22, width: 22, color: theme.palette.common.white }} />
|
||||
</Avatar>
|
||||
</StyledBadge>
|
||||
};
|
||||
|
||||
const ServerStatusBadge = () => {
|
||||
const { isOkay, successCheck } = useServerStatus();
|
||||
const { command, locked_at } = useCurrentServerProcess();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!successCheck) {
|
||||
@ -140,28 +178,16 @@ const ServerStatusBadge = () => {
|
||||
};
|
||||
|
||||
let tooltipText = "Click to view Server Status";
|
||||
let badgeBackgroundColor = isOkay ? theme.palette.success.light : theme.palette.error.main;
|
||||
let badgeColor = isOkay ? theme.palette.success.light : theme.palette.error.main;
|
||||
|
||||
if (command && locked_at) {
|
||||
badgeBackgroundColor = theme.palette.warning.main;
|
||||
badgeColor = theme.palette.warning.main;
|
||||
tooltipText = `Running: ${command}; ${tooltipText}`;
|
||||
}
|
||||
|
||||
return <Button onClick={handleServerStatusClick} size="medium" sx={{ minWidth: "auto", ".MuiButton-startIcon": { m: 0 }}}>
|
||||
<Tooltip title={tooltipText} sx={{ cursor: "pointer" }}>
|
||||
<StyledBadge
|
||||
overlap="circular"
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
variant="dot"
|
||||
backgroundColor={badgeBackgroundColor}
|
||||
badgeColor={badgeColor}
|
||||
>
|
||||
<Avatar sx={{ height: 24, width: 24, background: theme.palette.mode === "dark" ? theme.palette.background.default : "#2196f3" }}>
|
||||
<MonitorHeartIcon sx={{ height: 22, width: 22, color: theme.palette.common.white }} />
|
||||
</Avatar>
|
||||
</StyledBadge>
|
||||
<Box>
|
||||
<ServerStatusStyledBadge inSidebar={false} command={command || ""} locked_at={locked_at || ""} isOkay={isOkay} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
};
|
||||
|
@ -4,16 +4,8 @@ import CheckIcon from '@mui/icons-material/Check';
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import EngineeringIcon from '@mui/icons-material/Engineering';
|
||||
import { ServerProcessResponse, ServerStatusComponent, ServerStatusResponse } from "../../synapse/dataProvider";
|
||||
|
||||
const getTimeSince = (date: string) => {
|
||||
const now = new Date();
|
||||
const past = new Date(date);
|
||||
const diffInMinutes = Math.floor((now.getTime() - past.getTime()) / (1000 * 60));
|
||||
|
||||
if (diffInMinutes < 1) return "a couple of seconds";
|
||||
if (diffInMinutes === 1) return "1 minute";
|
||||
return `${diffInMinutes} minutes`;
|
||||
};
|
||||
import ServerCommandsPanel from "./ServerCommandsPanel";
|
||||
import { getTimeSince } from "../../utils/date";
|
||||
|
||||
const StatusChip = ({ isOkay, size = "medium", command }: { isOkay: boolean, size?: "small" | "medium", command?: string }) => {
|
||||
let label = "OK";
|
||||
@ -102,6 +94,8 @@ const ServerStatusPage = () => {
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<ServerCommandsPanel />
|
||||
|
||||
<Stack spacing={2} direction="row">
|
||||
{Object.keys(groupedResults).map((category, idx) => (
|
||||
<Box key={`category_${category}`} sx={{ flex: 1 }}>
|
||||
|
@ -2,7 +2,7 @@ import { AuthProvider, HttpError, Options, fetchUtils } from "react-admin";
|
||||
|
||||
import { MatrixError, displayError } from "../utils/error";
|
||||
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
|
||||
import { FetchConfig, ClearConfig } from "../utils/config";
|
||||
import { FetchConfig, ClearConfig, GetConfig } from "../utils/config";
|
||||
import decodeURLComponent from "../utils/decodeURLComponent";
|
||||
|
||||
const authProvider: AuthProvider = {
|
||||
@ -81,9 +81,16 @@ const authProvider: AuthProvider = {
|
||||
localStorage.setItem("access_token", accessToken ? accessToken : json.access_token);
|
||||
localStorage.setItem("device_id", json.device_id);
|
||||
localStorage.setItem("login_type", accessToken ? "accessToken" : "credentials");
|
||||
await FetchConfig();
|
||||
|
||||
return Promise.resolve({redirectTo: "/"});
|
||||
await FetchConfig();
|
||||
const config = GetConfig();
|
||||
let pageToRedirectTo = "/";
|
||||
|
||||
if (config && config.etkeccAdmin) {
|
||||
pageToRedirectTo = "/server_status";
|
||||
}
|
||||
|
||||
return Promise.resolve({redirectTo: pageToRedirectTo});
|
||||
} catch(err) {
|
||||
const error = err as HttpError;
|
||||
const errorStatus = error.status;
|
||||
|
@ -300,8 +300,8 @@ export interface ServerStatusResponse {
|
||||
}
|
||||
|
||||
export interface ServerProcessResponse {
|
||||
locked_at?: string;
|
||||
command?: string;
|
||||
locked_at: string;
|
||||
command: string;
|
||||
}
|
||||
|
||||
export interface ServerNotification {
|
||||
@ -315,6 +315,19 @@ export interface ServerNotificationsResponse {
|
||||
notifications: ServerNotification[];
|
||||
}
|
||||
|
||||
export interface ServerCommand {
|
||||
icon: string;
|
||||
name: string;
|
||||
description: string;
|
||||
args: boolean;
|
||||
with_lock: boolean;
|
||||
additionalArgs?: string;
|
||||
}
|
||||
|
||||
export interface ServerCommandsResponse {
|
||||
[command: string]: ServerCommand;
|
||||
}
|
||||
|
||||
export interface SynapseDataProvider extends DataProvider {
|
||||
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||
purgeRemoteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||
@ -329,6 +342,7 @@ export interface SynapseDataProvider extends DataProvider {
|
||||
getServerStatus: (etkeAdminUrl: string) => Promise<ServerStatusResponse>;
|
||||
getServerNotifications: (etkeAdminUrl: string) => Promise<ServerNotificationsResponse>;
|
||||
deleteServerNotifications: (etkeAdminUrl: string) => Promise<{ success: boolean }>;
|
||||
getServerCommands: (etkeAdminUrl: string) => Promise<ServerCommandsResponse>;
|
||||
}
|
||||
|
||||
const resourceMap = {
|
||||
@ -994,12 +1008,17 @@ const baseDataProvider: SynapseDataProvider = {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
getServerRunningProcess: async (runningProcessUrl: string): Promise<ServerProcessResponse> => {
|
||||
getServerRunningProcess: async (etkeAdminUrl: string, burstCache: boolean = false): Promise<ServerProcessResponse> => {
|
||||
const locked_at = "";
|
||||
const command = "";
|
||||
|
||||
let serverURL = `${etkeAdminUrl}/lock`;
|
||||
if (burstCache) {
|
||||
serverURL += `?time=${new Date().getTime()}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${runningProcessUrl}/lock`, {
|
||||
const response = await fetch(serverURL, {
|
||||
headers: {
|
||||
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
|
||||
}
|
||||
@ -1024,9 +1043,14 @@ const baseDataProvider: SynapseDataProvider = {
|
||||
|
||||
return { locked_at, command };
|
||||
},
|
||||
getServerStatus: async (serverStatusUrl: string): Promise<ServerStatusResponse> => {
|
||||
getServerStatus: async (etkeAdminUrl: string, burstCache: boolean = false): Promise<ServerStatusResponse> => {
|
||||
let serverURL = `${etkeAdminUrl}/status`;
|
||||
if (burstCache) {
|
||||
serverURL += `?time=${new Date().getTime()}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${serverStatusUrl}/status`, {
|
||||
const response = await fetch(serverURL, {
|
||||
headers: {
|
||||
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
|
||||
}
|
||||
@ -1048,9 +1072,14 @@ const baseDataProvider: SynapseDataProvider = {
|
||||
|
||||
return { success: false, ok: false, host: "", results: [] };
|
||||
},
|
||||
getServerNotifications: async (serverNotificationsUrl: string): Promise<ServerNotificationsResponse> => {
|
||||
getServerNotifications: async (serverNotificationsUrl: string, burstCache: boolean = false): Promise<ServerNotificationsResponse> => {
|
||||
let serverURL = `${serverNotificationsUrl}/notifications`;
|
||||
if (burstCache) {
|
||||
serverURL += `?time=${new Date().getTime()}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${serverNotificationsUrl}/notifications`, {
|
||||
const response = await fetch(serverURL, {
|
||||
headers: {
|
||||
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
|
||||
}
|
||||
@ -1101,6 +1130,66 @@ const baseDataProvider: SynapseDataProvider = {
|
||||
}
|
||||
|
||||
return { success: false };
|
||||
},
|
||||
getServerCommands: async (serverCommandsUrl: string) => {
|
||||
try {
|
||||
const response = await fetch(`${serverCommandsUrl}/commands`, {
|
||||
headers: {
|
||||
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.error(`Error fetching server commands: ${response.status} ${response.statusText}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
const status = response.status;
|
||||
|
||||
if (status === 200) {
|
||||
const json = await response.json();
|
||||
return json as ServerCommandsResponse;
|
||||
}
|
||||
|
||||
return {};
|
||||
} catch (error) {
|
||||
console.error("Error fetching server commands, error");
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
runServerCommand: async (serverCommandsUrl: string, command: string, additionalArgs: Record<string, any> = {}) => {
|
||||
const endpoint_url = `${serverCommandsUrl}/commands`;
|
||||
const body = {
|
||||
command: command,
|
||||
...additionalArgs
|
||||
}
|
||||
const response = await fetch(endpoint_url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Error running server command: ${response.status} ${response.statusText}`);
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
const status = response.status;
|
||||
|
||||
if (status === 204) {
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -26,3 +26,29 @@ export const dateFormatter = (v: string | number | Date | undefined | null): str
|
||||
// target format yyyy-MM-ddThh:mm
|
||||
return `${year}-${month}-${day}T${hour}:${minute}`;
|
||||
};
|
||||
|
||||
// assuming date is in format "2025-02-26 20:52:00" where no timezone is specified
|
||||
export const getTimeSince = (dateToCompare: string) => {
|
||||
const nowUTC = new Date().getTime();
|
||||
if (!dateToCompare.includes("Z")) {
|
||||
dateToCompare = dateToCompare + "Z";
|
||||
}
|
||||
const past = new Date(dateToCompare);
|
||||
|
||||
const pastUTC = past.getTime();
|
||||
const diffInMs = nowUTC - pastUTC;
|
||||
|
||||
const diffInMinutes = Math.floor(diffInMs / (1000 * 60));
|
||||
|
||||
if (diffInMinutes < 1) return "a couple of seconds";
|
||||
if (diffInMinutes === 1) return "1 minute";
|
||||
if (diffInMinutes < 60) return `${diffInMinutes} minutes`;
|
||||
if (diffInMinutes < 120) return "1 hour";
|
||||
if (diffInMinutes < 24 * 60) return `${Math.floor(diffInMinutes / 60)} hours`;
|
||||
if (diffInMinutes < 48 * 60) return "1 day";
|
||||
if (diffInMinutes < 7 * 24 * 60) return `${Math.floor(diffInMinutes / (24 * 60))} days`;
|
||||
if (diffInMinutes < 14 * 24 * 60) return "1 week";
|
||||
if (diffInMinutes < 30 * 24 * 60) return `${Math.floor(diffInMinutes / (7 * 24 * 60))} weeks`;
|
||||
if (diffInMinutes < 60 * 24 * 60) return "1 month";
|
||||
return `${Math.floor(diffInMinutes / (30 * 24 * 60))} months`;
|
||||
};
|
@ -3,6 +3,11 @@ import EngineeringIcon from '@mui/icons-material/Engineering';
|
||||
import HelpCenterIcon from '@mui/icons-material/HelpCenter';
|
||||
import SupportAgentIcon from '@mui/icons-material/SupportAgent';
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
||||
import PieChartIcon from '@mui/icons-material/PieChart';
|
||||
import UpgradeIcon from '@mui/icons-material/Upgrade';
|
||||
import RouterIcon from '@mui/icons-material/Router';
|
||||
import PriceCheckIcon from '@mui/icons-material/PriceCheck';
|
||||
import RestartAltIcon from '@mui/icons-material/RestartAlt';
|
||||
|
||||
export const Icons = {
|
||||
Announcement: AnnouncementIcon,
|
||||
@ -10,6 +15,11 @@ export const Icons = {
|
||||
HelpCenter: HelpCenterIcon,
|
||||
SupportAgent: SupportAgentIcon,
|
||||
Default: OpenInNewIcon,
|
||||
PieChart: PieChartIcon,
|
||||
Upgrade: UpgradeIcon,
|
||||
Router: RouterIcon,
|
||||
PriceCheck: PriceCheckIcon,
|
||||
RestartAlt: RestartAltIcon,
|
||||
// Add more icons as needed
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user