Add server status page and a badge in AppBar (#182)
* WIP: Add server status and running process indicators * Finish ServerStatusPage and ServerRunningProcess * fix typos, add running process info to the server status page * Remove ServerRunningProcess and integrate it into ServerStatusBadge * remove divider in menu * display time as started X minutes ago * add documentation; clearly state what new components are; update readme * change wording a bit, cross-link with docs/README.md * use returned HTML * Finish ServerStatus page and badges * Fix types * cleanup * remove some code * adjust config load
This commit is contained in:
parent
3b69e78bb8
commit
7c21692a1e
@ -101,6 +101,7 @@ The following changes are already implemented:
|
||||
* 🖼️ [Add rooms' avatars](https://github.com/etkecc/synapse-admin/pull/158)
|
||||
* 🤖 [User Badges](https://github.com/etkecc/synapse-admin/pull/160)
|
||||
* 🔑 [Allow prefilling any fields on the login form via GET params](https://github.com/etkecc/synapse-admin/pull/181)
|
||||
* _(for [etke.cc](https://etke.cc) customers only)_ [Server Status indicator and page](https://github.com/etkecc/synapse-admin/pull/182)
|
||||
|
||||
|
||||
_the list will be updated as new changes are added_
|
||||
|
@ -26,6 +26,14 @@ Specific configuration options:
|
||||
* [User Badges](./user-badges.md)
|
||||
* [Prefilling the Login Form](./prefill-login-form.md)
|
||||
|
||||
for [etke.cc](https://etke.cc) customers only:
|
||||
|
||||
> **Note:** The following features are only available for etke.cc customers. Due to specifics of the implementation,
|
||||
they are not available for any other Synapse Admin deployments.
|
||||
|
||||
* [Server Status icon](../src/components/etke.cc/README.md#server-status-icon)
|
||||
* [Server Status page](../src/components/etke.cc/README.md#server-status-page)
|
||||
|
||||
## Deployment
|
||||
|
||||
* [Serving Synapse Admin behind a reverse proxy](./reverse-proxy.md)
|
||||
|
BIN
screenshots/etke.cc/server-status/indicator.webp
Normal file
BIN
screenshots/etke.cc/server-status/indicator.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
BIN
screenshots/etke.cc/server-status/page.webp
Normal file
BIN
screenshots/etke.cc/server-status/page.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
@ -22,9 +22,10 @@ import rooms from "./resources/rooms";
|
||||
import userMediaStats from "./resources/user_media_statistics";
|
||||
import users from "./resources/users";
|
||||
import authProvider from "./synapse/authProvider";
|
||||
import dataProvider from "./synapse/dataProvider";
|
||||
import dataProvider, { ServerStatusResponse } from "./synapse/dataProvider";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Config } from "./utils/config";
|
||||
import ServerStatusPage from "./components/etke.cc/ServerStatusPage";
|
||||
|
||||
// TODO: Can we use lazy loading together with browser locale?
|
||||
const messages = {
|
||||
@ -64,6 +65,7 @@ export const App = () => (
|
||||
>
|
||||
<CustomRoutes>
|
||||
<Route path="/import_users" element={<UserImport />} />
|
||||
<Route path="/server_status" element={<ServerStatusPage />} />
|
||||
</CustomRoutes>
|
||||
<Resource {...users} />
|
||||
<Resource {...rooms} />
|
||||
|
@ -4,6 +4,7 @@ import { useEffect, useState, Suspense } from "react";
|
||||
import { Icons, DefaultIcon } from "../utils/icons";
|
||||
import { MenuItem, GetConfig, ClearConfig } from "../utils/config";
|
||||
import Footer from "./Footer";
|
||||
import ServerStatusBadge from "./etke.cc/ServerStatusBadge";
|
||||
|
||||
const AdminUserMenu = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
@ -48,6 +49,7 @@ const AdminUserMenu = () => {
|
||||
const AdminAppBar = () => {
|
||||
return (<AppBar userMenu={<AdminUserMenu />}>
|
||||
<TitlePortal />
|
||||
<ServerStatusBadge />
|
||||
<InspectorButton />
|
||||
</AppBar>);
|
||||
};
|
||||
|
30
src/components/etke.cc/README.md
Normal file
30
src/components/etke.cc/README.md
Normal file
@ -0,0 +1,30 @@
|
||||
# etke.cc-specific components
|
||||
|
||||
This directory contains [etke.cc](https://etke.cc)-specific components, unusable for any other purposes and/or configuration.
|
||||
|
||||
We at [etke.cc](https://etke.cc) attempting to develop everything open-source, but some things are too specific to be used by anyone else. This directory contains such components - they are only available for [etke.cc](https://etke.cc) customers.
|
||||
|
||||
Due to the specifics mentioned above, these components are documented here rather than in the [docs](../../../docs/README.md), plus they are not supported as part of the Synapse Admin open-source project (i.e.: no issues, no PRs, no support, no requests, etc.).
|
||||
|
||||
## Components
|
||||
|
||||
### Server Status icon
|
||||
|
||||

|
||||
|
||||
In the application bar the new monitoring icon is displayed that shows the current server status, and has the following color dot (and tooltip indicators):
|
||||
|
||||
* 🟢 (green) - the server is up and running, everything is fine, no issues detected
|
||||
* 🟡 (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
|
||||
|
||||
### Server Status page
|
||||
|
||||

|
||||
|
||||
When you click on the [Server Status icon](#server-status-icon) in the application bar, you will be redirected to the
|
||||
Server Status page. This page contains the following information:
|
||||
|
||||
* Overall server status (up/updating/has issues)
|
||||
* 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
|
169
src/components/etke.cc/ServerStatusBadge.tsx
Normal file
169
src/components/etke.cc/ServerStatusBadge.tsx
Normal file
@ -0,0 +1,169 @@
|
||||
import { Avatar, Badge, Theme, Tooltip } from "@mui/material";
|
||||
import { useEffect } from "react";
|
||||
import { useAppContext } from "../../App";
|
||||
import { Button, useDataProvider, useStore } from "react-admin";
|
||||
import { styled } from '@mui/material/styles';
|
||||
import MonitorHeartIcon from '@mui/icons-material/MonitorHeart';
|
||||
import { BadgeProps } from "@mui/material/Badge";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { ServerProcessResponse, ServerStatusResponse } from "../../synapse/dataProvider";
|
||||
|
||||
interface StyledBadgeProps extends BadgeProps {
|
||||
backgroundColor: string;
|
||||
badgeColor: string
|
||||
theme?: Theme;
|
||||
}
|
||||
|
||||
const StyledBadge = styled(Badge, { shouldForwardProp: (prop) => !['badgeColor', 'backgroundColor'].includes(prop as string) })<StyledBadgeProps>
|
||||
(({ theme, backgroundColor, badgeColor }) => ({
|
||||
'& .MuiBadge-badge': {
|
||||
backgroundColor: backgroundColor,
|
||||
color: badgeColor,
|
||||
boxShadow: `0 0 0 2px ${theme.palette.background.paper}`,
|
||||
'&::after': {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
animation: 'ripple 2.5s infinite ease-in-out',
|
||||
border: '1px solid currentColor',
|
||||
content: '""',
|
||||
},
|
||||
},
|
||||
'@keyframes ripple': {
|
||||
'0%': {
|
||||
transform: 'scale(.8)',
|
||||
opacity: 1,
|
||||
},
|
||||
'100%': {
|
||||
transform: 'scale(2.4)',
|
||||
opacity: 0,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// every 5 minutes
|
||||
const SERVER_STATUS_INTERVAL_TIME = 5 * 60 * 1000;
|
||||
// every 5 seconds
|
||||
const SERVER_CURRENT_PROCCESS_INTERVAL_TIME = 5 * 1000;
|
||||
|
||||
const useServerStatus = () => {
|
||||
const [serverStatus, setServerStatus] = useStore<ServerStatusResponse>("serverStatus", { ok: false, success: false, host: "", results: [] });
|
||||
const { etkeccAdmin } = useAppContext();
|
||||
const dataProvider = useDataProvider();
|
||||
const isOkay = serverStatus.ok;
|
||||
const successCheck = serverStatus.success;
|
||||
|
||||
const checkServerStatus = async () => {
|
||||
const serverStatus: ServerStatusResponse = await dataProvider.getServerStatus(etkeccAdmin);
|
||||
setServerStatus({
|
||||
ok: serverStatus.ok,
|
||||
success: serverStatus.success,
|
||||
host: serverStatus.host,
|
||||
results: serverStatus.results,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let serverStatusInterval: NodeJS.Timeout;
|
||||
if (etkeccAdmin) {
|
||||
checkServerStatus();
|
||||
setTimeout(() => {
|
||||
// start the interval after 10 seconds to avoid too many requests
|
||||
serverStatusInterval = setInterval(checkServerStatus, SERVER_STATUS_INTERVAL_TIME);
|
||||
}, 10000);
|
||||
} else {
|
||||
setServerStatus({ ok: false, success: false, host: "", results: [] });
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (serverStatusInterval) {
|
||||
clearInterval(serverStatusInterval);
|
||||
}
|
||||
}
|
||||
}, [etkeccAdmin]);
|
||||
|
||||
return { isOkay, successCheck };
|
||||
};
|
||||
|
||||
const useCurrentServerProcess = () => {
|
||||
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" });
|
||||
const { etkeccAdmin } = useAppContext();
|
||||
const dataProvider = useDataProvider();
|
||||
const { command, locked_at } = serverProcess;
|
||||
|
||||
const checkServerRunningProcess = async () => {
|
||||
const serverProcess: ServerProcessResponse = await dataProvider.getServerRunningProcess(etkeccAdmin);
|
||||
setServerProcess({
|
||||
...serverProcess,
|
||||
command: serverProcess.command,
|
||||
locked_at: serverProcess.locked_at
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let serverCheckInterval: NodeJS.Timeout;
|
||||
if (etkeccAdmin) {
|
||||
checkServerRunningProcess();
|
||||
setTimeout(() => {
|
||||
serverCheckInterval = setInterval(checkServerRunningProcess, SERVER_CURRENT_PROCCESS_INTERVAL_TIME);
|
||||
}, 5000);
|
||||
} else {
|
||||
setServerProcess({ command: "", locked_at: "" });
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (serverCheckInterval) {
|
||||
clearInterval(serverCheckInterval);
|
||||
}
|
||||
}
|
||||
}, [etkeccAdmin]);
|
||||
|
||||
return { command, locked_at };
|
||||
};
|
||||
|
||||
const ServerStatusBadge = () => {
|
||||
const { isOkay, successCheck } = useServerStatus();
|
||||
const { command, locked_at } = useCurrentServerProcess();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!successCheck) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleServerStatusClick = () => {
|
||||
navigate("/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) {
|
||||
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>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
};
|
||||
|
||||
export default ServerStatusBadge;
|
144
src/components/etke.cc/ServerStatusPage.tsx
Normal file
144
src/components/etke.cc/ServerStatusPage.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import { useStore } from "ra-core";
|
||||
import { Box, Stack, Typography, Paper, Link, Chip, Divider, Tooltip, ChipProps } from "@mui/material";
|
||||
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`;
|
||||
};
|
||||
|
||||
const StatusChip = ({ isOkay, size = "medium", command }: { isOkay: boolean, size?: "small" | "medium", command?: string }) => {
|
||||
let label = "OK";
|
||||
let icon = <CheckIcon />;
|
||||
let color: ChipProps["color"] = "success";
|
||||
if (!isOkay) {
|
||||
label = "Error";
|
||||
icon = <CloseIcon />;
|
||||
color = "error";
|
||||
}
|
||||
|
||||
if (command) {
|
||||
label = command;
|
||||
color = "warning";
|
||||
icon = <EngineeringIcon />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Chip icon={icon} label={label} color={color} variant="outlined" size={size} />
|
||||
);
|
||||
};
|
||||
|
||||
const ServerComponentText = ({ text }: { text: string }) => {
|
||||
return <Typography variant="body1" dangerouslySetInnerHTML={{ __html: text }} />;
|
||||
};
|
||||
|
||||
const ServerStatusPage = () => {
|
||||
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 successCheck = serverStatus.success;
|
||||
const isOkay = serverStatus.ok;
|
||||
const host = serverStatus.host;
|
||||
const results = serverStatus.results;
|
||||
|
||||
let groupedResults: Record<string, ServerStatusComponent[]> = {};
|
||||
for (const result of results) {
|
||||
if (!groupedResults[result.category]) {
|
||||
groupedResults[result.category] = [];
|
||||
}
|
||||
groupedResults[result.category].push(result);
|
||||
}
|
||||
|
||||
if (!successCheck) {
|
||||
return (
|
||||
<Paper elevation={3} sx={{ p: 3, mt: 3 }}>
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
<CloseIcon color="error" />
|
||||
<Typography color="error">
|
||||
Unable to fetch server status. Please try again later.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack spacing={3} mt={3}>
|
||||
<Stack spacing={1} direction="row" alignItems="center">
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Typography variant="h4">Status:</Typography>
|
||||
<StatusChip isOkay={isOkay} command={command} />
|
||||
</Box>
|
||||
<Typography variant="h5" color="primary" fontWeight="medium">
|
||||
{host}
|
||||
</Typography>
|
||||
</Stack>
|
||||
{command && locked_at && (
|
||||
<Stack spacing={1} direction="row" alignItems="center">
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Typography variant="h5">Currently running:</Typography>
|
||||
<Typography variant="h5" color="text.secondary">
|
||||
<Link href={"https://etke.cc/help/extras/scheduler/#"+command} target="_blank">
|
||||
{command}
|
||||
</Link>
|
||||
<Tooltip title={locked_at.toString()}>
|
||||
<Typography component="span" color="text.secondary" sx={{ display: "inline-block", ml: 1 }}>(started {getTimeSince(locked_at)} ago)</Typography>
|
||||
</Tooltip>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Stack spacing={2} direction="row">
|
||||
{Object.keys(groupedResults).map((category, idx) => (
|
||||
<Box key={`category_${category}`} sx={{ flex: 1 }}>
|
||||
<Typography variant="h5" mb={1}>
|
||||
{category}
|
||||
</Typography>
|
||||
<Paper elevation={2} sx={{ p: 3 }}>
|
||||
<Stack spacing={1} divider={<Divider />}>
|
||||
{groupedResults[category].map((result, idx) => (
|
||||
<Box key={`${category}_${idx}`}>
|
||||
<Stack spacing={2}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<StatusChip isOkay={result.ok} size="small" />
|
||||
{result.label.url ? (
|
||||
<Link href={result.label.url} target="_blank" rel="noopener noreferrer">
|
||||
<ServerComponentText text={result.label.text} />
|
||||
</Link>
|
||||
) : (
|
||||
<ServerComponentText text={result.label.text} />
|
||||
)}
|
||||
</Box>
|
||||
{result.reason && <Typography color="text.secondary" dangerouslySetInnerHTML={{ __html: result.reason }}/>}
|
||||
{(!result.ok && result.help) && (
|
||||
<Link href={result.help} target="_blank" rel="noopener noreferrer" sx={{ mt: 1 }}>
|
||||
Learn more
|
||||
</Link>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerStatusPage;
|
@ -265,6 +265,31 @@ export interface UsernameAvailabilityResult {
|
||||
errcode?: string;
|
||||
}
|
||||
|
||||
export interface ServerStatusComponent {
|
||||
ok: boolean;
|
||||
category: string;
|
||||
reason: string;
|
||||
url: string;
|
||||
help: string;
|
||||
label: {
|
||||
url: string;
|
||||
icon: string;
|
||||
text: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ServerStatusResponse {
|
||||
success: boolean;
|
||||
ok: boolean;
|
||||
host: string;
|
||||
results: ServerStatusComponent[];
|
||||
}
|
||||
|
||||
export interface ServerProcessResponse {
|
||||
locked_at?: string;
|
||||
command?: string;
|
||||
}
|
||||
|
||||
export interface SynapseDataProvider extends DataProvider {
|
||||
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||
uploadMedia: (params: UploadMediaParams) => Promise<UploadMediaResult>;
|
||||
@ -273,6 +298,8 @@ export interface SynapseDataProvider extends DataProvider {
|
||||
setRateLimits: (id: Identifier, rateLimits: RateLimitsModel) => Promise<void>;
|
||||
checkUsernameAvailability: (username: string) => Promise<UsernameAvailabilityResult>;
|
||||
makeRoomAdmin: (room_id: string, user_id: string) => Promise<{ success: boolean; error?: string; errcode?: string }>;
|
||||
getServerRunningProcess: (etkeAdminUrl: string) => Promise<ServerProcessResponse>;
|
||||
getServerStatus: (etkeAdminUrl: string) => Promise<ServerStatusResponse>;
|
||||
}
|
||||
|
||||
const resourceMap = {
|
||||
@ -880,6 +907,60 @@ const baseDataProvider: SynapseDataProvider = {
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
getServerRunningProcess: async (runningProcessUrl: string): Promise<ServerProcessResponse> => {
|
||||
const locked_at = "";
|
||||
const command = "";
|
||||
|
||||
try {
|
||||
const response = await fetch(`${runningProcessUrl}/lock`, {
|
||||
headers: {
|
||||
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Error getting server running process: ${response.status} ${response.statusText}`);
|
||||
return { locked_at, command };
|
||||
}
|
||||
const status = response.status;
|
||||
|
||||
if (status === 200) {
|
||||
const json = await response.json();
|
||||
return json as { locked_at: string; command: string };
|
||||
}
|
||||
if (status === 204) {
|
||||
return { locked_at, command };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting server running process", error);
|
||||
}
|
||||
|
||||
return { locked_at, command };
|
||||
},
|
||||
getServerStatus: async (serverStatusUrl: string): Promise<ServerStatusResponse> => {
|
||||
try {
|
||||
const response = await fetch(`${serverStatusUrl}/status`, {
|
||||
headers: {
|
||||
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.error(`Error getting server status: ${response.status} ${response.statusText}`);
|
||||
return { success: false, ok: false, host: "", results: [] };
|
||||
}
|
||||
|
||||
const status = response.status;
|
||||
if (status === 200) {
|
||||
const json = await response.json();
|
||||
const result = { success: true, ...json } as ServerStatusResponse;
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting server status", error);
|
||||
}
|
||||
|
||||
return { success: false, ok: false, host: "", results: [] };
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -2,6 +2,7 @@ export interface Config {
|
||||
restrictBaseUrl: string | string[];
|
||||
asManagedUsers: RegExp[];
|
||||
menu: MenuItem[];
|
||||
etkeccAdmin?: string;
|
||||
}
|
||||
|
||||
export interface MenuItem {
|
||||
@ -17,6 +18,7 @@ let config: Config = {
|
||||
restrictBaseUrl: "",
|
||||
asManagedUsers: [],
|
||||
menu: [],
|
||||
etkeccAdmin: ""
|
||||
};
|
||||
|
||||
export const FetchConfig = async () => {
|
||||
@ -49,6 +51,8 @@ export const FetchConfig = async () => {
|
||||
}
|
||||
|
||||
// load config from context
|
||||
// we deliberately processing each key separately to avoid overwriting the whole config, loosing some keys, and messing
|
||||
// with typescript types
|
||||
export const LoadConfig = (context: any) => {
|
||||
if (context?.restrictBaseUrl) {
|
||||
config.restrictBaseUrl = context.restrictBaseUrl as string | string[];
|
||||
@ -65,6 +69,10 @@ export const LoadConfig = (context: any) => {
|
||||
if (menu.length > 0) {
|
||||
config.menu = menu;
|
||||
}
|
||||
|
||||
if (context?.etkeccAdmin) {
|
||||
config.etkeccAdmin = context.etkeccAdmin;
|
||||
}
|
||||
}
|
||||
|
||||
// get config
|
||||
|
Loading…
x
Reference in New Issue
Block a user