1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

feat: admin menu (#9617)

This commit is contained in:
David Leek 2025-03-26 15:08:56 +01:00 committed by GitHub
parent 328c5368ed
commit 07a4106f48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 855 additions and 105 deletions

View File

@ -2,6 +2,7 @@ import { Routes, Route } from 'react-router-dom';
import { ApiTokenPage } from './apiToken/ApiTokenPage/ApiTokenPage';
import { CreateApiToken } from './apiToken/CreateApiToken/CreateApiToken';
import { AuthSettings } from './auth/AuthSettings';
import { OldAuthSettings } from './auth/OldAuthSettings';
import { Billing } from './billing/Billing';
import FlaggedBillingRedirect from './billing/FlaggedBillingRedirect/FlaggedBillingRedirect';
import { CorsAdmin } from './cors';
@ -47,7 +48,11 @@ export const Admin = () => {
<Route path='banners' element={<Banners />} />
<Route path='license' element={<License />} />
<Route path='cors' element={<CorsAdmin />} />
<Route path='auth' element={<AuthSettings />} />
{newAdminUIEnabled ? (
<Route path='auth/*' element={<AuthSettings />} />
) : (
<Route path='auth' element={<OldAuthSettings />} />
)}
<Route
path='admin-invoices'
element={<FlaggedBillingRedirect />}

View File

@ -1,7 +1,7 @@
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import type { VFC } from 'react';
import { adminGroups } from './adminRoutes';
import { adminGroups } from './oldAdminRoutes';
import type { INavigationMenuItem } from 'interfaces/route';
import { Box, Link, Typography } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';

View File

@ -3,27 +3,26 @@ import type { INavigationMenuItem } from 'interfaces/route';
export const adminGroups: Record<string, string> = {
users: 'User administration',
access: 'Access control',
sso: 'Single sign-on',
network: 'Network',
instance: 'Instance configuration',
log: 'Logs',
other: 'Other',
};
export const adminRoutes: INavigationMenuItem[] = [
// Admin home
{
path: '/admin',
title: 'Admin home',
menu: {},
},
// Users
{
path: '/admin/users',
title: 'Users',
menu: { adminSettings: true },
group: 'users',
},
{
path: '/admin/service-accounts',
title: 'Service accounts',
menu: {
adminSettings: true,
mode: ['enterprise'],
},
group: 'users',
},
{
path: '/admin/groups',
title: 'Groups',
@ -34,14 +33,44 @@ export const adminRoutes: INavigationMenuItem[] = [
group: 'users',
},
{
path: '/admin/roles/*',
title: 'Roles',
path: '/admin/roles',
title: 'Root roles',
menu: {
adminSettings: true,
mode: ['enterprise'],
},
group: 'users',
},
{
path: '/admin/roles/project-roles',
title: 'Project roles',
menu: {
adminSettings: true,
mode: ['enterprise'],
},
group: 'users',
},
{
path: '/admin/logins',
title: 'Login history',
menu: {
adminSettings: true,
mode: ['enterprise'],
},
group: 'users',
},
// Service accounts
{
path: '/admin/service-accounts',
title: 'Service accounts',
menu: {
adminSettings: true,
mode: ['enterprise'],
},
},
// Access control
{
path: '/admin/api',
title: 'API access',
@ -55,18 +84,73 @@ export const adminRoutes: INavigationMenuItem[] = [
menu: { adminSettings: true },
group: 'access',
},
// Single sign-on/login
{
path: '/admin/auth',
title: 'Single sign-on',
title: 'Open ID Connect',
menu: { adminSettings: true, mode: ['enterprise'] },
group: 'access',
group: 'sso',
},
{
path: '/admin/network/*',
title: 'Network',
menu: { adminSettings: true, mode: ['pro', 'enterprise'] },
group: 'instance',
path: '/admin/auth/saml',
title: 'SAML 2.0',
menu: { adminSettings: true, mode: ['enterprise'] },
group: 'sso',
},
{
path: '/admin/auth/password',
title: 'Password login',
menu: { adminSettings: true, mode: ['enterprise'] },
group: 'sso',
},
{
path: '/admin/auth/google',
title: 'Google',
menu: { adminSettings: true, mode: ['enterprise'] },
flag: 'googleAuthEnabled',
group: 'sso',
},
{
path: '/admin/auth/scim',
title: 'SCIM',
menu: { adminSettings: true, mode: ['enterprise'] },
group: 'sso',
},
// Network
{
path: '/admin/network',
title: 'Overview',
menu: { adminSettings: true, mode: ['pro', 'enterprise'] },
group: 'network',
},
{
path: '/admin/network/traffic',
title: 'Traffic',
menu: { adminSettings: true, mode: ['pro', 'enterprise'] },
group: 'network',
},
{
path: '/admin/network/connected-edges',
title: 'Connected edges',
menu: { adminSettings: true, mode: ['pro', 'enterprise'] },
group: 'network',
},
{
path: '/admin/network/backend-connections',
title: 'Backend connections',
menu: { adminSettings: true, mode: ['pro', 'enterprise'] },
group: 'network',
},
{
path: '/admin/network/frontend-data-usage',
title: 'Frontend data usage',
menu: { adminSettings: true, mode: ['pro', 'enterprise'] },
group: 'network',
},
// Instance configuration
{
path: '/admin/maintenance',
title: 'Maintenance',
@ -79,12 +163,6 @@ export const adminRoutes: INavigationMenuItem[] = [
menu: { adminSettings: true, mode: ['enterprise'] },
group: 'instance',
},
{
path: '/admin/instance',
title: 'Instance stats',
menu: { adminSettings: true },
group: 'instance',
},
{
path: '/admin/license',
title: 'License',
@ -92,31 +170,30 @@ export const adminRoutes: INavigationMenuItem[] = [
flag: 'enableLicense',
group: 'instance',
},
{
path: '/admin/instance',
title: 'Instance stats',
menu: { adminSettings: true },
group: 'instance',
},
{
path: '/admin/instance-privacy',
title: 'Instance privacy',
menu: { adminSettings: true },
group: 'instance',
},
// Billing
{
path: '/admin/admin-invoices',
path: '/admin/billing',
title: 'Billing & invoices',
menu: { adminSettings: true, billing: true },
group: 'instance',
},
{
path: '/admin/logins',
title: 'Login history',
menu: {
adminSettings: true,
mode: ['enterprise'],
},
group: 'log',
},
// Event log
{
path: '/history',
title: 'Event log',
menu: { adminSettings: true },
group: 'log',
},
];

View File

@ -1,4 +1,4 @@
import { Tab, Tabs } from '@mui/material';
import {} from '@mui/material';
import { PageContent } from 'component/common/PageContent/PageContent';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { OidcAuth } from './OidcAuth/OidcAuth';
@ -9,40 +9,42 @@ import { GoogleAuth } from './GoogleAuth/GoogleAuth';
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
import { ADMIN, UPDATE_AUTH_CONFIGURATION } from '@server/types/permissions';
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
import { useState } from 'react';
import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel';
import { Route, Routes, useLocation } from 'react-router-dom';
import { usePageTitle } from 'hooks/usePageTitle';
import { useUiFlag } from 'hooks/useUiFlag';
export const AuthSettings = () => {
const { uiConfig, isEnterprise } = useUiConfig();
const { isEnterprise } = useUiConfig();
const googleAuthEnabled = useUiFlag('googleAuthEnabled');
const tabs = [
{
label: 'OpenID Connect',
component: <OidcAuth />,
label: 'Single sign-on: OpenID Connect',
path: '/admin/auth',
},
{
label: 'SAML 2.0',
component: <SamlAuth />,
label: 'Single sign-on: SAML 2.0',
path: '/admin/auth/saml',
},
{
label: 'Password',
component: <PasswordAuth />,
label: 'Password login',
path: '/admin/auth/password',
},
{
label: 'Google',
component: <GoogleAuth />,
label: 'Single sign-on: Google',
path: '/admin/auth/google',
},
{
label: 'SCIM',
component: <ScimSettings />,
label: 'Single sign-on: SCIM',
path: '/admin/auth/scim',
},
].filter(
(item) => uiConfig.flags?.googleAuthEnabled || item.label !== 'Google',
);
];
const { pathname } = useLocation();
const activeTab =
tabs.find((tab) => pathname === tab.path)?.label ||
'Single sign-on: OpenID Connect';
const [activeTab, setActiveTab] = useState(0);
usePageTitle(`Single sign-on: ${tabs[activeTab].label}`);
usePageTitle(activeTab);
if (!isEnterprise()) {
return <PremiumFeature feature='sso' page />;
@ -51,44 +53,16 @@ export const AuthSettings = () => {
return (
<div>
<PermissionGuard permissions={[ADMIN, UPDATE_AUTH_CONFIGURATION]}>
<PageContent
withTabs
header={
<Tabs
value={activeTab}
onChange={(_, tabId) => {
setActiveTab(tabId);
}}
indicatorColor='primary'
textColor='primary'
>
{tabs.map((tab, index) => (
<Tab
key={`${tab.label}_${index}`}
label={tab.label}
id={`tab-${index}`}
aria-controls={`tabpanel-${index}`}
sx={{
minWidth: {
lg: 160,
},
}}
/>
))}
</Tabs>
}
>
<div>
{tabs.map((tab, index) => (
<TabPanel
key={index}
value={activeTab}
index={index}
>
{tab.component}
</TabPanel>
))}
</div>
<PageContent header={activeTab}>
<Routes>
<Route path='/' element={<OidcAuth />} />
<Route path='/saml' element={<SamlAuth />} />
<Route path='/password' element={<PasswordAuth />} />
{googleAuthEnabled && (
<Route path='/google' element={<GoogleAuth />} />
)}
<Route path='/scim' element={<ScimSettings />} />
</Routes>
</PageContent>
</PermissionGuard>
</div>

View File

@ -0,0 +1,96 @@
import { Tab, Tabs } from '@mui/material';
import { PageContent } from 'component/common/PageContent/PageContent';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { OidcAuth } from './OidcAuth/OidcAuth';
import { SamlAuth } from './SamlAuth/SamlAuth';
import { ScimSettings } from './ScimSettings/ScimSettings';
import { PasswordAuth } from './PasswordAuth/PasswordAuth';
import { GoogleAuth } from './GoogleAuth/GoogleAuth';
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
import { ADMIN, UPDATE_AUTH_CONFIGURATION } from '@server/types/permissions';
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
import { useState } from 'react';
import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel';
import { usePageTitle } from 'hooks/usePageTitle';
export const OldAuthSettings = () => {
const { uiConfig, isEnterprise } = useUiConfig();
const tabs = [
{
label: 'OpenID Connect',
component: <OidcAuth />,
},
{
label: 'SAML 2.0',
component: <SamlAuth />,
},
{
label: 'Password',
component: <PasswordAuth />,
},
{
label: 'Google',
component: <GoogleAuth />,
},
{
label: 'SCIM',
component: <ScimSettings />,
},
].filter(
(item) => uiConfig.flags?.googleAuthEnabled || item.label !== 'Google',
);
const [activeTab, setActiveTab] = useState(0);
usePageTitle(`Single sign-on: ${tabs[activeTab].label}`);
if (!isEnterprise()) {
return <PremiumFeature feature='sso' page />;
}
return (
<div>
<PermissionGuard permissions={[ADMIN, UPDATE_AUTH_CONFIGURATION]}>
<PageContent
withTabs
header={
<Tabs
value={activeTab}
onChange={(_, tabId) => {
setActiveTab(tabId);
}}
indicatorColor='primary'
textColor='primary'
>
{tabs.map((tab, index) => (
<Tab
key={`${tab.label}_${index}`}
label={tab.label}
id={`tab-${index}`}
aria-controls={`tabpanel-${index}`}
sx={{
minWidth: {
lg: 160,
},
}}
/>
))}
</Tabs>
}
>
<div>
{tabs.map((tab, index) => (
<TabPanel
key={index}
value={activeTab}
index={index}
>
{tab.component}
</TabPanel>
))}
</div>
</PageContent>
</PermissionGuard>
</div>
);
};

View File

@ -0,0 +1,122 @@
import type { INavigationMenuItem } from 'interfaces/route';
export const adminGroups: Record<string, string> = {
users: 'User administration',
access: 'Access control',
instance: 'Instance configuration',
log: 'Logs',
other: 'Other',
};
export const adminRoutes: INavigationMenuItem[] = [
{
path: '/admin/users',
title: 'Users',
menu: { adminSettings: true },
group: 'users',
},
{
path: '/admin/service-accounts',
title: 'Service accounts',
menu: {
adminSettings: true,
mode: ['enterprise'],
},
group: 'users',
},
{
path: '/admin/groups',
title: 'Groups',
menu: {
adminSettings: true,
mode: ['enterprise'],
},
group: 'users',
},
{
path: '/admin/roles/*',
title: 'Roles',
menu: {
adminSettings: true,
mode: ['enterprise'],
},
group: 'users',
},
{
path: '/admin/api',
title: 'API access',
menu: { adminSettings: true },
group: 'access',
},
{
path: '/admin/cors',
title: 'CORS origins',
flag: 'embedProxyFrontend',
menu: { adminSettings: true },
group: 'access',
},
{
path: '/admin/auth',
title: 'Single sign-on',
menu: { adminSettings: true, mode: ['enterprise'] },
group: 'access',
},
{
path: '/admin/network/*',
title: 'Network',
menu: { adminSettings: true, mode: ['pro', 'enterprise'] },
group: 'instance',
},
{
path: '/admin/maintenance',
title: 'Maintenance',
menu: { adminSettings: true },
group: 'instance',
},
{
path: '/admin/banners',
title: 'Banners',
menu: { adminSettings: true, mode: ['enterprise'] },
group: 'instance',
},
{
path: '/admin/instance',
title: 'Instance stats',
menu: { adminSettings: true },
group: 'instance',
},
{
path: '/admin/license',
title: 'License',
menu: { adminSettings: true, mode: ['enterprise'] },
flag: 'enableLicense',
group: 'instance',
},
{
path: '/admin/instance-privacy',
title: 'Instance privacy',
menu: { adminSettings: true },
group: 'instance',
},
{
path: '/admin/admin-invoices',
title: 'Billing & invoices',
menu: { adminSettings: true, billing: true },
group: 'instance',
},
{
path: '/admin/logins',
title: 'Login history',
menu: {
adminSettings: true,
mode: ['enterprise'],
},
group: 'log',
},
{
path: '/history',
title: 'Event log',
menu: { adminSettings: true },
group: 'log',
},
];

View File

@ -1,5 +1,5 @@
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { adminRoutes } from './adminRoutes';
import { adminRoutes } from './oldAdminRoutes';
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
import { filterAdminRoutes } from './filterAdminRoutes';
import { filterByConfig, mapRouteLink } from 'component/common/util';

View File

@ -0,0 +1,170 @@
import {
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Typography,
styled,
Accordion,
AccordionSummary,
AccordionDetails,
} from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import type { FC, ReactNode } from 'react';
import { Link } from 'react-router-dom';
import type { Theme } from '@mui/material/styles/createTheme';
const listItemButtonStyle = (theme: Theme) => ({
borderRadius: theme.spacing(0.5),
borderLeft: `${theme.spacing(0.5)} solid transparent`,
m: 0,
'&.Mui-selected': {
borderLeft: `${theme.spacing(0.5)} solid ${theme.palette.primary.main}`,
},
minHeight: '0px',
'.MuiAccordionSummary-content': { margin: 0 },
'&>.MuiAccordionSummary-content.MuiAccordionSummary-content': {
margin: '0',
alignItems: 'center',
padding: theme.spacing(0.5, 0),
},
});
const subListItemButtonStyle = (theme: Theme) => ({
paddingLeft: theme.spacing(4),
borderRadius: theme.spacing(0.5),
borderLeft: `${theme.spacing(0.5)} solid transparent`,
m: 0,
'&.Mui-selected': {
borderLeft: `${theme.spacing(0.5)} solid ${theme.palette.primary.main}`,
},
});
const CappedText = styled(Typography)({
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
width: '100%',
});
const StyledListItemIcon = styled(ListItemIcon)(({ theme }) => ({
minWidth: theme.spacing(4),
margin: theme.spacing(0.25, 0),
}));
const StyledListItemText = styled(ListItemText)(({ theme }) => ({
margin: 0,
}));
const StyledAccordion = styled(Accordion)(({ theme }) => ({
paddingTop: theme.spacing(0),
}));
const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
}));
interface IMenuGroupProps {
title: string;
children: ReactNode;
icon: ReactNode;
activeIcon: ReactNode;
isActiveMenu: boolean;
}
export const MenuGroup = ({
title,
children,
icon,
activeIcon,
isActiveMenu,
}: IMenuGroupProps) => {
return (
<StyledAccordion
disableGutters={true}
sx={{
boxShadow: 'none',
'&:before': {
display: 'none',
},
}}
>
<StyledAccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls='configure-content'
id='configure-header'
sx={listItemButtonStyle}
>
<StyledListItemIcon>
{isActiveMenu ? activeIcon : icon}
</StyledListItemIcon>
<StyledListItemText>
<CappedText
sx={{ fontWeight: isActiveMenu ? 'bold' : 'normal' }}
>
{title}
</CappedText>
</StyledListItemText>
</StyledAccordionSummary>
<AccordionDetails sx={{ p: 0 }}>{children}</AccordionDetails>
</StyledAccordion>
);
};
export const AdminListItem: FC<{
href: string;
text: string;
badge?: ReactNode;
selected?: boolean;
children?: React.ReactNode;
onClick: () => void;
}> = ({ href, text, badge, selected, children, onClick }) => {
return (
<ListItem disablePadding>
<ListItemButton
dense={true}
component={Link}
to={href}
sx={listItemButtonStyle}
selected={selected}
onClick={onClick}
>
<StyledListItemIcon>{children}</StyledListItemIcon>
<StyledListItemText>
<CappedText>{text}</CappedText>
</StyledListItemText>
{badge}
</ListItemButton>
</ListItem>
);
};
export const AdminSubListItem: FC<{
href: string;
text: string;
badge?: ReactNode;
selected?: boolean;
children?: React.ReactNode;
onClick: () => void;
}> = ({ href, text, badge, selected, children, onClick }) => {
return (
<ListItem disablePadding>
<ListItemButton
dense={true}
component={Link}
to={href}
sx={subListItemButtonStyle}
selected={selected}
onClick={onClick}
>
<StyledListItemIcon>{children}</StyledListItemIcon>
<StyledListItemText>
<CappedText>{text}</CappedText>
</StyledListItemText>
{badge}
</ListItemButton>
</ListItem>
);
};

View File

@ -0,0 +1,258 @@
import {
Grid,
styled,
Paper,
Typography,
Button,
List,
useMediaQuery,
useTheme,
} from '@mui/material';
import { useUiFlag } from 'hooks/useUiFlag';
import type { ReactNode } from 'react';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import StopRoundedIcon from '@mui/icons-material/StopRounded';
import { AdminListItem, AdminSubListItem, MenuGroup } from './AdminListItem';
import { useLocation } from 'react-router-dom';
import { Sticky } from 'component/common/Sticky/Sticky';
import { adminRoutes, adminGroups } from 'component/admin/adminRoutes';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { filterByConfig } from 'component/common/util';
import { filterAdminRoutes } from 'component/admin/filterAdminRoutes';
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
import { IconRenderer } from './AdminMenuIcons';
interface IMenuLinkItem {
href: string;
text: string;
icon: ReactNode;
}
interface IMenuItem {
href: string;
text: string;
items?: IMenuLinkItem[];
}
const StyledAdminMainGrid = styled(Grid)(({ theme }) => ({
minWidth: 0, // this is a fix for overflowing flex
maxWidth: '1812px',
margin: '0 auto',
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
[theme.breakpoints.up(2156)]: {
width: '100%',
},
[theme.breakpoints.down(2156)]: {
marginLeft: 0,
marginRight: 0,
},
[theme.breakpoints.down('lg')]: {
maxWidth: '1550px',
paddingLeft: theme.spacing(1),
paddingRight: theme.spacing(1),
},
[theme.breakpoints.down(1024)]: {
marginLeft: 0,
marginRight: 0,
},
[theme.breakpoints.down('sm')]: {
minWidth: '100%',
},
minHeight: '94vh',
}));
const StyledMenuPaper = styled(Paper)(({ theme }) => ({
width: '100%',
minWidth: 320,
padding: theme.spacing(3),
marginTop: theme.spacing(6.5),
borderRadius: `${theme.shape.borderRadiusLarge}px`,
boxShadow: 'none',
}));
const StickyContainer = styled(Sticky)(({ theme }) => ({
position: 'sticky',
top: 0,
zIndex: theme.zIndex.sticky,
background: theme.palette.background.application,
transition: 'padding 0.3s ease',
}));
const SettingsHeader = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.mainHeader,
fontWeight: theme.fontWeight.bold,
}));
const StyledButton = styled(Button)(({ theme }) => ({
paddingLeft: theme.spacing(0),
marginBottom: theme.spacing(3),
}));
const StyledStopRoundedIcon = styled(StopRoundedIcon)(({ theme }) => ({
color: theme.palette.primary.main,
}));
interface IWrapIfAdminSubpageProps {
children: ReactNode;
}
export const WrapIfAdminSubpage = ({ children }: IWrapIfAdminSubpageProps) => {
const newAdminUIEnabled = useUiFlag('adminNavUI');
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('lg'));
const showAdminMenu =
!isSmallScreen &&
newAdminUIEnabled &&
location.pathname.indexOf('/admin') === 0;
if (showAdminMenu) {
return <AdminMenu>{children}</AdminMenu>;
}
return <>{children}</>;
};
const DashboardLink = () => {
return (
<>
<StyledButton
href='/personal'
rel='noreferrer'
startIcon={<ArrowBackIcon />}
>
Back to Unleash
</StyledButton>
</>
);
};
interface IAdminMenuProps {
children: ReactNode;
}
export const AdminMenu = ({ children }: IAdminMenuProps) => {
const { uiConfig, isPro, isEnterprise } = useUiConfig();
const { isBilling } = useInstanceStatus();
const isActiveItem = (item?: string) =>
item !== undefined && location.pathname === item;
const theme = useTheme();
const isBreakpoint = useMediaQuery(theme.breakpoints.down(1350));
const onClick = () => {
scrollTo({
top: 0,
behavior: 'smooth',
});
};
const location = useLocation();
const routes = adminRoutes
.filter(filterByConfig(uiConfig))
.filter((route) =>
filterAdminRoutes(route?.menu, {
enterprise: isEnterprise(),
pro: isPro(),
billing: isBilling,
}),
);
const menuStructure = routes.reduce(
(acc: Record<string, IMenuItem>, route) => {
if (route.group && adminGroups[route.group]) {
if (!acc[route.group]) {
acc[route.group] = {
href: route.group,
text: adminGroups[route.group],
items: [],
};
}
acc[route.group].items?.push({
href: route.path,
text: route.title,
icon: <StopRoundedIcon />,
});
}
if (!route.group) {
acc[route.path] = {
href: route.path,
text: route.title,
};
}
return acc;
},
{},
);
const items = Object.values(menuStructure);
return (
<StyledAdminMainGrid container spacing={1}>
<Grid item>
<StickyContainer>
<StyledMenuPaper>
<SettingsHeader>Admin settings</SettingsHeader>
<DashboardLink />
<List>
{items.map((item) => {
if (item.items) {
const isActiveMenu = item.items.find(
(itm) => isActiveItem(itm.href),
);
return (
<MenuGroup
title={item.text}
icon={
<IconRenderer
path={item.href}
active={false}
/>
}
activeIcon={
<IconRenderer
path={item.href}
active={true}
/>
}
isActiveMenu={Boolean(isActiveMenu)}
key={item.text}
>
{item.items.map((subItem) => (
<AdminSubListItem
href={subItem.href}
text={subItem.text}
selected={isActiveItem(
subItem.href,
)}
onClick={onClick}
key={subItem.href}
>
<StyledStopRoundedIcon />
</AdminSubListItem>
))}
</MenuGroup>
);
}
return (
<AdminListItem
href={item.href}
text={item.text}
selected={isActiveItem(item.href)}
onClick={onClick}
key={item.href}
>
<IconRenderer
path={item.href}
active={false}
/>
</AdminListItem>
);
})}
</List>
</StyledMenuPaper>
</StickyContainer>
</Grid>
<Grid item md={isBreakpoint ? true : 9}>
{children}
</Grid>
</StyledAdminMainGrid>
);
};

View File

@ -0,0 +1,43 @@
import type SvgIcon from '@mui/material/SvgIcon/SvgIcon';
import HomeIcon from '@mui/icons-material/Home';
import LaptopIcon from '@mui/icons-material/Laptop';
import EventNoteIcon from '@mui/icons-material/EventNote';
import BillingIcon from '@mui/icons-material/CreditCardOutlined';
import PeopleOutlineRoundedIcon from '@mui/icons-material/PeopleOutlineRounded';
import KeyRoundedIcon from '@mui/icons-material/KeyRounded';
import CloudIcon from '@mui/icons-material/Cloud';
import HubOutlinedIcon from '@mui/icons-material/HubOutlined';
import BuildOutlinedIcon from '@mui/icons-material/BuildOutlined';
import EmptyIcon from '@mui/icons-material/CheckBoxOutlineBlankOutlined';
import type { FC } from 'react';
const icons: Record<string, typeof SvgIcon> = {
'/admin': HomeIcon,
users: PeopleOutlineRoundedIcon,
'/admin/service-accounts': LaptopIcon,
access: KeyRoundedIcon,
sso: CloudIcon,
network: HubOutlinedIcon,
instance: BuildOutlinedIcon,
'/admin/billing': BillingIcon,
'/history': EventNoteIcon,
};
const findIcon = (key: string) => {
return icons[key] || EmptyIcon;
};
export const IconRenderer: FC<{ path: string; active: boolean }> = ({
path,
active = false,
}) => {
const IconComponent = findIcon(path); // Fallback to 'default' if the type is not found
return (
<IconComponent
sx={{
color: active ? 'primary.main' : 'inherit',
}}
/>
);
};

View File

@ -19,6 +19,7 @@ import { NavigationSidebar } from './NavigationSidebar/NavigationSidebar';
import { EventTimelineProvider } from 'component/events/EventTimeline/EventTimelineProvider';
import { NewInUnleash } from './NavigationSidebar/NewInUnleash/NewInUnleash';
import { useUiFlag } from 'hooks/useUiFlag';
import { WrapIfAdminSubpage } from './AdminMenu/AdminMenu';
interface IMainLayoutProps {
children: ReactNode;
@ -145,14 +146,18 @@ export const MainLayout = forwardRef<HTMLDivElement, IMainLayoutProps>(
>
<Header />
<MainLayoutContent>
<SkipNavTarget />
<MainLayoutContentContainer ref={ref}>
<BreadcrumbNav />
<Proclamation toast={uiConfig.toast} />
{children}
</MainLayoutContentContainer>
</MainLayoutContent>
<WrapIfAdminSubpage>
<MainLayoutContent>
<SkipNavTarget />
<MainLayoutContentContainer ref={ref}>
<BreadcrumbNav />
<Proclamation
toast={uiConfig.toast}
/>
{children}
</MainLayoutContentContainer>
</MainLayoutContent>
</WrapIfAdminSubpage>
</Box>
</Box>