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:
parent
bec23e5c20
commit
ebce31066a
@ -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
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user