Add ServerCommandsPanel to ServerStatusPage

This commit is contained in:
Borislav Pantaleev 2025-02-25 23:03:49 +02:00
parent fbb578392d
commit 79c46c2f46
7 changed files with 314 additions and 34 deletions

View File

@ -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 { LoginMethod } from "../pages/LoginPage";
import { useEffect, useState, Suspense } from "react"; import { useEffect, useState, Suspense } from "react";
import { Icons, DefaultIcon } from "../utils/icons"; import { Icons, DefaultIcon } from "../utils/icons";
@ -6,6 +6,8 @@ import { MenuItem, GetConfig, ClearConfig } from "../utils/config";
import Footer from "./Footer"; import Footer from "./Footer";
import ServerStatusBadge from "./etke.cc/ServerStatusBadge"; import ServerStatusBadge from "./etke.cc/ServerStatusBadge";
import { ServerNotificationsBadge } from "./etke.cc/ServerNotificationsBadge"; import { ServerNotificationsBadge } from "./etke.cc/ServerNotificationsBadge";
import { ServerProcessResponse, ServerStatusResponse } from "../synapse/dataProvider";
import { ServerStatusStyledBadge } from "./etke.cc/ServerStatusBadge";
const AdminUserMenu = () => { const AdminUserMenu = () => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -59,10 +61,20 @@ const AdminAppBar = () => {
const AdminMenu = (props) => { const AdminMenu = (props) => {
const [menu, setMenu] = useState([] as MenuItem[]); const [menu, setMenu] = useState([] as MenuItem[]);
useEffect(() => setMenu(GetConfig().menu), []); 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 ( return (
<Menu {...props}> <Menu {...props}>
<Menu.ResourceItems /> <Menu.ResourceItems />
{menu && <Menu.Item to="/server_status" leftIcon={
<ServerStatusStyledBadge
command={serverProcess.command}
locked_at={serverProcess.locked_at}
isOkay={serverStatus.ok} />
}
primaryText="Server Status" />
}
{menu && menu.map((item, index) => { {menu && menu.map((item, index) => {
const { url, icon, label } = item; const { url, icon, label } = item;
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined; const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;

View File

@ -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<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>(false);
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] = 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(<Box>{commandScheduledText}</Box>);
commandResult.push(<Box>Expect your result in the <Link to={createPath({ resource: "server_notifications", type: "list" })}>Notifications</Link> page soon.</Box>);
updatedServerCommands[command].additionalArgs = "";
setServerCommands(updatedServerCommands);
setCommandResult(commandResult);
const serverProcess: ServerProcessResponse = await dataProvider.getServerRunningProcess(etkeccAdmin, true);
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"
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;

View File

@ -5,12 +5,16 @@ import { useDataProvider, useStore } from "react-admin";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { Fragment, useEffect, useState } from "react"; import { Fragment, useEffect, useState } from "react";
import { useAppContext } from "../../Context"; import { useAppContext } from "../../Context";
import { ServerNotificationsResponse } from "../../synapse/dataProvider"; import { ServerNotificationsResponse, ServerProcessResponse } from "../../synapse/dataProvider";
// 5 minutes
const SERVER_NOTIFICATIONS_INTERVAL_TIME = 300000; const SERVER_NOTIFICATIONS_INTERVAL_TIME = 300000;
const useServerNotifications = () => { const useServerNotifications = () => {
const [serverNotifications, setServerNotifications] = useStore<ServerNotificationsResponse>("serverNotifications", { notifications: [], success: false }); 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 { etkeccAdmin } = useAppContext();
const dataProvider = useDataProvider(); const dataProvider = useDataProvider();
const { notifications, success } = serverNotifications; const { notifications, success } = serverNotifications;
@ -19,7 +23,7 @@ const useServerNotifications = () => {
const notificationsResponse: ServerNotificationsResponse = await dataProvider.getServerNotifications(etkeccAdmin); const notificationsResponse: ServerNotificationsResponse = await dataProvider.getServerNotifications(etkeccAdmin);
setServerNotifications({ setServerNotifications({
...notificationsResponse, ...notificationsResponse,
notifications: notificationsResponse.notifications, notifications: notificationsResponse.notifications.reverse(),
success: notificationsResponse.success success: notificationsResponse.success
}); });
}; };
@ -35,21 +39,26 @@ const useServerNotifications = () => {
}; };
useEffect(() => { useEffect(() => {
let serverNotificationsInterval: NodeJS.Timeout; let serverNotificationsInterval: NodeJS.Timeout | null = null;
let timeoutId: NodeJS.Timeout | null = null;
if (etkeccAdmin) { if (etkeccAdmin) {
fetchNotifications(); fetchNotifications();
setTimeout(() => { timeoutId = setTimeout(() => {
// start the interval after the SERVER_NOTIFICATIONS_INTERVAL_TIME to avoid too many requests // start the interval after the SERVER_NOTIFICATIONS_INTERVAL_TIME to avoid too many requests
serverNotificationsInterval = setInterval(fetchNotifications, SERVER_NOTIFICATIONS_INTERVAL_TIME); serverNotificationsInterval = setInterval(fetchNotifications, SERVER_NOTIFICATIONS_INTERVAL_TIME);
}, SERVER_NOTIFICATIONS_INTERVAL_TIME); }, SERVER_NOTIFICATIONS_INTERVAL_TIME);
} }
return () => { return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (serverNotificationsInterval) { if (serverNotificationsInterval) {
clearInterval(serverNotificationsInterval); clearInterval(serverNotificationsInterval);
} }
} }
}, [etkeccAdmin]); }, [etkeccAdmin, command, locked_at]);
return { success, notifications, deleteServerNotifications }; return { success, notifications, deleteServerNotifications };
}; };
@ -108,6 +117,7 @@ export const ServerNotificationsBadge = () => {
sx={{ sx={{
p: 1, p: 1,
maxHeight: "350px", maxHeight: "350px",
paddingTop: 0,
overflowY: "auto", overflowY: "auto",
minWidth: "300px", minWidth: "300px",
maxWidth: { maxWidth: {
@ -126,7 +136,6 @@ export const ServerNotificationsBadge = () => {
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
fontWeight: "bold", fontWeight: "bold",
backgroundColor: "inherit",
}} }}
> >
<Typography variant="h6">Notifications</Typography> <Typography variant="h6">Notifications</Typography>
@ -134,10 +143,14 @@ export const ServerNotificationsBadge = () => {
</ListSubheader> </ListSubheader>
<Divider /> <Divider />
{notifications.map((notification, index) => { {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 <ListItem
onClick={() => handleSeeAllNotifications()} onClick={() => handleSeeAllNotifications()}
sx={{ sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
overflow: "hidden",
"&:hover": { "&:hover": {
backgroundColor: "action.hover", backgroundColor: "action.hover",
cursor: "pointer" cursor: "pointer"
@ -157,6 +170,11 @@ export const ServerNotificationsBadge = () => {
/> />
} }
/> />
<ListItemText
primary={
<Typography variant="body2">{notification.sent_at}</Typography>
}
/>
</ListItem> </ListItem>
<Divider /> <Divider />
</Fragment> </Fragment>

View File

@ -68,10 +68,12 @@ const useServerStatus = () => {
}; };
useEffect(() => { useEffect(() => {
let serverStatusInterval: NodeJS.Timeout; let serverStatusInterval: NodeJS.Timeout | null = null;
let timeoutId: NodeJS.Timeout | null = null;
if (etkeccAdmin) { if (etkeccAdmin) {
checkServerStatus(); checkServerStatus();
setTimeout(() => { timeoutId = setTimeout(() => {
// start the interval after 10 seconds to avoid too many requests // start the interval after 10 seconds to avoid too many requests
serverStatusInterval = setInterval(checkServerStatus, SERVER_STATUS_INTERVAL_TIME); serverStatusInterval = setInterval(checkServerStatus, SERVER_STATUS_INTERVAL_TIME);
}, 10000); }, 10000);
@ -80,6 +82,9 @@ const useServerStatus = () => {
} }
return () => { return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (serverStatusInterval) { if (serverStatusInterval) {
clearInterval(serverStatusInterval); clearInterval(serverStatusInterval);
} }
@ -105,10 +110,12 @@ const useCurrentServerProcess = () => {
} }
useEffect(() => { useEffect(() => {
let serverCheckInterval: NodeJS.Timeout; let serverCheckInterval: NodeJS.Timeout | null = null;
let timeoutId: NodeJS.Timeout | null = null;
if (etkeccAdmin) { if (etkeccAdmin) {
checkServerRunningProcess(); checkServerRunningProcess();
setTimeout(() => { timeoutId = setTimeout(() => {
serverCheckInterval = setInterval(checkServerRunningProcess, SERVER_CURRENT_PROCCESS_INTERVAL_TIME); serverCheckInterval = setInterval(checkServerRunningProcess, SERVER_CURRENT_PROCCESS_INTERVAL_TIME);
}, 5000); }, 5000);
} else { } else {
@ -116,6 +123,9 @@ const useCurrentServerProcess = () => {
} }
return () => { return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (serverCheckInterval) { if (serverCheckInterval) {
clearInterval(serverCheckInterval); clearInterval(serverCheckInterval);
} }
@ -125,10 +135,32 @@ const useCurrentServerProcess = () => {
return { command, locked_at }; 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 <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>
};
const ServerStatusBadge = () => { const ServerStatusBadge = () => {
const { isOkay, successCheck } = useServerStatus(); const { isOkay, successCheck } = useServerStatus();
const { command, locked_at } = useCurrentServerProcess(); const { command, locked_at } = useCurrentServerProcess();
const theme = useTheme();
const navigate = useNavigate(); const navigate = useNavigate();
if (!successCheck) { if (!successCheck) {
@ -140,28 +172,14 @@ const ServerStatusBadge = () => {
}; };
let tooltipText = "Click to view Server Status"; 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) { if (command && locked_at) {
badgeBackgroundColor = theme.palette.warning.main;
badgeColor = theme.palette.warning.main;
tooltipText = `Running: ${command}; ${tooltipText}`; tooltipText = `Running: ${command}; ${tooltipText}`;
} }
return <Button onClick={handleServerStatusClick} size="medium" sx={{ minWidth: "auto", ".MuiButton-startIcon": { m: 0 }}}> return <Button onClick={handleServerStatusClick} size="medium" sx={{ minWidth: "auto", ".MuiButton-startIcon": { m: 0 }}}>
<Tooltip title={tooltipText} sx={{ cursor: "pointer" }}> <Tooltip title={tooltipText} sx={{ cursor: "pointer" }}>
<StyledBadge <ServerStatusStyledBadge command={command || ""} locked_at={locked_at || ""} isOkay={isOkay} />
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>
</Tooltip> </Tooltip>
</Button> </Button>
}; };

View File

@ -4,6 +4,7 @@ import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import EngineeringIcon from '@mui/icons-material/Engineering'; import EngineeringIcon from '@mui/icons-material/Engineering';
import { ServerProcessResponse, ServerStatusComponent, ServerStatusResponse } from "../../synapse/dataProvider"; import { ServerProcessResponse, ServerStatusComponent, ServerStatusResponse } from "../../synapse/dataProvider";
import ServerCommandsPanel from "./ServerCommandsPanel";
const getTimeSince = (date: string) => { const getTimeSince = (date: string) => {
const now = new Date(); const now = new Date();
@ -102,6 +103,8 @@ const ServerStatusPage = () => {
</Stack> </Stack>
)} )}
<ServerCommandsPanel />
<Stack spacing={2} direction="row"> <Stack spacing={2} direction="row">
{Object.keys(groupedResults).map((category, idx) => ( {Object.keys(groupedResults).map((category, idx) => (
<Box key={`category_${category}`} sx={{ flex: 1 }}> <Box key={`category_${category}`} sx={{ flex: 1 }}>

View File

@ -300,8 +300,8 @@ export interface ServerStatusResponse {
} }
export interface ServerProcessResponse { export interface ServerProcessResponse {
locked_at?: string; locked_at: string;
command?: string; command: string;
} }
export interface ServerNotification { export interface ServerNotification {
@ -315,6 +315,18 @@ export interface ServerNotificationsResponse {
notifications: ServerNotification[]; 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 { export interface SynapseDataProvider extends DataProvider {
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>; deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
purgeRemoteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>; purgeRemoteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
@ -329,6 +341,7 @@ export interface SynapseDataProvider extends DataProvider {
getServerStatus: (etkeAdminUrl: string) => Promise<ServerStatusResponse>; getServerStatus: (etkeAdminUrl: string) => Promise<ServerStatusResponse>;
getServerNotifications: (etkeAdminUrl: string) => Promise<ServerNotificationsResponse>; getServerNotifications: (etkeAdminUrl: string) => Promise<ServerNotificationsResponse>;
deleteServerNotifications: (etkeAdminUrl: string) => Promise<{ success: boolean }>; deleteServerNotifications: (etkeAdminUrl: string) => Promise<{ success: boolean }>;
getServerCommands: (etkeAdminUrl: string) => Promise<ServerCommandsResponse>;
} }
const resourceMap = { const resourceMap = {
@ -994,12 +1007,17 @@ const baseDataProvider: SynapseDataProvider = {
throw error; throw error;
} }
}, },
getServerRunningProcess: async (runningProcessUrl: string): Promise<ServerProcessResponse> => { getServerRunningProcess: async (etkeAdminUrl: string, burstCache: boolean = false): Promise<ServerProcessResponse> => {
const locked_at = ""; const locked_at = "";
const command = ""; const command = "";
let serverURL = `${etkeAdminUrl}/lock`;
if (burstCache) {
serverURL += `?time=${new Date().getTime()}`;
}
try { try {
const response = await fetch(`${runningProcessUrl}/lock`, { const response = await fetch(serverURL, {
headers: { headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}` "Authorization": `Bearer ${localStorage.getItem("access_token")}`
} }
@ -1024,9 +1042,9 @@ const baseDataProvider: SynapseDataProvider = {
return { locked_at, command }; return { locked_at, command };
}, },
getServerStatus: async (serverStatusUrl: string): Promise<ServerStatusResponse> => { getServerStatus: async (etkeAdminUrl: string): Promise<ServerStatusResponse> => {
try { try {
const response = await fetch(`${serverStatusUrl}/status`, { const response = await fetch(`${etkeAdminUrl}/status`, {
headers: { headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}` "Authorization": `Bearer ${localStorage.getItem("access_token")}`
} }
@ -1101,6 +1119,66 @@ const baseDataProvider: SynapseDataProvider = {
} }
return { success: false }; 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,
}
} }
}; };

View File

@ -3,6 +3,11 @@ import EngineeringIcon from '@mui/icons-material/Engineering';
import HelpCenterIcon from '@mui/icons-material/HelpCenter'; import HelpCenterIcon from '@mui/icons-material/HelpCenter';
import SupportAgentIcon from '@mui/icons-material/SupportAgent'; import SupportAgentIcon from '@mui/icons-material/SupportAgent';
import OpenInNewIcon from '@mui/icons-material/OpenInNew'; 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 = { export const Icons = {
Announcement: AnnouncementIcon, Announcement: AnnouncementIcon,
@ -10,6 +15,11 @@ export const Icons = {
HelpCenter: HelpCenterIcon, HelpCenter: HelpCenterIcon,
SupportAgent: SupportAgentIcon, SupportAgent: SupportAgentIcon,
Default: OpenInNewIcon, Default: OpenInNewIcon,
PieChart: PieChartIcon,
Upgrade: UpgradeIcon,
Router: RouterIcon,
PriceCheck: PriceCheckIcon,
RestartAlt: RestartAltIcon,
// Add more icons as needed // Add more icons as needed
}; };