/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. * * Any modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import './collapsible_nav.scss'; import { EuiCollapsibleNav, EuiCollapsibleNavGroup, EuiFlexItem, EuiHorizontalRule, EuiListGroup, EuiListGroupItem, EuiShowFor, EuiText, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { groupBy, sortBy } from 'lodash'; import React, { Fragment, useRef } from 'react'; import useObservable from 'react-use/lib/useObservable'; import * as Rx from 'rxjs'; import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../..'; import { AppCategory } from '../../../../types'; import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { OnIsLockedUpdate } from './'; import { createEuiListItem, createRecentNavLink, isModifiedOrPrevented } from './nav_link'; import { ChromeBranding } from '../../chrome_service'; function getAllCategories(allCategorizedLinks: Record) { const allCategories = {} as Record; for (const [key, value] of Object.entries(allCategorizedLinks)) { allCategories[key] = value[0].category; } return allCategories; } function getOrderedCategories( mainCategories: Record, categoryDictionary: ReturnType ) { return sortBy( Object.keys(mainCategories), (categoryName) => categoryDictionary[categoryName]?.order ); } function getCategoryLocalStorageKey(id: string) { return `core.navGroup.${id}`; } function getIsCategoryOpen(id: string, storage: Storage) { const value = storage.getItem(getCategoryLocalStorageKey(id)) ?? 'true'; return value === 'true'; } function setIsCategoryOpen(id: string, isOpen: boolean, storage: Storage) { storage.setItem(getCategoryLocalStorageKey(id), `${isOpen}`); } interface Props { appId$: InternalApplicationStart['currentAppId$']; basePath: HttpStart['basePath']; id: string; isLocked: boolean; isNavOpen: boolean; homeHref: string; navLinks$: Rx.Observable; recentlyAccessed$: Rx.Observable; storage?: Storage; onIsLockedUpdate: OnIsLockedUpdate; closeNav: () => void; navigateToApp: InternalApplicationStart['navigateToApp']; navigateToUrl: InternalApplicationStart['navigateToUrl']; customNavLink$: Rx.Observable; branding: ChromeBranding; } 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(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 ( {customNavLink && ( )} {/* Recently viewed */} setIsCategoryOpen('recentlyViewed', isCategoryOpen, storage)} data-test-subj="collapsibleNavGroup-recentlyViewed" > {recentlyAccessed.length > 0 ? ( { // 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" /> ) : (

{i18n.translate('core.ui.EmptyRecentlyViewed', { defaultMessage: 'No recently viewed items', })}

)}
{/* OpenSearchDashboards, Observability, Security, and Management sections */} {orderedCategories.map((categoryName) => { const category = categoryDictionary[categoryName]!; const opensearchLinkLogo = category.id === 'opensearchDashboards' ? customSideMenuLogo() : category.euiIconType; return ( setIsCategoryOpen(category.id, isCategoryOpen, storage)} data-test-subj={`collapsibleNavGroup-${category.id}`} data-test-opensearch-logo={opensearchLinkLogo} > readyForEUI(link))} maxWidth="none" color="subdued" gutterSize="none" size="s" /> ); })} {/* Things with no category (largely for custom plugins) */} {unknowns.map((link, i) => ( ))} {/* Docking button only for larger screens that can support it*/} { onIsLockedUpdate(!isLocked); if (lockRef.current) { lockRef.current.focus(); } }} iconType={isLocked ? 'lock' : 'lockOpen'} />
); }