1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-10 01:16:39 +02:00

feat: New mobile sidebar (#7135)

This commit is contained in:
Mateusz Kwasniewski 2024-05-24 09:35:03 +02:00 committed by GitHub
parent bec23e5c20
commit ebce31066a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 237 additions and 77 deletions

View File

@ -43,6 +43,7 @@ import FlagTypesIcon from '@mui/icons-material/OutlinedFlag';
import EmptyIcon from '@mui/icons-material/CheckBoxOutlineBlankOutlined'; import EmptyIcon from '@mui/icons-material/CheckBoxOutlineBlankOutlined';
import CorsIcon from '@mui/icons-material/StorageOutlined'; import CorsIcon from '@mui/icons-material/StorageOutlined';
import BillingIcon from '@mui/icons-material/CreditCardOutlined'; import BillingIcon from '@mui/icons-material/CreditCardOutlined';
import SignOutIcon from '@mui/icons-material/ExitToApp';
import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg'; import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg';
import { import {
type FC, type FC,
@ -60,6 +61,9 @@ import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadg
import type { INavigationMenuItem } from 'interfaces/route'; import type { INavigationMenuItem } from 'interfaces/route';
import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import GitHubIcon from '@mui/icons-material/GitHub';
import LibraryBooksIcon from '@mui/icons-material/LibraryBooks';
import { basePath } from 'utils/formatPath';
export const StyledProjectIcon = styled(ProjectIcon)(({ theme }) => ({ export const StyledProjectIcon = styled(ProjectIcon)(({ theme }) => ({
fill: theme.palette.neutral.main, fill: theme.palette.neutral.main,
@ -83,14 +87,14 @@ const EnterprisePlanBadge = () => (
</Tooltip> </Tooltip>
); );
const FullListItem: FC<{ href: string; text: string; badge?: ReactNode }> = ({ const FullListItem: FC<{
href, href: string;
text, text: string;
badge, badge?: ReactNode;
children, onClick?: () => void;
}) => { }> = ({ href, text, badge, onClick, children }) => {
return ( return (
<ListItem disablePadding> <ListItem disablePadding onClick={onClick}>
<ListItemButton dense={true} component={Link} to={href}> <ListItemButton dense={true} component={Link} to={href}>
<ListItemIcon sx={(theme) => ({ minWidth: theme.spacing(4) })}> <ListItemIcon sx={(theme) => ({ minWidth: theme.spacing(4) })}>
{children} {children}
@ -102,6 +106,46 @@ const FullListItem: FC<{ href: string; text: string; badge?: ReactNode }> = ({
); );
}; };
const ExternalFullListItem: FC<{ href: string; text: string }> = ({
href,
text,
children,
}) => {
return (
<ListItem disablePadding>
<ListItemButton
dense={true}
component={Link}
to={href}
rel='noopener noreferrer'
target='_blank'
>
<ListItemIcon sx={(theme) => ({ minWidth: theme.spacing(4) })}>
{children}
</ListItemIcon>
<ListItemText primary={text} />
</ListItemButton>
</ListItem>
);
};
const SignOutItem = () => {
return (
<form method='POST' action={`${basePath}/logout`}>
<ListItem disablePadding>
<ListItemButton dense={true} component='button' type='submit'>
<ListItemIcon
sx={(theme) => ({ minWidth: theme.spacing(4) })}
>
<SignOutIcon />
</ListItemIcon>
<ListItemText primary='Sign out' />
</ListItemButton>
</ListItem>
</form>
);
};
const MiniListItem: FC<{ href: string; text: string }> = ({ const MiniListItem: FC<{ href: string; text: string }> = ({
href, href,
text, text,
@ -156,6 +200,8 @@ const icons: Record<string, typeof SvgIcon> = {
'/admin/cors': CorsIcon, '/admin/cors': CorsIcon,
'/admin/billing': BillingIcon, '/admin/billing': BillingIcon,
'/history': EventLogIcon, '/history': EventLogIcon,
GitHub: GitHubIcon,
Documentation: LibraryBooksIcon,
}; };
const findIconByPath = (path: string) => { const findIconByPath = (path: string) => {
@ -199,7 +245,7 @@ const ShowHide: FC<{ mode: 'full' | 'mini'; onChange: () => void }> = ({
}; };
const useRoutes = () => { const useRoutes = () => {
const { uiConfig, isPro } = useUiConfig(); const { uiConfig } = useUiConfig();
const routes = getRoutes(); const routes = getRoutes();
const adminRoutes = useAdminRoutes(); const adminRoutes = useAdminRoutes();
@ -213,6 +259,12 @@ const useRoutes = () => {
adminRoutes, adminRoutes,
}; };
return { routes: filteredMainRoutes };
};
const useShowBadge = () => {
const { isPro } = useUiConfig();
const showBadge = useCallback( const showBadge = useCallback(
(mode?: INavigationMenuItem['menu']['mode']) => { (mode?: INavigationMenuItem['menu']['mode']) => {
return !!( return !!(
@ -223,8 +275,7 @@ const useRoutes = () => {
}, },
[isPro], [isPro],
); );
return showBadge;
return { routes: filteredMainRoutes, showBadge };
}; };
const useNavigationMode = () => { const useNavigationMode = () => {
@ -247,29 +298,137 @@ const useNavigationMode = () => {
return [mode, setMode] as const; return [mode, setMode] as const;
}; };
export const NavigationSidebar = () => { const MainNavigationList: FC<{ mode: 'mini' | 'full'; onClick?: () => void }> =
const { routes, showBadge } = useRoutes(); ({ mode, onClick }) => {
const [mode, setMode] = useNavigationMode();
const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem; const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem;
return ( return (
<StyledBox>
<List> <List>
<DynamicListItem href='/projects' text='Projects'> <DynamicListItem
href='/projects'
text='Projects'
onClick={onClick}
>
<StyledProjectIcon /> <StyledProjectIcon />
</DynamicListItem> </DynamicListItem>
<DynamicListItem href='/search' text='Search'> <DynamicListItem href='/search' text='Search' onClick={onClick}>
<SearchIcon /> <SearchIcon />
</DynamicListItem> </DynamicListItem>
<DynamicListItem href='/playground' text='Playground'> <DynamicListItem
href='/playground'
text='Playground'
onClick={onClick}
>
<PlaygroundIcon /> <PlaygroundIcon />
</DynamicListItem> </DynamicListItem>
<DynamicListItem href='/insights' text='Insights'> <DynamicListItem
href='/insights'
text='Insights'
onClick={onClick}
>
<InsightsIcon /> <InsightsIcon />
</DynamicListItem> </DynamicListItem>
</List> </List>
);
};
const ConfigureNavigationList: FC<{
routes: INavigationMenuItem[];
mode: 'mini' | 'full';
onClick?: () => void;
}> = ({ routes, mode, onClick }) => {
const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem;
return (
<List>
{routes.map((route) => (
<DynamicListItem
href={route.path}
text={route.title}
onClick={onClick}
>
<IconRenderer path={route.path} />
</DynamicListItem>
))}
</List>
);
};
const AdminNavigationList: FC<{
routes: INavigationMenuItem[];
mode: 'mini' | 'full';
badge?: ReactNode;
onClick?: () => void;
}> = ({ routes, mode, onClick, badge }) => {
const showBadge = useShowBadge();
const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem;
return (
<List>
{routes.map((route) => (
<DynamicListItem
onClick={onClick}
href={route.path}
text={route.title}
badge={
showBadge(route?.menu?.mode) ? (
<EnterprisePlanBadge />
) : null
}
>
<IconRenderer path={route.path} />
</DynamicListItem>
))}
</List>
);
};
const OtherLinksList = () => {
const { uiConfig } = useUiConfig();
return (
<List>
{uiConfig.links.map((link) => (
<ExternalFullListItem href={link.href} text={link.value}>
<IconRenderer path={link.value} />
</ExternalFullListItem>
))}
<SignOutItem />
</List>
);
};
export const MobileNavigationSidebar: FC<{ onClick: () => void }> = ({
onClick,
}) => {
const { routes } = useRoutes();
return (
<>
<MainNavigationList mode='full' onClick={onClick} />
<ConfigureNavigationList
routes={routes.mainNavRoutes}
mode='full'
onClick={onClick}
/>
<AdminNavigationList
routes={routes.adminRoutes}
mode='full'
onClick={onClick}
/>
<OtherLinksList />
</>
);
};
export const NavigationSidebar = () => {
const { routes } = useRoutes();
const [mode, setMode] = useNavigationMode();
return (
<StyledBox>
<MainNavigationList mode={mode} />
<Accordion disableGutters={true} sx={{ boxShadow: 'none' }}> <Accordion disableGutters={true} sx={{ boxShadow: 'none' }}>
{mode === 'full' && ( {mode === 'full' && (
<AccordionSummary <AccordionSummary
@ -285,16 +444,10 @@ export const NavigationSidebar = () => {
</AccordionSummary> </AccordionSummary>
)} )}
<AccordionDetails sx={{ p: 0 }}> <AccordionDetails sx={{ p: 0 }}>
<List> <ConfigureNavigationList
{routes.mainNavRoutes.map((route) => ( routes={routes.mainNavRoutes}
<DynamicListItem mode={mode}
href={route.path} />
text={route.title}
>
<IconRenderer path={route.path} />
</DynamicListItem>
))}
</List>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
<Accordion disableGutters={true} sx={{ boxShadow: 'none' }}> <Accordion disableGutters={true} sx={{ boxShadow: 'none' }}>
@ -313,21 +466,10 @@ export const NavigationSidebar = () => {
)} )}
<AccordionDetails sx={{ p: 0 }}> <AccordionDetails sx={{ p: 0 }}>
<List> <AdminNavigationList
{routes.adminRoutes.map((route) => ( routes={routes.adminRoutes}
<DynamicListItem mode={mode}
href={route.path} />
text={route.title}
badge={
showBadge(route?.menu?.mode) ? (
<EnterprisePlanBadge />
) : null
}
>
<IconRenderer path={route.path} />
</DynamicListItem>
))}
</List>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
<ShowHide <ShowHide

View File

@ -12,6 +12,9 @@ import type { INavigationMenuItem } from 'interfaces/route';
import styles from './DrawerMenu.module.scss'; // FIXME: useStyle - theme import styles from './DrawerMenu.module.scss'; // FIXME: useStyle - theme
import theme from 'themes/theme'; import theme from 'themes/theme';
import { ThemeMode } from 'component/common/ThemeMode/ThemeMode'; import { ThemeMode } from 'component/common/ThemeMode/ThemeMode';
import { MobileNavigationSidebar } from 'component/layout/MainLayout/NavigationSidebar/NavigationSidebar';
import { useUiFlag } from 'hooks/useUiFlag';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
const StyledDrawerHeader = styled('div')(({ theme }) => ({ const StyledDrawerHeader = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@ -48,6 +51,8 @@ export const DrawerMenu: VFC<IDrawerMenuProps> = ({
toggleDrawer, toggleDrawer,
routes, routes,
}) => { }) => {
const sidebarNavigationEnabled = useUiFlag('navigationSidebar');
const renderLinks = () => { const renderLinks = () => {
return links.map((link) => { return links.map((link) => {
let icon = null; let icon = null;
@ -99,6 +104,11 @@ export const DrawerMenu: VFC<IDrawerMenuProps> = ({
</Link> </Link>
</StyledDrawerHeader> </StyledDrawerHeader>
<Divider /> <Divider />
<ConditionallyRender
condition={Boolean(sidebarNavigationEnabled)}
show={<MobileNavigationSidebar onClick={toggleDrawer} />}
elseShow={
<>
<List className={styles.drawerList}> <List className={styles.drawerList}>
{routes.mobileRoutes.map((item) => ( {routes.mobileRoutes.map((item) => (
<NavigationLink <NavigationLink
@ -125,11 +135,19 @@ export const DrawerMenu: VFC<IDrawerMenuProps> = ({
<Divider /> <Divider />
<div className={styles.iconLinkList}> <div className={styles.iconLinkList}>
{renderLinks()} {renderLinks()}
<a className={styles.iconLink} href={`${basePath}/logout`}> <a
<ExitToApp className={styles.navigationIcon} /> className={styles.iconLink}
href={`${basePath}/logout`}
>
<ExitToApp
className={styles.navigationIcon}
/>
Sign out Sign out
</a> </a>
</div> </div>
</>
}
/>
</nav> </nav>
</Drawer> </Drawer>
); );