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:
parent
328c5368ed
commit
07a4106f48
@ -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 />}
|
||||
|
@ -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';
|
||||
|
@ -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',
|
||||
},
|
||||
];
|
||||
|
@ -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>
|
||||
|
96
frontend/src/component/admin/auth/OldAuthSettings.tsx
Normal file
96
frontend/src/component/admin/auth/OldAuthSettings.tsx
Normal 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>
|
||||
);
|
||||
};
|
122
frontend/src/component/admin/oldAdminRoutes.ts
Normal file
122
frontend/src/component/admin/oldAdminRoutes.ts
Normal 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',
|
||||
},
|
||||
];
|
@ -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';
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
258
frontend/src/component/layout/MainLayout/AdminMenu/AdminMenu.tsx
Normal file
258
frontend/src/component/layout/MainLayout/AdminMenu/AdminMenu.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user