From 07a4106f48f21db9da794709a47cbe831a346ed4 Mon Sep 17 00:00:00 2001 From: David Leek Date: Wed, 26 Mar 2025 15:08:56 +0100 Subject: [PATCH] feat: admin menu (#9617) --- frontend/src/component/admin/Admin.tsx | 7 +- frontend/src/component/admin/AdminIndex.tsx | 2 +- frontend/src/component/admin/adminRoutes.ts | 151 +++++++--- .../src/component/admin/auth/AuthSettings.tsx | 88 +++--- .../component/admin/auth/OldAuthSettings.tsx | 96 +++++++ .../src/component/admin/oldAdminRoutes.ts | 122 +++++++++ .../src/component/admin/useAdminRoutes.ts | 2 +- .../MainLayout/AdminMenu/AdminListItem.tsx | 170 ++++++++++++ .../layout/MainLayout/AdminMenu/AdminMenu.tsx | 258 ++++++++++++++++++ .../MainLayout/AdminMenu/AdminMenuIcons.tsx | 43 +++ .../layout/MainLayout/MainLayout.tsx | 21 +- 11 files changed, 855 insertions(+), 105 deletions(-) create mode 100644 frontend/src/component/admin/auth/OldAuthSettings.tsx create mode 100644 frontend/src/component/admin/oldAdminRoutes.ts create mode 100644 frontend/src/component/layout/MainLayout/AdminMenu/AdminListItem.tsx create mode 100644 frontend/src/component/layout/MainLayout/AdminMenu/AdminMenu.tsx create mode 100644 frontend/src/component/layout/MainLayout/AdminMenu/AdminMenuIcons.tsx diff --git a/frontend/src/component/admin/Admin.tsx b/frontend/src/component/admin/Admin.tsx index 549e929246..7820c14db9 100644 --- a/frontend/src/component/admin/Admin.tsx +++ b/frontend/src/component/admin/Admin.tsx @@ -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 = () => { } /> } /> } /> - } /> + {newAdminUIEnabled ? ( + } /> + ) : ( + } /> + )} } diff --git a/frontend/src/component/admin/AdminIndex.tsx b/frontend/src/component/admin/AdminIndex.tsx index 36f021f32b..ac9b7b255f 100644 --- a/frontend/src/component/admin/AdminIndex.tsx +++ b/frontend/src/component/admin/AdminIndex.tsx @@ -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'; diff --git a/frontend/src/component/admin/adminRoutes.ts b/frontend/src/component/admin/adminRoutes.ts index 6ab6f00b24..04f26e4ae9 100644 --- a/frontend/src/component/admin/adminRoutes.ts +++ b/frontend/src/component/admin/adminRoutes.ts @@ -3,27 +3,26 @@ import type { INavigationMenuItem } from 'interfaces/route'; export const adminGroups: Record = { 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', }, ]; diff --git a/frontend/src/component/admin/auth/AuthSettings.tsx b/frontend/src/component/admin/auth/AuthSettings.tsx index b91e7f99f0..8c6721beab 100644 --- a/frontend/src/component/admin/auth/AuthSettings.tsx +++ b/frontend/src/component/admin/auth/AuthSettings.tsx @@ -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: , + label: 'Single sign-on: OpenID Connect', + path: '/admin/auth', }, { - label: 'SAML 2.0', - component: , + label: 'Single sign-on: SAML 2.0', + path: '/admin/auth/saml', }, { - label: 'Password', - component: , + label: 'Password login', + path: '/admin/auth/password', }, { - label: 'Google', - component: , + label: 'Single sign-on: Google', + path: '/admin/auth/google', }, { - label: 'SCIM', - component: , + 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 ; @@ -51,44 +53,16 @@ export const AuthSettings = () => { return (
- { - setActiveTab(tabId); - }} - indicatorColor='primary' - textColor='primary' - > - {tabs.map((tab, index) => ( - - ))} - - } - > -
- {tabs.map((tab, index) => ( - - {tab.component} - - ))} -
+ + + } /> + } /> + } /> + {googleAuthEnabled && ( + } /> + )} + } /> +
diff --git a/frontend/src/component/admin/auth/OldAuthSettings.tsx b/frontend/src/component/admin/auth/OldAuthSettings.tsx new file mode 100644 index 0000000000..c309a33c17 --- /dev/null +++ b/frontend/src/component/admin/auth/OldAuthSettings.tsx @@ -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: , + }, + { + label: 'SAML 2.0', + component: , + }, + { + label: 'Password', + component: , + }, + { + label: 'Google', + component: , + }, + { + label: 'SCIM', + component: , + }, + ].filter( + (item) => uiConfig.flags?.googleAuthEnabled || item.label !== 'Google', + ); + + const [activeTab, setActiveTab] = useState(0); + usePageTitle(`Single sign-on: ${tabs[activeTab].label}`); + + if (!isEnterprise()) { + return ; + } + + return ( +
+ + { + setActiveTab(tabId); + }} + indicatorColor='primary' + textColor='primary' + > + {tabs.map((tab, index) => ( + + ))} + + } + > +
+ {tabs.map((tab, index) => ( + + {tab.component} + + ))} +
+
+
+
+ ); +}; diff --git a/frontend/src/component/admin/oldAdminRoutes.ts b/frontend/src/component/admin/oldAdminRoutes.ts new file mode 100644 index 0000000000..6ab6f00b24 --- /dev/null +++ b/frontend/src/component/admin/oldAdminRoutes.ts @@ -0,0 +1,122 @@ +import type { INavigationMenuItem } from 'interfaces/route'; + +export const adminGroups: Record = { + 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', + }, +]; diff --git a/frontend/src/component/admin/useAdminRoutes.ts b/frontend/src/component/admin/useAdminRoutes.ts index 00e93615a7..ba3f6c1b04 100644 --- a/frontend/src/component/admin/useAdminRoutes.ts +++ b/frontend/src/component/admin/useAdminRoutes.ts @@ -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'; diff --git a/frontend/src/component/layout/MainLayout/AdminMenu/AdminListItem.tsx b/frontend/src/component/layout/MainLayout/AdminMenu/AdminListItem.tsx new file mode 100644 index 0000000000..c9059271a4 --- /dev/null +++ b/frontend/src/component/layout/MainLayout/AdminMenu/AdminListItem.tsx @@ -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 ( + + } + aria-controls='configure-content' + id='configure-header' + sx={listItemButtonStyle} + > + + {isActiveMenu ? activeIcon : icon} + + + + {title} + + + + {children} + + ); +}; + +export const AdminListItem: FC<{ + href: string; + text: string; + badge?: ReactNode; + selected?: boolean; + children?: React.ReactNode; + onClick: () => void; +}> = ({ href, text, badge, selected, children, onClick }) => { + return ( + + + {children} + + {text} + + {badge} + + + ); +}; + +export const AdminSubListItem: FC<{ + href: string; + text: string; + badge?: ReactNode; + selected?: boolean; + children?: React.ReactNode; + onClick: () => void; +}> = ({ href, text, badge, selected, children, onClick }) => { + return ( + + + {children} + + {text} + + {badge} + + + ); +}; diff --git a/frontend/src/component/layout/MainLayout/AdminMenu/AdminMenu.tsx b/frontend/src/component/layout/MainLayout/AdminMenu/AdminMenu.tsx new file mode 100644 index 0000000000..f30ad1e777 --- /dev/null +++ b/frontend/src/component/layout/MainLayout/AdminMenu/AdminMenu.tsx @@ -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 {children}; + } + + return <>{children}; +}; + +const DashboardLink = () => { + return ( + <> + } + > + Back to Unleash + + + ); +}; + +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, 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: , + }); + } + if (!route.group) { + acc[route.path] = { + href: route.path, + text: route.title, + }; + } + return acc; + }, + {}, + ); + + const items = Object.values(menuStructure); + + return ( + + + + + Admin settings + + + {items.map((item) => { + if (item.items) { + const isActiveMenu = item.items.find( + (itm) => isActiveItem(itm.href), + ); + return ( + + } + activeIcon={ + + } + isActiveMenu={Boolean(isActiveMenu)} + key={item.text} + > + {item.items.map((subItem) => ( + + + + ))} + + ); + } + return ( + + + + ); + })} + + + + + + {children} + + + ); +}; diff --git a/frontend/src/component/layout/MainLayout/AdminMenu/AdminMenuIcons.tsx b/frontend/src/component/layout/MainLayout/AdminMenu/AdminMenuIcons.tsx new file mode 100644 index 0000000000..c20ec3a228 --- /dev/null +++ b/frontend/src/component/layout/MainLayout/AdminMenu/AdminMenuIcons.tsx @@ -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 = { + '/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 ( + + ); +}; diff --git a/frontend/src/component/layout/MainLayout/MainLayout.tsx b/frontend/src/component/layout/MainLayout/MainLayout.tsx index 8a279e1d77..8efa4818a5 100644 --- a/frontend/src/component/layout/MainLayout/MainLayout.tsx +++ b/frontend/src/component/layout/MainLayout/MainLayout.tsx @@ -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( >
- - - - - - {children} - - + + + + + + + {children} + + +