in src/core/public/chrome/ui/header/collapsible_nav.tsx [109:394]
export function CollapsibleNav({
basePath,
id,
isLocked,
isNavOpen,
homeHref,
storage = window.localStorage,
onIsLockedUpdate,
closeNav,
navigateToApp,
navigateToUrl,
branding,
...observables
}: Props) {
const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden);
const recentlyAccessed = useObservable(observables.recentlyAccessed$, []);
const customNavLink = useObservable(observables.customNavLink$, undefined);
const appId = useObservable(observables.appId$, '');
const lockRef = useRef<HTMLButtonElement>(null);
const groupedNavLinks = groupBy(navLinks, (link) => link?.category?.id);
const { undefined: unknowns = [], ...allCategorizedLinks } = groupedNavLinks;
const categoryDictionary = getAllCategories(allCategorizedLinks);
const orderedCategories = getOrderedCategories(allCategorizedLinks, categoryDictionary);
const readyForEUI = (link: ChromeNavLink, needsIcon: boolean = false) => {
return createEuiListItem({
link,
appId,
dataTestSubj: 'collapsibleNavAppLink',
navigateToApp,
onClick: closeNav,
...(needsIcon && { basePath }),
});
};
const DEFAULT_OPENSEARCH_MARK = `${branding.assetFolderUrl}/opensearch_mark_default_mode.svg`;
const DARKMODE_OPENSEARCH_MARK = `${branding.assetFolderUrl}/opensearch_mark_dark_mode.svg`;
const darkMode = branding.darkMode;
const markDefault = branding.mark?.defaultUrl;
const markDarkMode = branding.mark?.darkModeUrl;
/**
* Use branding configurations to check which URL to use for rendering
* side menu opensearch logo in default mode
*
* @returns a valid custom URL or original default mode opensearch mark if no valid URL is provided
*/
const customSideMenuLogoDefaultMode = () => {
return markDefault ?? DEFAULT_OPENSEARCH_MARK;
};
/**
* Use branding configurations to check which URL to use for rendering
* side menu opensearch logo in dark mode
*
* @returns a valid custom URL or original dark mode opensearch mark if no valid URL is provided
*/
const customSideMenuLogoDarkMode = () => {
return markDarkMode ?? markDefault ?? DARKMODE_OPENSEARCH_MARK;
};
/**
* Render custom side menu logo for both default mode and dark mode
*
* @returns a valid logo URL
*/
const customSideMenuLogo = () => {
return darkMode ? customSideMenuLogoDarkMode() : customSideMenuLogoDefaultMode();
};
return (
<EuiCollapsibleNav
data-test-subj="collapsibleNav"
id={id}
aria-label={i18n.translate('core.ui.primaryNav.screenReaderLabel', {
defaultMessage: 'Primary',
})}
isOpen={isNavOpen}
isDocked={isLocked}
onClose={closeNav}
>
{customNavLink && (
<Fragment>
<EuiFlexItem grow={false} style={{ flexShrink: 0 }}>
<EuiCollapsibleNavGroup
background="light"
className="eui-yScroll"
style={{ maxHeight: '40vh' }}
>
<EuiListGroup
listItems={[
createEuiListItem({
link: customNavLink,
basePath,
navigateToApp,
dataTestSubj: 'collapsibleNavCustomNavLink',
onClick: closeNav,
externalLink: true,
}),
]}
maxWidth="none"
color="text"
gutterSize="none"
size="s"
/>
</EuiCollapsibleNavGroup>
</EuiFlexItem>
<EuiHorizontalRule margin="none" />
</Fragment>
)}
{/* Pinned items */}
<EuiFlexItem grow={false} style={{ flexShrink: 0 }}>
<EuiCollapsibleNavGroup
background="light"
className="eui-yScroll"
style={{ maxHeight: '40vh' }}
>
<EuiListGroup
aria-label={i18n.translate('core.ui.primaryNav.pinnedLinksAriaLabel', {
defaultMessage: 'Pinned links',
})}
listItems={[
{
label: 'Home',
iconType: 'home',
href: homeHref,
onClick: (event) => {
if (isModifiedOrPrevented(event)) {
return;
}
event.preventDefault();
closeNav();
navigateToApp('home');
},
},
]}
maxWidth="none"
color="text"
gutterSize="none"
size="s"
/>
</EuiCollapsibleNavGroup>
</EuiFlexItem>
{/* Recently viewed */}
<EuiCollapsibleNavGroup
key="recentlyViewed"
background="light"
title={i18n.translate('core.ui.recentlyViewed', { defaultMessage: 'Recently viewed' })}
isCollapsible={true}
initialIsOpen={getIsCategoryOpen('recentlyViewed', storage)}
onToggle={(isCategoryOpen) => setIsCategoryOpen('recentlyViewed', isCategoryOpen, storage)}
data-test-subj="collapsibleNavGroup-recentlyViewed"
>
{recentlyAccessed.length > 0 ? (
<EuiListGroup
aria-label={i18n.translate('core.ui.recentlyViewedAriaLabel', {
defaultMessage: 'Recently viewed links',
})}
listItems={recentlyAccessed.map((link) => {
// TODO #64541
// Can remove icon from recent links completely
const { iconType, onClick, ...hydratedLink } = createRecentNavLink(
link,
navLinks,
basePath,
navigateToUrl
);
return {
...hydratedLink,
'data-test-subj': 'collapsibleNavAppLink--recent',
onClick: (event) => {
if (!isModifiedOrPrevented(event)) {
closeNav();
onClick(event);
}
},
};
})}
maxWidth="none"
color="subdued"
gutterSize="none"
size="s"
className="osdCollapsibleNav__recentsListGroup"
/>
) : (
<EuiText size="s" color="subdued" style={{ padding: '0 8px 8px' }}>
<p>
{i18n.translate('core.ui.EmptyRecentlyViewed', {
defaultMessage: 'No recently viewed items',
})}
</p>
</EuiText>
)}
</EuiCollapsibleNavGroup>
<EuiHorizontalRule margin="none" />
<EuiFlexItem className="eui-yScroll">
{/* OpenSearchDashboards, Observability, Security, and Management sections */}
{orderedCategories.map((categoryName) => {
const category = categoryDictionary[categoryName]!;
const opensearchLinkLogo =
category.id === 'opensearchDashboards' ? customSideMenuLogo() : category.euiIconType;
return (
<EuiCollapsibleNavGroup
key={category.id}
iconType={opensearchLinkLogo}
title={category.label}
isCollapsible={true}
initialIsOpen={getIsCategoryOpen(category.id, storage)}
onToggle={(isCategoryOpen) => setIsCategoryOpen(category.id, isCategoryOpen, storage)}
data-test-subj={`collapsibleNavGroup-${category.id}`}
data-test-opensearch-logo={opensearchLinkLogo}
>
<EuiListGroup
aria-label={i18n.translate('core.ui.primaryNavSection.screenReaderLabel', {
defaultMessage: 'Primary navigation links, {category}',
values: { category: category.label },
})}
listItems={allCategorizedLinks[categoryName].map((link) => readyForEUI(link))}
maxWidth="none"
color="subdued"
gutterSize="none"
size="s"
/>
</EuiCollapsibleNavGroup>
);
})}
{/* Things with no category (largely for custom plugins) */}
{unknowns.map((link, i) => (
<EuiCollapsibleNavGroup data-test-subj={`collapsibleNavGroup-noCategory`} key={i}>
<EuiListGroup flush>
<EuiListGroupItem color="text" size="s" {...readyForEUI(link, true)} />
</EuiListGroup>
</EuiCollapsibleNavGroup>
))}
{/* Docking button only for larger screens that can support it*/}
<EuiShowFor sizes={['l', 'xl']}>
<EuiCollapsibleNavGroup>
<EuiListGroup flush>
<EuiListGroupItem
data-test-subj="collapsible-nav-lock"
buttonRef={lockRef}
size="xs"
color="subdued"
label={
isLocked
? i18n.translate('core.ui.primaryNavSection.undockLabel', {
defaultMessage: 'Undock navigation',
})
: i18n.translate('core.ui.primaryNavSection.dockLabel', {
defaultMessage: 'Dock navigation',
})
}
aria-label={
isLocked
? i18n.translate('core.ui.primaryNavSection.undockAriaLabel', {
defaultMessage: 'Undock primary navigation',
})
: i18n.translate('core.ui.primaryNavSection.dockAriaLabel', {
defaultMessage: 'Dock primary navigation',
})
}
onClick={() => {
onIsLockedUpdate(!isLocked);
if (lockRef.current) {
lockRef.current.focus();
}
}}
iconType={isLocked ? 'lock' : 'lockOpen'}
/>
</EuiListGroup>
</EuiCollapsibleNavGroup>
</EuiShowFor>
</EuiFlexItem>
</EuiCollapsibleNav>
);
}