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 = () => ( ); 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 ( ({ minWidth: theme.spacing(4) })}> {children} {badge} ); }; const ExternalFullListItem: FC<{ href: string; text: string }> = ({ href, text, children, }) => { return ( ({ minWidth: theme.spacing(4) })}> {children} ); }; const SignOutItem = () => { return (
({ minWidth: theme.spacing(4) })} >
); }; const MiniListItem: FC<{ href: string; text: string }> = ({ href, text, children, }) => { return ( ({ minWidth: theme.spacing(4) })} > {children} ); }; export const StyledBox = styled(Box)(({ theme }) => ({ backgroundColor: theme.palette.background.paper, padding: theme.spacing(2, 1, 6, 1), })); const icons: Record = { '/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 ; }; 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 ( {mode === 'full' && ( ({ color: theme.palette.neutral.main, fontSize: 'small', })} > Hide (⌘ + B) )} {mode === 'full' ? ( ) : ( )} ); }; 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( '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 ( ); }; const ConfigureNavigationList: FC<{ routes: INavigationMenuItem[]; mode: NavigationMode; onClick?: () => void; }> = ({ routes, mode, onClick }) => { const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem; return ( {routes.map((route) => ( ))} ); }; 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 ( {routes.map((route) => ( ) : null } > ))} ); }; const OtherLinksList = () => { const { uiConfig } = useUiConfig(); return ( {uiConfig.links.map((link) => ( ))} ); }; export const MobileNavigationSidebar: FC<{ onClick: () => void }> = ({ onClick, }) => { const { routes } = useRoutes(); return ( <> ); }; const useExpanded = () => { const [expanded, setExpanded] = useLocalStorageState>( '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 NavigationSidebar = () => { const { routes } = useRoutes(); const [mode, setMode] = useNavigationMode(); const [expanded, changeExpanded] = useExpanded<'configure' | 'admin'>(); return ( { changeExpanded('configure', expand); }} > {mode === 'full' && ( } aria-controls='configure-content' id='configure-header' > Configure )} { changeExpanded('admin', expand); }} > {mode === 'full' && ( } aria-controls='admin-content' id='admin-header' > Admin )} { setMode(mode === 'full' ? 'mini' : 'full'); }} /> ); };