mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-23 00:16:25 +01:00
refactor: navigation sidebar (#7171)
This commit is contained in:
parent
c8db321b3e
commit
08629e9041
frontend/src/component/layout/MainLayout/NavigationSidebar
@ -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),
|
||||||
|
}));
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export type NavigationMode = 'mini' | 'full';
|
@ -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 {
|
import {
|
||||||
Box,
|
AdminNavigationList,
|
||||||
IconButton,
|
ConfigureNavigationList,
|
||||||
List,
|
OtherLinksList,
|
||||||
ListItem,
|
PrimaryNavigationList,
|
||||||
ListItemButton,
|
SecondaryNavigation,
|
||||||
ListItemIcon,
|
} from './NavigationList';
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MobileNavigationSidebar: FC<{ onClick: () => void }> = ({
|
export const MobileNavigationSidebar: FC<{ onClick: () => void }> = ({
|
||||||
onClick,
|
onClick,
|
||||||
@ -430,7 +19,7 @@ export const MobileNavigationSidebar: FC<{ onClick: () => void }> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MainNavigationList mode='full' onClick={onClick} />
|
<PrimaryNavigationList mode='full' onClick={onClick} />
|
||||||
<ConfigureNavigationList
|
<ConfigureNavigationList
|
||||||
routes={routes.mainNavRoutes}
|
routes={routes.mainNavRoutes}
|
||||||
mode='full'
|
mode='full'
|
||||||
@ -446,22 +35,10 @@ export const MobileNavigationSidebar: FC<{ onClick: () => void }> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const useExpanded = <T extends string>() => {
|
export const StyledBox = styled(Box)(({ theme }) => ({
|
||||||
const [expanded, setExpanded] = useLocalStorageState<Array<T>>(
|
backgroundColor: theme.palette.background.paper,
|
||||||
'navigation-expanded:v1',
|
padding: theme.spacing(2, 1, 6, 1),
|
||||||
[],
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
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 NavigationSidebar = () => {
|
export const NavigationSidebar = () => {
|
||||||
const { routes } = useRoutes();
|
const { routes } = useRoutes();
|
||||||
@ -471,64 +48,27 @@ export const NavigationSidebar = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledBox>
|
<StyledBox>
|
||||||
<MainNavigationList mode={mode} />
|
<PrimaryNavigationList mode={mode} />
|
||||||
<Accordion
|
<SecondaryNavigation
|
||||||
disableGutters={true}
|
|
||||||
sx={{ boxShadow: 'none' }}
|
|
||||||
expanded={expanded.includes('configure')}
|
expanded={expanded.includes('configure')}
|
||||||
onChange={(_, expand) => {
|
onChange={(expand) => {
|
||||||
changeExpanded('configure', expand);
|
changeExpanded('configure', expand);
|
||||||
}}
|
}}
|
||||||
>
|
mode={mode}
|
||||||
{mode === 'full' && (
|
routes={routes.mainNavRoutes}
|
||||||
<AccordionSummary
|
|
||||||
expandIcon={<ExpandMoreIcon />}
|
|
||||||
aria-controls='configure-content'
|
|
||||||
id='configure-header'
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
sx={{ fontWeight: 'bold', fontSize: 'small' }}
|
|
||||||
>
|
>
|
||||||
Configure
|
Configure
|
||||||
</Typography>
|
</SecondaryNavigation>
|
||||||
</AccordionSummary>
|
<SecondaryNavigation
|
||||||
)}
|
|
||||||
<AccordionDetails sx={{ p: 0 }}>
|
|
||||||
<ConfigureNavigationList
|
|
||||||
routes={routes.mainNavRoutes}
|
|
||||||
mode={mode}
|
|
||||||
/>
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
<Accordion
|
|
||||||
disableGutters={true}
|
|
||||||
sx={{ boxShadow: 'none' }}
|
|
||||||
expanded={expanded.includes('admin')}
|
expanded={expanded.includes('admin')}
|
||||||
onChange={(_, expand) => {
|
onChange={(expand) => {
|
||||||
changeExpanded('admin', expand);
|
changeExpanded('admin', expand);
|
||||||
}}
|
}}
|
||||||
>
|
mode={mode}
|
||||||
{mode === 'full' && (
|
routes={routes.adminRoutes}
|
||||||
<AccordionSummary
|
|
||||||
expandIcon={<ExpandMoreIcon />}
|
|
||||||
aria-controls='admin-content'
|
|
||||||
id='admin-header'
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
sx={{ fontWeight: 'bold', fontSize: 'small' }}
|
|
||||||
>
|
>
|
||||||
Admin
|
Admin
|
||||||
</Typography>
|
</SecondaryNavigation>
|
||||||
</AccordionSummary>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<AccordionDetails sx={{ p: 0 }}>
|
|
||||||
<AdminNavigationList
|
|
||||||
routes={routes.adminRoutes}
|
|
||||||
mode={mode}
|
|
||||||
/>
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
<ShowHide
|
<ShowHide
|
||||||
mode={mode}
|
mode={mode}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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;
|
||||||
|
};
|
@ -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;
|
||||||
|
};
|
@ -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 };
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user