1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: plan specific navigation (#7126)

This commit is contained in:
Mateusz Kwasniewski 2024-05-23 12:44:00 +02:00 committed by GitHub
parent a4c48a16a4
commit b783e89c88
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 130 additions and 98 deletions

View File

@ -1,5 +1,5 @@
import { forwardRef, type ReactNode } from 'react'; import { forwardRef, type ReactNode } from 'react';
import { Box, Grid, styled } from '@mui/material'; import { Box, Grid, styled, useMediaQuery, useTheme } from '@mui/material';
import Header from 'component/menu/Header/Header'; import Header from 'component/menu/Header/Header';
import Footer from 'component/menu/Footer/Footer'; import Footer from 'component/menu/Footer/Footer';
import Proclamation from 'component/common/Proclamation/Proclamation'; import Proclamation from 'component/common/Proclamation/Proclamation';
@ -105,6 +105,8 @@ export const MainLayout = forwardRef<HTMLDivElement, IMainLayoutProps>(
const StyledMainLayoutContent = SpaciousMainLayoutContent; const StyledMainLayoutContent = SpaciousMainLayoutContent;
const sidebarNavigationEnabled = useUiFlag('navigationSidebar'); const sidebarNavigationEnabled = useUiFlag('navigationSidebar');
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
return ( return (
<> <>
@ -120,9 +122,16 @@ export const MainLayout = forwardRef<HTMLDivElement, IMainLayoutProps>(
)} )}
show={<DraftBanner project={projectId || ''} />} show={<DraftBanner project={projectId || ''} />}
/> />
<Box sx={{ display: 'flex', mt: '2px' }}> <Box
sx={(theme) => ({
display: 'flex',
mt: theme.spacing(0.25),
})}
>
<ConditionallyRender <ConditionallyRender
condition={sidebarNavigationEnabled} condition={
sidebarNavigationEnabled && !isSmallScreen
}
show={<NavigationSidebar />} show={<NavigationSidebar />}
/> />
<StyledMainLayoutContent <StyledMainLayoutContent

View File

@ -6,6 +6,7 @@ import {
ListItemIcon, ListItemIcon,
ListItemText, ListItemText,
styled, styled,
Tooltip,
Typography, Typography,
} from '@mui/material'; } from '@mui/material';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@ -37,8 +38,19 @@ import LicenseIcon from '@mui/icons-material/ReceiptLongOutlined';
import InstancePrivacyIcon from '@mui/icons-material/ShieldOutlined'; import InstancePrivacyIcon from '@mui/icons-material/ShieldOutlined';
import LoginHistoryIcon from '@mui/icons-material/HistoryOutlined'; import LoginHistoryIcon from '@mui/icons-material/HistoryOutlined';
import EventLogIcon from '@mui/icons-material/EventNoteOutlined'; import EventLogIcon from '@mui/icons-material/EventNoteOutlined';
import FlagTypesIcon from '@mui/icons-material/OutlinedFlag';
import EmptyIcon from '@mui/icons-material/CheckBoxOutlineBlankOutlined';
import CorsIcon from '@mui/icons-material/StorageOutlined';
import BillingIcon from '@mui/icons-material/CreditCardOutlined';
import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg'; import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg';
import type { FC } from 'react'; import { type FC, type ReactNode, useCallback } from 'react';
import { getCondensedRoutes, getRoutes } from '../../../menu/routes';
import { useAdminRoutes } from '../../../admin/useAdminRoutes';
import { filterByConfig, mapRouteLink } from 'component/common/util';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import type SvgIcon from '@mui/material/SvgIcon/SvgIcon';
import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadge';
import type { INavigationMenuItem } from 'interfaces/route';
export const StyledProjectIcon = styled(ProjectIcon)(({ theme }) => ({ export const StyledProjectIcon = styled(ProjectIcon)(({ theme }) => ({
fill: theme.palette.neutral.main, fill: theme.palette.neutral.main,
@ -49,9 +61,23 @@ export const StyledProjectIcon = styled(ProjectIcon)(({ theme }) => ({
fontSize: theme.spacing(3), fontSize: theme.spacing(3),
})); }));
const StyledListItem: FC<{ href: string; text: string }> = ({ const StyledBadgeContainer = styled('div')(({ theme }) => ({
paddingLeft: theme.spacing(2),
display: 'flex',
}));
const EnterprisePlanBadge = () => (
<Tooltip title='This is an Enterprise feature'>
<StyledBadgeContainer>
<EnterpriseBadge />
</StyledBadgeContainer>
</Tooltip>
);
const StyledListItem: FC<{ href: string; text: string; badge?: ReactNode }> = ({
href, href,
text, text,
badge,
children, children,
}) => { }) => {
return ( return (
@ -61,6 +87,7 @@ const StyledListItem: FC<{ href: string; text: string }> = ({
{children} {children}
</ListItemIcon> </ListItemIcon>
<ListItemText primary={text} /> <ListItemText primary={text} />
{badge}
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
); );
@ -73,7 +100,75 @@ export const StyledBox = styled(Box)(({ theme }) => ({
minHeight: '95vh', minHeight: '95vh',
})); }));
const icons: Record<string, typeof SvgIcon> = {
'/applications': ApplicationsIcon,
'/context': ContextFieldsIcon,
'/feature-toggle-type': FlagTypesIcon,
'/integrations': IntegrationsIcon,
'/segments': SegmentsIcon,
'/strategies': CustomStrategiesIcon,
'/tag-types': TagTypesIcon,
'/environments': EnvironmentsIcon,
'/admin/users': UsersIcon,
'/admin/service-accounts': ServiceAccountIcon,
'/admin/groups': GroupsIcon,
'/admin/roles': RoleIcon,
'/admin/api': ApiAccessIcon,
'/admin/auth': SingleSignOnIcon,
'/admin/network': NetworkIcon,
'/admin/maintenance': MaintenanceIcon,
'/admin/banners': BannersIcon,
'/admin/instance': InstanceStatsIcon,
'/admin/license': LicenseIcon,
'/admin/instance-privacy': InstancePrivacyIcon,
'/admin/logins': LoginHistoryIcon,
'/admin/cors': CorsIcon,
'/admin/billing': BillingIcon,
'/history': EventLogIcon,
};
const findIconByPath = (path: string) => {
return icons[path] || EmptyIcon;
};
const IconRenderer: FC<{ path: string }> = ({ path }) => {
const IconComponent = findIconByPath(path); // Fallback to 'default' if the type is not found
return <IconComponent />;
};
const useRoutes = () => {
const { uiConfig, isPro } = useUiConfig();
const routes = getRoutes();
const adminRoutes = useAdminRoutes();
const filteredMainRoutes = {
mainNavRoutes: getCondensedRoutes(routes.mainNavRoutes)
.filter(filterByConfig(uiConfig))
.map(mapRouteLink),
mobileRoutes: getCondensedRoutes(routes.mobileRoutes)
.filter(filterByConfig(uiConfig))
.map(mapRouteLink),
adminRoutes,
};
const showBadge = useCallback(
(mode?: INavigationMenuItem['menu']['mode']) => {
return !!(
isPro() &&
!mode?.includes('pro') &&
mode?.includes('enterprise')
);
},
[isPro],
);
return { routes: filteredMainRoutes, showBadge };
};
export const NavigationSidebar = () => { export const NavigationSidebar = () => {
const { routes, showBadge } = useRoutes();
return ( return (
<StyledBox> <StyledBox>
<List> <List>
@ -102,39 +197,14 @@ export const NavigationSidebar = () => {
</AccordionSummary> </AccordionSummary>
<AccordionDetails sx={{ p: 0 }}> <AccordionDetails sx={{ p: 0 }}>
<List> <List>
<StyledListItem {routes.mainNavRoutes.map((route) => (
href='/integrations' <StyledListItem
text='Integrations' href={route.path}
> text={route.title}
<IntegrationsIcon /> >
</StyledListItem> <IconRenderer path={route.path} />
<StyledListItem </StyledListItem>
href='/environments' ))}
text='Environments'
>
<EnvironmentsIcon />
</StyledListItem>
<StyledListItem href='/context' text='Context fields'>
<ContextFieldsIcon />
</StyledListItem>
<StyledListItem href='/segments' text='Segments'>
<SegmentsIcon />
</StyledListItem>
<StyledListItem href='/tag-types' text='Tag types'>
<TagTypesIcon />
</StyledListItem>
<StyledListItem
href='/applications'
text='Applications'
>
<ApplicationsIcon />
</StyledListItem>
<StyledListItem
href='/strategies'
text='Custom strategies'
>
<CustomStrategiesIcon />
</StyledListItem>
</List> </List>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
@ -150,66 +220,19 @@ export const NavigationSidebar = () => {
</AccordionSummary> </AccordionSummary>
<AccordionDetails sx={{ p: 0 }}> <AccordionDetails sx={{ p: 0 }}>
<List> <List>
<StyledListItem href='/admin/users' text='Users'> {routes.adminRoutes.map((route) => (
<UsersIcon /> <StyledListItem
</StyledListItem> href={route.path}
<StyledListItem text={route.title}
href='/admin/service-accounts' badge={
text='Service accounts' showBadge(route?.menu?.mode) ? (
> <EnterprisePlanBadge />
<ServiceAccountIcon /> ) : null
</StyledListItem> }
<StyledListItem href='/admin/groups' text='Groups'> >
<GroupsIcon /> <IconRenderer path={route.path} />
</StyledListItem> </StyledListItem>
<StyledListItem href='/admin/roles' text='Roles'> ))}
<RoleIcon />
</StyledListItem>
<StyledListItem href='/admin/api' text='API Access'>
<ApiAccessIcon />
</StyledListItem>
<StyledListItem
href='/admin/auth'
text='Single sign-on'
>
<SingleSignOnIcon />
</StyledListItem>
<StyledListItem href='/admin/network' text='Network'>
<NetworkIcon />
</StyledListItem>
<StyledListItem
href='/admin/maintenance'
text='Maintenance'
>
<MaintenanceIcon />
</StyledListItem>
<StyledListItem href='/admin/banners' text='Banners'>
<BannersIcon />
</StyledListItem>
<StyledListItem
href='/admin/instance'
text='Instance stats'
>
<InstanceStatsIcon />
</StyledListItem>
<StyledListItem href='/admin/license' text='License'>
<LicenseIcon />
</StyledListItem>
<StyledListItem
href='/admin/instance-privacy'
text='Instance privacy'
>
<InstancePrivacyIcon />
</StyledListItem>
<StyledListItem
href='/admin/logins'
text='Login history'
>
<LoginHistoryIcon />
</StyledListItem>
<StyledListItem href='/history' text='Event log'>
<EventLogIcon />
</StyledListItem>
</List> </List>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>