1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-18 00:19:49 +01:00

refactor: navigation sidebar (#7171)

This commit is contained in:
Mateusz Kwasniewski 2024-05-27 16:29:20 +02:00 committed by GitHub
parent c8db321b3e
commit 08629e9041
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 508 additions and 490 deletions

View File

@ -0,0 +1,79 @@
import EmptyIcon from '@mui/icons-material/CheckBoxOutlineBlankOutlined';
import type SvgIcon from '@mui/material/SvgIcon/SvgIcon';
import ApplicationsIcon from '@mui/icons-material/AppsOutlined';
import ContextFieldsIcon from '@mui/icons-material/AccountTreeOutlined';
import FlagTypesIcon from '@mui/icons-material/OutlinedFlag';
import IntegrationsIcon from '@mui/icons-material/IntegrationInstructionsOutlined';
import SegmentsIcon from '@mui/icons-material/DonutLargeOutlined';
import CustomStrategiesIcon from '@mui/icons-material/ExtensionOutlined';
import TagTypesIcon from '@mui/icons-material/LabelImportantOutlined';
import EnvironmentsIcon from '@mui/icons-material/CloudOutlined';
import UsersIcon from '@mui/icons-material/GroupOutlined';
import ServiceAccountIcon from '@mui/icons-material/SmartToyOutlined';
import GroupsIcon from '@mui/icons-material/GroupsOutlined';
import RoleIcon from '@mui/icons-material/AdminPanelSettingsOutlined';
import ApiAccessIcon from '@mui/icons-material/KeyOutlined';
import SingleSignOnIcon from '@mui/icons-material/AssignmentOutlined';
import NetworkIcon from '@mui/icons-material/HubOutlined';
import MaintenanceIcon from '@mui/icons-material/BuildOutlined';
import BannersIcon from '@mui/icons-material/PhotoOutlined';
import InstanceStatsIcon from '@mui/icons-material/QueryStatsOutlined';
import LicenseIcon from '@mui/icons-material/ReceiptLongOutlined';
import InstancePrivacyIcon from '@mui/icons-material/ShieldOutlined';
import LoginHistoryIcon from '@mui/icons-material/HistoryOutlined';
import CorsIcon from '@mui/icons-material/StorageOutlined';
import BillingIcon from '@mui/icons-material/CreditCardOutlined';
import EventLogIcon from '@mui/icons-material/EventNoteOutlined';
import GitHubIcon from '@mui/icons-material/GitHub';
import LibraryBooksIcon from '@mui/icons-material/LibraryBooks';
import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg';
import type { FC } from 'react';
import { styled } from '@mui/material';
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,
GitHub: GitHubIcon,
Documentation: LibraryBooksIcon,
};
const findIcon = (key: string) => {
return icons[key] || EmptyIcon;
};
export const IconRenderer: FC<{ path: string }> = ({ path }) => {
const IconComponent = findIcon(path); // Fallback to 'default' if the type is not found
return <IconComponent />;
};
export const StyledProjectIcon = styled(ProjectIcon)(({ theme }) => ({
fill: theme.palette.neutral.main,
stroke: theme.palette.neutral.main,
// same as built-in icons
width: theme.spacing(3),
height: theme.spacing(3),
fontSize: theme.spacing(3),
}));

View File

@ -0,0 +1,108 @@
import type { FC, ReactNode } from 'react';
import {
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Tooltip,
} from '@mui/material';
import { Link } from 'react-router-dom';
import { basePath } from 'utils/formatPath';
import SignOutIcon from '@mui/icons-material/ExitToApp';
import type { Theme } from '@mui/material/styles/createTheme';
const listItemButtonStyle = (theme: Theme) => ({
borderRadius: theme.spacing(0.5),
borderLeft: `${theme.spacing(0.5)} solid transparent`,
'&:hover': {
borderLeft: `${theme.spacing(0.5)} solid ${theme.palette.primary.main}`,
},
});
export const FullListItem: FC<{
href: string;
text: string;
badge?: ReactNode;
onClick?: () => void;
}> = ({ href, text, badge, onClick, children }) => {
return (
<ListItem disablePadding onClick={onClick}>
<ListItemButton
dense={true}
component={Link}
to={href}
sx={listItemButtonStyle}
>
<ListItemIcon sx={(theme) => ({ minWidth: theme.spacing(4) })}>
{children}
</ListItemIcon>
<ListItemText sx={{ whiteSpace: 'nowrap' }} primary={text} />
{badge}
</ListItemButton>
</ListItem>
);
};
export 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>
);
};
export 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>
);
};
export const MiniListItem: FC<{ href: string; text: string }> = ({
href,
text,
children,
}) => {
return (
<ListItem disablePadding>
<ListItemButton
dense={true}
component={Link}
to={href}
sx={listItemButtonStyle}
>
<Tooltip title={text} placement='right'>
<ListItemIcon
sx={(theme) => ({ minWidth: theme.spacing(4) })}
>
{children}
</ListItemIcon>
</Tooltip>
</ListItemButton>
</ListItem>
);
};

View File

@ -0,0 +1,179 @@
import { type FC, type ReactNode, useCallback } from 'react';
import type { INavigationMenuItem } from 'interfaces/route';
import type { NavigationMode } from './NavigationMode';
import {
ExternalFullListItem,
FullListItem,
MiniListItem,
SignOutItem,
} from './ListItems';
import { List, styled, Tooltip, Typography } from '@mui/material';
import { IconRenderer, StyledProjectIcon } from './IconRenderer';
import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadge';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import SearchIcon from '@mui/icons-material/Search';
import PlaygroundIcon from '@mui/icons-material/AutoFixNormal';
import InsightsIcon from '@mui/icons-material/Insights';
import Accordion from '@mui/material/Accordion';
import AccordionDetails from '@mui/material/AccordionDetails';
import AccordionSummary from '@mui/material/AccordionSummary';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
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 useShowBadge = () => {
const { isPro } = useUiConfig();
const showBadge = useCallback(
(mode?: INavigationMenuItem['menu']['mode']) => {
return !!(
isPro() &&
!mode?.includes('pro') &&
mode?.includes('enterprise')
);
},
[isPro],
);
return showBadge;
};
export const ConfigureNavigationList: FC<{
routes: INavigationMenuItem[];
mode: NavigationMode;
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>
);
};
export const AdminNavigationList: FC<{
routes: INavigationMenuItem[];
mode: NavigationMode;
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>
);
};
export 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 PrimaryNavigationList: FC<{
mode: NavigationMode;
onClick?: () => void;
}> = ({ mode, onClick }) => {
const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem;
return (
<List>
<DynamicListItem href='/projects' text='Projects' onClick={onClick}>
<StyledProjectIcon />
</DynamicListItem>
<DynamicListItem href='/search' text='Search' onClick={onClick}>
<SearchIcon />
</DynamicListItem>
<DynamicListItem
href='/playground'
text='Playground'
onClick={onClick}
>
<PlaygroundIcon />
</DynamicListItem>
<DynamicListItem href='/insights' text='Insights' onClick={onClick}>
<InsightsIcon />
</DynamicListItem>
</List>
);
};
const AccordionHeader: FC = ({ children }) => {
return (
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls='configure-content'
id='configure-header'
>
<Typography sx={{ fontWeight: 'bold', fontSize: 'small' }}>
{children}
</Typography>
</AccordionSummary>
);
};
export const SecondaryNavigation: FC<{
expanded: boolean;
onChange: (expanded: boolean) => void;
mode: NavigationMode;
routes: INavigationMenuItem[];
}> = ({ mode, expanded, onChange, routes, children }) => {
return (
<Accordion
disableGutters={true}
sx={{ boxShadow: 'none' }}
expanded={expanded}
onChange={(_, expand) => {
onChange(expand);
}}
>
{mode === 'full' && <AccordionHeader>{children}</AccordionHeader>}
<AccordionDetails sx={{ p: 0 }}>
<ConfigureNavigationList routes={routes} mode={mode} />
</AccordionDetails>
</Accordion>
);
};

View File

@ -0,0 +1 @@
export type NavigationMode = 'mini' | 'full';

View File

@ -1,427 +1,16 @@
import { Box, styled } from '@mui/material';
import type { FC } from 'react';
import { useNavigationMode } from './useNavigationMode';
import { ShowHide } from './ShowHide';
import { useRoutes } from './useRoutes';
import { useExpanded } from './useExpanded';
import {
Box,
IconButton,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
styled,
Tooltip,
Typography,
} from '@mui/material';
import { Link } from 'react-router-dom';
import SearchIcon from '@mui/icons-material/Search';
import PlaygroundIcon from '@mui/icons-material/AutoFixNormal';
import InsightsIcon from '@mui/icons-material/Insights';
import Accordion from '@mui/material/Accordion';
import AccordionSummary from '@mui/material/AccordionSummary';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import AccordionDetails from '@mui/material/AccordionDetails';
import IntegrationsIcon from '@mui/icons-material/IntegrationInstructionsOutlined';
import EnvironmentsIcon from '@mui/icons-material/CloudOutlined';
import ContextFieldsIcon from '@mui/icons-material/AccountTreeOutlined';
import SegmentsIcon from '@mui/icons-material/DonutLargeOutlined';
import TagTypesIcon from '@mui/icons-material/LabelImportantOutlined';
import ApplicationsIcon from '@mui/icons-material/AppsOutlined';
import CustomStrategiesIcon from '@mui/icons-material/ExtensionOutlined';
import UsersIcon from '@mui/icons-material/GroupOutlined';
import ServiceAccountIcon from '@mui/icons-material/SmartToyOutlined';
import GroupsIcon from '@mui/icons-material/GroupsOutlined';
import RoleIcon from '@mui/icons-material/AdminPanelSettingsOutlined';
import ApiAccessIcon from '@mui/icons-material/KeyOutlined';
import SingleSignOnIcon from '@mui/icons-material/AssignmentOutlined';
import NetworkIcon from '@mui/icons-material/HubOutlined';
import MaintenanceIcon from '@mui/icons-material/BuildOutlined';
import BannersIcon from '@mui/icons-material/PhotoOutlined';
import InstanceStatsIcon from '@mui/icons-material/QueryStatsOutlined';
import LicenseIcon from '@mui/icons-material/ReceiptLongOutlined';
import InstancePrivacyIcon from '@mui/icons-material/ShieldOutlined';
import LoginHistoryIcon from '@mui/icons-material/HistoryOutlined';
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 SignOutIcon from '@mui/icons-material/ExitToApp';
import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg';
import { type FC, type ReactNode, useCallback, useEffect } 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';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
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';
import { useLocalStorageState } from 'hooks/useLocalStorageState';
import type { Theme } from '@mui/material/styles/createTheme';
import { unique } from 'utils/unique';
export const StyledProjectIcon = styled(ProjectIcon)(({ theme }) => ({
fill: theme.palette.neutral.main,
stroke: theme.palette.neutral.main,
// same as built-in icons
width: theme.spacing(3),
height: theme.spacing(3),
fontSize: theme.spacing(3),
}));
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 listItemButtonStyle = (theme: Theme) => ({
borderRadius: theme.spacing(0.5),
borderLeft: `${theme.spacing(0.5)} solid transparent`,
'&:hover': {
borderLeft: `${theme.spacing(0.5)} solid ${theme.palette.primary.main}`,
},
});
const FullListItem: FC<{
href: string;
text: string;
badge?: ReactNode;
onClick?: () => void;
}> = ({ href, text, badge, onClick, children }) => {
return (
<ListItem disablePadding onClick={onClick}>
<ListItemButton
dense={true}
component={Link}
to={href}
sx={listItemButtonStyle}
>
<ListItemIcon sx={(theme) => ({ minWidth: theme.spacing(4) })}>
{children}
</ListItemIcon>
<ListItemText sx={{ whiteSpace: 'nowrap' }} primary={text} />
{badge}
</ListItemButton>
</ListItem>
);
};
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 }> = ({
href,
text,
children,
}) => {
return (
<ListItem disablePadding>
<ListItemButton
dense={true}
component={Link}
to={href}
sx={listItemButtonStyle}
>
<Tooltip title={text} placement='right'>
<ListItemIcon
sx={(theme) => ({ minWidth: theme.spacing(4) })}
>
{children}
</ListItemIcon>
</Tooltip>
</ListItemButton>
</ListItem>
);
};
export const StyledBox = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(2, 1, 6, 1),
}));
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,
GitHub: GitHubIcon,
Documentation: LibraryBooksIcon,
};
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 ShowHideWrapper = styled(Box, {
shouldForwardProp: (prop) => prop !== 'mode',
})<{ mode: NavigationMode }>(({ theme, mode }) => ({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
margin: theme.spacing(2, 1, 0, mode === 'mini' ? 1.5 : 2),
cursor: 'pointer',
}));
const ShowHide: FC<{ mode: NavigationMode; onChange: () => void }> = ({
mode,
onChange,
}) => {
return (
<ShowHideWrapper onClick={onChange} mode={mode}>
{mode === 'full' && (
<Box
sx={(theme) => ({
color: theme.palette.neutral.main,
fontSize: 'small',
})}
>
Hide ( + B)
</Box>
)}
<IconButton>
{mode === 'full' ? (
<ChevronLeftIcon />
) : (
<Tooltip title='Expand (⌘ + B)' placement='right'>
<ChevronRightIcon />
</Tooltip>
)}
</IconButton>
</ShowHideWrapper>
);
};
const useRoutes = () => {
const { uiConfig } = 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,
};
return { routes: filteredMainRoutes };
};
const useShowBadge = () => {
const { isPro } = useUiConfig();
const showBadge = useCallback(
(mode?: INavigationMenuItem['menu']['mode']) => {
return !!(
isPro() &&
!mode?.includes('pro') &&
mode?.includes('enterprise')
);
},
[isPro],
);
return showBadge;
};
type NavigationMode = 'mini' | 'full';
const useNavigationMode = () => {
const [mode, setMode] = useLocalStorageState<NavigationMode>(
'navigation-mode:v1',
'full',
);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'b' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
setMode(mode === 'mini' ? 'full' : 'mini');
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [mode]);
return [mode, setMode] as const;
};
const MainNavigationList: FC<{ mode: NavigationMode; onClick?: () => void }> =
({ mode, onClick }) => {
const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem;
return (
<List>
<DynamicListItem
href='/projects'
text='Projects'
onClick={onClick}
>
<StyledProjectIcon />
</DynamicListItem>
<DynamicListItem href='/search' text='Search' onClick={onClick}>
<SearchIcon />
</DynamicListItem>
<DynamicListItem
href='/playground'
text='Playground'
onClick={onClick}
>
<PlaygroundIcon />
</DynamicListItem>
<DynamicListItem
href='/insights'
text='Insights'
onClick={onClick}
>
<InsightsIcon />
</DynamicListItem>
</List>
);
};
const ConfigureNavigationList: FC<{
routes: INavigationMenuItem[];
mode: NavigationMode;
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: NavigationMode;
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>
);
};
AdminNavigationList,
ConfigureNavigationList,
OtherLinksList,
PrimaryNavigationList,
SecondaryNavigation,
} from './NavigationList';
export const MobileNavigationSidebar: FC<{ onClick: () => void }> = ({
onClick,
@ -430,7 +19,7 @@ export const MobileNavigationSidebar: FC<{ onClick: () => void }> = ({
return (
<>
<MainNavigationList mode='full' onClick={onClick} />
<PrimaryNavigationList mode='full' onClick={onClick} />
<ConfigureNavigationList
routes={routes.mainNavRoutes}
mode='full'
@ -446,22 +35,10 @@ export const MobileNavigationSidebar: FC<{ onClick: () => void }> = ({
);
};
const useExpanded = <T extends string>() => {
const [expanded, setExpanded] = useLocalStorageState<Array<T>>(
'navigation-expanded:v1',
[],
);
const changeExpanded = (key: T, expand: boolean) => {
if (expand) {
setExpanded(unique([...expanded, key]));
} else {
setExpanded(expanded.filter((name) => name !== key));
}
};
return [expanded, changeExpanded] as const;
};
export const StyledBox = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(2, 1, 6, 1),
}));
export const NavigationSidebar = () => {
const { routes } = useRoutes();
@ -471,64 +48,27 @@ export const NavigationSidebar = () => {
return (
<StyledBox>
<MainNavigationList mode={mode} />
<Accordion
disableGutters={true}
sx={{ boxShadow: 'none' }}
<PrimaryNavigationList mode={mode} />
<SecondaryNavigation
expanded={expanded.includes('configure')}
onChange={(_, expand) => {
onChange={(expand) => {
changeExpanded('configure', expand);
}}
>
{mode === 'full' && (
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls='configure-content'
id='configure-header'
>
<Typography
sx={{ fontWeight: 'bold', fontSize: 'small' }}
mode={mode}
routes={routes.mainNavRoutes}
>
Configure
</Typography>
</AccordionSummary>
)}
<AccordionDetails sx={{ p: 0 }}>
<ConfigureNavigationList
routes={routes.mainNavRoutes}
mode={mode}
/>
</AccordionDetails>
</Accordion>
<Accordion
disableGutters={true}
sx={{ boxShadow: 'none' }}
</SecondaryNavigation>
<SecondaryNavigation
expanded={expanded.includes('admin')}
onChange={(_, expand) => {
onChange={(expand) => {
changeExpanded('admin', expand);
}}
>
{mode === 'full' && (
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls='admin-content'
id='admin-header'
>
<Typography
sx={{ fontWeight: 'bold', fontSize: 'small' }}
mode={mode}
routes={routes.adminRoutes}
>
Admin
</Typography>
</AccordionSummary>
)}
<AccordionDetails sx={{ p: 0 }}>
<AdminNavigationList
routes={routes.adminRoutes}
mode={mode}
/>
</AccordionDetails>
</Accordion>
</SecondaryNavigation>
<ShowHide
mode={mode}
onChange={() => {

View File

@ -0,0 +1,44 @@
import { Box, IconButton, styled, Tooltip } from '@mui/material';
import type { NavigationMode } from './NavigationMode';
import type { FC } from 'react';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
const ShowHideWrapper = styled(Box, {
shouldForwardProp: (prop) => prop !== 'mode',
})<{ mode: NavigationMode }>(({ theme, mode }) => ({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
margin: theme.spacing(2, 1, 0, mode === 'mini' ? 1.5 : 2),
cursor: 'pointer',
}));
export const ShowHide: FC<{ mode: NavigationMode; onChange: () => void }> = ({
mode,
onChange,
}) => {
return (
<ShowHideWrapper onClick={onChange} mode={mode}>
{mode === 'full' && (
<Box
sx={(theme) => ({
color: theme.palette.neutral.main,
fontSize: 'small',
})}
>
Hide ( + B)
</Box>
)}
<IconButton>
{mode === 'full' ? (
<ChevronLeftIcon />
) : (
<Tooltip title='Expand (⌘ + B)' placement='right'>
<ChevronRightIcon />
</Tooltip>
)}
</IconButton>
</ShowHideWrapper>
);
};

View File

@ -0,0 +1,19 @@
import { useLocalStorageState } from 'hooks/useLocalStorageState';
import { unique } from 'utils/unique';
export const useExpanded = <T extends string>() => {
const [expanded, setExpanded] = useLocalStorageState<Array<T>>(
'navigation-expanded:v1',
[],
);
const changeExpanded = (key: T, expand: boolean) => {
if (expand) {
setExpanded(unique([...expanded, key]));
} else {
setExpanded(expanded.filter((name) => name !== key));
}
};
return [expanded, changeExpanded] as const;
};

View File

@ -0,0 +1,26 @@
import { useLocalStorageState } from 'hooks/useLocalStorageState';
import type { NavigationMode } from './NavigationMode';
import { useEffect } from 'react';
export const useNavigationMode = () => {
const [mode, setMode] = useLocalStorageState<NavigationMode>(
'navigation-mode:v1',
'full',
);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'b' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
setMode(mode === 'mini' ? 'full' : 'mini');
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [mode]);
return [mode, setMode] as const;
};

View File

@ -0,0 +1,22 @@
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { getCondensedRoutes, getRoutes } from 'component/menu/routes';
import { useAdminRoutes } from 'component/admin/useAdminRoutes';
import { filterByConfig, mapRouteLink } from 'component/common/util';
export const useRoutes = () => {
const { uiConfig } = 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,
};
return { routes: filteredMainRoutes };
};