diff --git a/src/components/AdminLayout.tsx b/src/components/AdminLayout.tsx index b87ddb6..e70135e 100644 --- a/src/components/AdminLayout.tsx +++ b/src/components/AdminLayout.tsx @@ -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,10 +61,20 @@ const AdminAppBar = () => { const AdminMenu = (props) => { const [menu, setMenu] = useState([] as MenuItem[]); useEffect(() => setMenu(GetConfig().menu), []); + const [serverProcess, setServerProcess] = useStore("serverProcess", { command: "", locked_at: "" }); + const [serverStatus, setServerStatus] = useStore("serverStatus", { success: false, ok: false, host: "", results: [] }); return ( + {menu && + } + primaryText="Server Status" /> + } {menu && menu.map((item, index) => { const { url, icon, label } = item; const IconComponent = Icons[icon] as React.ComponentType | undefined; diff --git a/src/components/etke.cc/ServerCommandsPanel.tsx b/src/components/etke.cc/ServerCommandsPanel.tsx new file mode 100644 index 0000000..3ec85d5 --- /dev/null +++ b/src/components/etke.cc/ServerCommandsPanel.tsx @@ -0,0 +1,141 @@ +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 | undefined; + return IconComponent ? : 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("serverProcess", { command: "", locked_at: "" }); + const [commandIsRunning, setCommandIsRunning] = useState(false); + const [commandResult, setCommandResult] = useState([]); + + 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] = serverCommandsResponse[command]; + serverCommands[command].additionalArgs = ""; + }); + setServerCommands(serverCommands); + } + setLoading(false); + } + fetchIsAdmin(); + }, []); + + const setCommandAdditionalArgs = (command: string, additionalArgs: string) => { + const updatedServerCommands = {...serverCommands}; + updatedServerCommands[command].additionalArgs = additionalArgs; + setServerCommands(updatedServerCommands); + } + + const runCommand = async (command: string) => { + setCommandResult([]); + setCommandIsRunning(true); + + const response = await dataProvider.runServerCommand(etkeccAdmin, command); + + setCommandIsRunning(false); + if (!response.success) { + return; + } + + const updatedServerCommands = {...serverCommands}; + const commandResult: React.ReactNode[] = []; + + let commandScheduledText = `Command scheduled: ${command}`; + if (serverCommands[command].additionalArgs) { + commandScheduledText += `, with additional args: ${serverCommands[command].additionalArgs}`; + } + commandResult.push({commandScheduledText}); + commandResult.push(Expect your result in the Notifications page soon.); + + updatedServerCommands[command].additionalArgs = ""; + setServerCommands(updatedServerCommands); + + setCommandResult(commandResult); + + const serverProcess: ServerProcessResponse = await dataProvider.getServerRunningProcess(etkeccAdmin, true); + setServerProcess({...serverProcess}); + }; + + if (isLoading) { + return + } + + return (<> +

Server Commands

+ + + + + Command + Description + + + + + {Object.entries(serverCommands).map(([command, { icon, args, description, additionalArgs }]) => ( + + + + {renderIcon(icon)} + {command} + + + {description} + + {args && { + setCommandAdditionalArgs(command, e.target.value); + }} + value={additionalArgs} + />} + + + + ))} + +
+
+ + {commandResult.length > 0 && } severity="success"> + {commandResult.map((result, index) => ( +
{result}
+ ))} +
} + + ) +}; + +export default ServerCommandsPanel; \ No newline at end of file diff --git a/src/components/etke.cc/ServerNotificationsBadge.tsx b/src/components/etke.cc/ServerNotificationsBadge.tsx index 9628d12..d492e3a 100644 --- a/src/components/etke.cc/ServerNotificationsBadge.tsx +++ b/src/components/etke.cc/ServerNotificationsBadge.tsx @@ -5,12 +5,16 @@ 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"; +// 5 minutes const SERVER_NOTIFICATIONS_INTERVAL_TIME = 300000; const useServerNotifications = () => { const [serverNotifications, setServerNotifications] = useStore("serverNotifications", { notifications: [], success: false }); + const [ serverProcess, setServerProcess ] = useStore("serverProcess", { command: "", locked_at: "" }); + const { command, locked_at } = serverProcess; + const { etkeccAdmin } = useAppContext(); const dataProvider = useDataProvider(); const { notifications, success } = serverNotifications; @@ -19,7 +23,7 @@ const useServerNotifications = () => { const notificationsResponse: ServerNotificationsResponse = await dataProvider.getServerNotifications(etkeccAdmin); setServerNotifications({ ...notificationsResponse, - notifications: notificationsResponse.notifications, + notifications: notificationsResponse.notifications.reverse(), success: notificationsResponse.success }); }; @@ -35,21 +39,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 +117,7 @@ export const ServerNotificationsBadge = () => { sx={{ p: 1, maxHeight: "350px", + paddingTop: 0, overflowY: "auto", minWidth: "300px", maxWidth: { @@ -126,7 +136,6 @@ export const ServerNotificationsBadge = () => { justifyContent: "space-between", alignItems: "center", fontWeight: "bold", - backgroundColor: "inherit", }} > Notifications @@ -134,10 +143,14 @@ export const ServerNotificationsBadge = () => { {notifications.map((notification, index) => { - return ( + return ( handleSeeAllNotifications()} sx={{ + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + overflow: "hidden", "&:hover": { backgroundColor: "action.hover", cursor: "pointer" @@ -157,6 +170,11 @@ export const ServerNotificationsBadge = () => { /> } /> + {notification.sent_at} + } + /> diff --git a/src/components/etke.cc/ServerStatusBadge.tsx b/src/components/etke.cc/ServerStatusBadge.tsx index 4d0a461..de6f038 100644 --- a/src/components/etke.cc/ServerStatusBadge.tsx +++ b/src/components/etke.cc/ServerStatusBadge.tsx @@ -68,10 +68,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,6 +82,9 @@ const useServerStatus = () => { } return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } if (serverStatusInterval) { clearInterval(serverStatusInterval); } @@ -105,10 +110,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,6 +123,9 @@ const useCurrentServerProcess = () => { } return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } if (serverCheckInterval) { clearInterval(serverCheckInterval); } @@ -125,10 +135,32 @@ const useCurrentServerProcess = () => { return { command, locked_at }; }; +export const ServerStatusStyledBadge = ({ command, locked_at, isOkay }: { command: string, locked_at: string, isOkay: 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; + } + + return + + + + +}; + const ServerStatusBadge = () => { const { isOkay, successCheck } = useServerStatus(); const { command, locked_at } = useCurrentServerProcess(); - const theme = useTheme(); const navigate = useNavigate(); if (!successCheck) { @@ -140,28 +172,14 @@ 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 }; diff --git a/src/components/etke.cc/ServerStatusPage.tsx b/src/components/etke.cc/ServerStatusPage.tsx index 9b75fca..b06dbfb 100644 --- a/src/components/etke.cc/ServerStatusPage.tsx +++ b/src/components/etke.cc/ServerStatusPage.tsx @@ -4,6 +4,7 @@ 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"; +import ServerCommandsPanel from "./ServerCommandsPanel"; const getTimeSince = (date: string) => { const now = new Date(); @@ -102,6 +103,8 @@ const ServerStatusPage = () => { )} + + {Object.keys(groupedResults).map((category, idx) => ( diff --git a/src/synapse/dataProvider.ts b/src/synapse/dataProvider.ts index a099d76..83d46ba 100644 --- a/src/synapse/dataProvider.ts +++ b/src/synapse/dataProvider.ts @@ -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,18 @@ export interface ServerNotificationsResponse { notifications: ServerNotification[]; } +export interface ServerCommand { + icon: string; + name: string; + description: string; + args: boolean; + additionalArgs?: string; +} + +export interface ServerCommandsResponse { + [command: string]: ServerCommand; +} + export interface SynapseDataProvider extends DataProvider { deleteMedia: (params: DeleteMediaParams) => Promise; purgeRemoteMedia: (params: DeleteMediaParams) => Promise; @@ -329,6 +341,7 @@ export interface SynapseDataProvider extends DataProvider { getServerStatus: (etkeAdminUrl: string) => Promise; getServerNotifications: (etkeAdminUrl: string) => Promise; deleteServerNotifications: (etkeAdminUrl: string) => Promise<{ success: boolean }>; + getServerCommands: (etkeAdminUrl: string) => Promise; } const resourceMap = { @@ -994,12 +1007,17 @@ const baseDataProvider: SynapseDataProvider = { throw error; } }, - getServerRunningProcess: async (runningProcessUrl: string): Promise => { + getServerRunningProcess: async (etkeAdminUrl: string, burstCache: boolean = false): Promise => { 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 +1042,9 @@ const baseDataProvider: SynapseDataProvider = { return { locked_at, command }; }, - getServerStatus: async (serverStatusUrl: string): Promise => { + getServerStatus: async (etkeAdminUrl: string): Promise => { try { - const response = await fetch(`${serverStatusUrl}/status`, { + const response = await fetch(`${etkeAdminUrl}/status`, { headers: { "Authorization": `Bearer ${localStorage.getItem("access_token")}` } @@ -1101,6 +1119,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 = {}) => { + 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, + } } }; diff --git a/src/utils/icons.ts b/src/utils/icons.ts index 133dfe5..a0653ed 100644 --- a/src/utils/icons.ts +++ b/src/utils/icons.ts @@ -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 };