From 405cb6b1cc1120b725435b11936eeadd4914022c Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Mon, 1 Jul 2024 17:51:00 +0800 Subject: [PATCH 1/9] Add current nav group into chrome service Signed-off-by: Hailong Cui breadcrumbs Signed-off-by: Hailong Cui --- changelogs/fragments/7130.yml | 2 + src/core/public/chrome/chrome_service.mock.ts | 8 +- src/core/public/chrome/chrome_service.tsx | 9 +- .../nav_group/nav_group_service.test.ts | 9 +- .../chrome/nav_group/nav_group_service.ts | 106 +- .../header/__snapshots__/header.test.tsx.snap | 24 + .../header/collapsible_nav_group_enabled.tsx | 427 + .../public/chrome/ui/header/header.test.tsx | 5 + src/core/public/chrome/ui/header/header.tsx | 12 + .../ui/header/header_breadcrumbs.test.tsx | 8 +- .../chrome/ui/header/header_breadcrumbs.tsx | 17 +- .../dashboard_listing.test.tsx.snap | 8218 ++++++++++++++++- .../dashboard_top_nav.test.tsx.snap | 18 + .../home/public/application/application.tsx | 1 + src/plugins/workspace/public/plugin.ts | 4 +- 15 files changed, 8851 insertions(+), 17 deletions(-) create mode 100644 changelogs/fragments/7130.yml create mode 100644 src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx diff --git a/changelogs/fragments/7130.yml b/changelogs/fragments/7130.yml new file mode 100644 index 00000000000..0e39759fa65 --- /dev/null +++ b/changelogs/fragments/7130.yml @@ -0,0 +1,2 @@ +feat: +- 1/ Add current nav group into chrome service 2/ prepend current nav group into breadcrumbs ([#7130](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7130)) \ No newline at end of file diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index d81ec2340ff..68ed367776e 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -37,9 +37,12 @@ const createSetupContractMock = () => { return { registerCollapsibleNavHeader: jest.fn(), navGroup: { - addNavLinksToGroup: jest.fn(), getNavGroupEnabled: jest.fn(), registerNavGroupUpdater: jest.fn(), + addNavLinksToGroup: jest.fn(), + prependNavgroupToBreadcrumbs: jest.fn(), + getCurrentNavGroup$: jest.fn(() => new BehaviorSubject(undefined)), + setCurrentNavGroup: jest.fn(), }, }; }; @@ -78,6 +81,9 @@ const createStartContractMock = () => { navGroup: { getNavGroupsMap$: jest.fn(() => new BehaviorSubject({})), getNavGroupEnabled: jest.fn(), + prependCurrentNavgroupToBreadcrumbs: jest.fn(), + getCurrentNavGroup$: jest.fn(() => new BehaviorSubject(undefined)), + setCurrentNavGroup: jest.fn(), }, setAppTitle: jest.fn(), setIsVisible: jest.fn(), diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 822b4c23822..91d1c5fc263 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -109,7 +109,7 @@ export interface StartDeps { overlays: OverlayStart; } -type CollapsibleNavHeaderRender = () => JSX.Element | null; +export type CollapsibleNavHeaderRender = () => JSX.Element | null; /** @internal */ export class ChromeService { @@ -198,7 +198,7 @@ export class ChromeService { const navLinks = this.navLinks.start({ application, http }); const recentlyAccessed = await this.recentlyAccessed.start({ http }); const docTitle = this.docTitle.start({ document: window.document }); - const navGroup = await this.navGroup.start({ navLinks }); + const navGroup = await this.navGroup.start({ navLinks, application }); // erase chrome fields from a previous app while switching to a next app application.currentAppId$.subscribe(() => { @@ -299,6 +299,11 @@ export class ChromeService { survey={injectedMetadata.getSurvey()} collapsibleNavHeaderRender={this.collapsibleNavHeaderRender} sidecarConfig$={sidecarConfig$} + navGroupsMap$={navGroup.getNavGroupsMap$()} + navGroupEnabled={navGroup.getNavGroupEnabled()} + onNavGroupSelected={navGroup.setCurrentNavGroup} + currentNavgroup$={navGroup.getCurrentNavGroup$()} + prependCurrentNavgroupToBreadcrumbs={navGroup.prependCurrentNavgroupToBreadcrumbs} /> ), diff --git a/src/core/public/chrome/nav_group/nav_group_service.test.ts b/src/core/public/chrome/nav_group/nav_group_service.test.ts index 64997b4e3bd..ab4d24c990c 100644 --- a/src/core/public/chrome/nav_group/nav_group_service.test.ts +++ b/src/core/public/chrome/nav_group/nav_group_service.test.ts @@ -94,6 +94,7 @@ describe('ChromeNavGroupService#setup()', () => { chromeNavGroupServiceSetup.addNavLinksToGroup(mockedGroupBar, [mockedGroupBar]); const chromeNavGroupServiceStart = await chromeNavGroupService.start({ navLinks: mockedNavLinkService, + application: mockedApplicationService, }); const groupsMap = await chromeNavGroupServiceStart.getNavGroupsMap$().pipe(first()).toPromise(); expect(groupsMap[mockedGroupFoo.id].navLinks.length).toEqual(2); @@ -116,6 +117,7 @@ describe('ChromeNavGroupService#setup()', () => { chromeNavGroupServiceSetup.addNavLinksToGroup(mockedGroupBar, [mockedGroupBar]); const chromeNavGroupServiceStart = await chromeNavGroupService.start({ navLinks: mockedNavLinkService, + application: mockedApplicationService, }); const groupsMap = await chromeNavGroupServiceStart.getNavGroupsMap$().pipe(first()).toPromise(); expect(groupsMap[mockedGroupFoo.id].navLinks.length).toEqual(1); @@ -172,7 +174,10 @@ describe('ChromeNavGroupService#start()', () => { ]); chromeNavGroupServiceSetup.addNavLinksToGroup(mockedGroupBar, [mockedNavLinkBar]); - const chromeStart = await chromeNavGroupService.start({ navLinks: mockedNavLinkService }); + const chromeStart = await chromeNavGroupService.start({ + navLinks: mockedNavLinkService, + application: mockedApplicationService, + }); const groupsMap = await chromeStart.getNavGroupsMap$().pipe(first()).toPromise(); @@ -196,6 +201,7 @@ describe('ChromeNavGroupService#start()', () => { chromeNavGroupService.setup({ uiSettings }); const chromeNavGroupServiceStart = await chromeNavGroupService.start({ navLinks: mockedNavLinkService, + application: mockedApplicationService, }); expect(chromeNavGroupServiceStart.getNavGroupEnabled()).toBe(true); @@ -210,6 +216,7 @@ describe('ChromeNavGroupService#start()', () => { chromeNavGroupService.setup({ uiSettings }); const chromeNavGroupServiceStart = await chromeNavGroupService.start({ navLinks: mockedNavLinkService, + application: mockedApplicationService, }); navGroupEnabled$.next(false); diff --git a/src/core/public/chrome/nav_group/nav_group_service.ts b/src/core/public/chrome/nav_group/nav_group_service.ts index fa52b726219..f13c5f433c1 100644 --- a/src/core/public/chrome/nav_group/nav_group_service.ts +++ b/src/core/public/chrome/nav_group/nav_group_service.ts @@ -4,8 +4,15 @@ */ import { BehaviorSubject, combineLatest, Observable, of, ReplaySubject, Subscription } from 'rxjs'; -import { AppCategory, ChromeNavGroup, ChromeNavLink } from 'opensearch-dashboards/public'; +import { + AppCategory, + ChromeBreadcrumb, + ChromeNavGroup, + ChromeNavLink, +} from 'opensearch-dashboards/public'; import { map, switchMap, takeUntil } from 'rxjs/operators'; +import { i18n } from '@osd/i18n'; +import _ from 'lodash'; import { IUiSettingsClient } from '../../ui_settings'; import { flattenLinksOrCategories, @@ -13,6 +20,9 @@ import { getOrderedLinksOrCategories, } from '../utils'; import { ChromeNavLinks } from '../nav_links'; +import { InternalApplicationStart } from '../../application'; + +const CURRENT_NAV_GROUP_ID = 'core.chrome.currentNavGroupId'; /** @public */ export interface ChromeRegistrationNavLink { @@ -45,6 +55,23 @@ export interface ChromeNavGroupServiceSetupContract { export interface ChromeNavGroupServiceStartContract { getNavGroupsMap$: () => Observable>; getNavGroupEnabled: ChromeNavGroupServiceSetupContract['getNavGroupEnabled']; + /** + * Get an observable of the current selected nav group + */ + getCurrentNavGroup$: () => Observable; + + /** + * Set current selected nav group + * @param navGroupId The id of the nav group to be set as current + */ + setCurrentNavGroup: (navGroupId: string | undefined) => void; + + /** + * prepend Home & NavGroup into current breadcrumbs + * @param breadcrumbs current breadcrumbs + * @returns new prepend breadcrumbs + */ + prependCurrentNavgroupToBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[]) => ChromeBreadcrumb[]; } /** @internal */ @@ -55,6 +82,8 @@ export class ChromeNavGroupService { private navGroupEnabled: boolean = false; private navGroupEnabledUiSettingsSubscription: Subscription | undefined; private navGroupUpdaters$$ = new BehaviorSubject>>([]); + private currentNavGroup$ = new BehaviorSubject(undefined); + private addNavLinkToGroup( currentGroupsMap: Record, navGroup: ChromeNavGroup, @@ -81,6 +110,18 @@ export class ChromeNavGroupService { return currentGroupsMap; } + + private sortNavGroupNavLinks( + navGroup: NavGroupItemInMap, + allVaildNavLinks: Array> + ) { + return flattenLinksOrCategories( + getOrderedLinksOrCategories( + fulfillRegistrationLinksToChromeNavLinks(navGroup.navLinks, allVaildNavLinks) + ) + ); + } + private getSortedNavGroupsMap$() { return combineLatest([this.getUpdatedNavGroupsMap$(), this.navLinks$]) .pipe(takeUntil(this.stop$)) @@ -88,12 +129,9 @@ export class ChromeNavGroupService { map(([navGroupsMap, navLinks]) => { return Object.keys(navGroupsMap).reduce((sortedNavGroupsMap, navGroupId) => { const navGroup = navGroupsMap[navGroupId]; - const sortedNavLinks = getOrderedLinksOrCategories( - fulfillRegistrationLinksToChromeNavLinks(navGroup.navLinks, navLinks) - ); sortedNavGroupsMap[navGroupId] = { ...navGroup, - navLinks: flattenLinksOrCategories(sortedNavLinks), + navLinks: this.sortNavGroupNavLinks(navGroup, navLinks), }; return sortedNavGroupsMap; }, {} as Record); @@ -159,15 +197,73 @@ export class ChromeNavGroupService { } async start({ navLinks, + application, }: { navLinks: ChromeNavLinks; + application: InternalApplicationStart; }): Promise { this.navLinks$ = navLinks.getNavLinks$(); + + const currentNavGroupId = sessionStorage.getItem(CURRENT_NAV_GROUP_ID); + this.currentNavGroup$ = new BehaviorSubject( + currentNavGroupId ? this.navGroupsMap$.getValue()[currentNavGroupId] : undefined + ); + return { getNavGroupsMap$: () => this.getSortedNavGroupsMap$(), getNavGroupEnabled: () => this.navGroupEnabled, + + getCurrentNavGroup$: () => this.currentNavGroup$, + setCurrentNavGroup: (navGroupId: string | undefined) => { + const navGroup = navGroupId ? this.navGroupsMap$.getValue()[navGroupId] : undefined; + if (navGroup) { + this.currentNavGroup$.next(navGroup); + sessionStorage.setItem(CURRENT_NAV_GROUP_ID, navGroup.id); + } else { + this.currentNavGroup$.next(undefined); + sessionStorage.removeItem(CURRENT_NAV_GROUP_ID); + } + }, + + prependCurrentNavgroupToBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[]) => { + const navGroupId = this.currentNavGroup$.getValue()?.id; + const homeTitle = i18n.translate('core.breadcrumbs.homeTitle', { defaultMessage: 'Home' }); + // home page will not have nav group information + const isHome = breadcrumbs && breadcrumbs.length === 1 && breadcrumbs[0].text === homeTitle; + + if (this.navGroupEnabled && navGroupId && !isHome) { + const currentNavGroup = this.navGroupsMap$.getValue()[navGroupId]; + + // breadcrumb order is home > navgroup > application, navgroup will be second one + const navGroupInBreadcrumbs = + breadcrumbs.length > 1 && breadcrumbs[1]?.text === currentNavGroup.title; + if (!navGroupInBreadcrumbs) { + const navgroupBreadcrumb: ChromeBreadcrumb = { + text: currentNavGroup.title, + onClick: () => { + if (currentNavGroup.navLinks) { + const orderedLinks = this.sortNavGroupNavLinks( + currentNavGroup, + navLinks.getAll() + ); + application.navigateToApp(orderedLinks[0].id); + } + }, + }; + const homeBreadcrumb: ChromeBreadcrumb = { + text: homeTitle, + onClick: () => { + application.navigateToApp('home'); + }, + }; + return [homeBreadcrumb, navgroupBreadcrumb, ...breadcrumbs]; + } + } + return breadcrumbs; + }, }; } + async stop() { this.stop$.next(); this.navGroupEnabledUiSettingsSubscription?.unsubscribe(); diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 790f24bc20e..741edc68db1 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -304,6 +304,17 @@ exports[`Header handles visibility and lock changes 1`] = ` "thrownError": null, } } + currentNavgroup$={ + BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + } + } customNavLink$={ BehaviorSubject { "_isScalar": false, @@ -1870,6 +1881,7 @@ exports[`Header handles visibility and lock changes 1`] = ` } } onIsLockedUpdate={[Function]} + onNavGroupSelected={[Function]} opensearchDashboardsDocLink="/docs" opensearchDashboardsVersion="1.0.0" recentlyAccessed$={ @@ -7072,6 +7084,17 @@ exports[`Header renders condensed header 1`] = ` "thrownError": null, } } + currentNavgroup$={ + BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + } + } customNavLink$={ BehaviorSubject { "_isScalar": false, @@ -8476,6 +8499,7 @@ exports[`Header renders condensed header 1`] = ` } } onIsLockedUpdate={[Function]} + onNavGroupSelected={[Function]} opensearchDashboardsDocLink="/docs" opensearchDashboardsVersion="1.0.0" recentlyAccessed$={ diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx new file mode 100644 index 00000000000..d6a69a3ca35 --- /dev/null +++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx @@ -0,0 +1,427 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import './collapsible_nav_group_enabled.scss'; +import { + EuiCollapsibleNavGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiListGroup, + EuiListGroupItem, + EuiShowFor, + EuiFlyout, + EuiButtonIcon, + EuiFlexGroup, + EuiSideNavItemType, + EuiSideNav, +} from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { sortBy } from 'lodash'; +import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import * as Rx from 'rxjs'; +import { ChromeNavLink } from '../..'; +import { ChromeNavGroup, NavGroupType } from '../../../../types'; +import { InternalApplicationStart } from '../../../application/types'; +import { HttpStart } from '../../../http'; +import { HeaderProps, OnIsLockedUpdate } from './'; +import { createEuiListItem } from './nav_link'; +import type { Logos } from '../../../../common/types'; +import { CollapsibleNavHeaderRender } from '../../chrome_service'; +import { NavGroupItemInMap } from '../../nav_group'; +import { + fulfillRegistrationLinksToChromeNavLinks, + getOrderedLinksOrCategories, + LinkItem, + LinkItemType, +} from '../../utils'; + +interface Props { + appId$: InternalApplicationStart['currentAppId$']; + basePath: HttpStart['basePath']; + collapsibleNavHeaderRender?: CollapsibleNavHeaderRender; + id: string; + isLocked: boolean; + isNavOpen: boolean; + navLinks$: Rx.Observable; + storage?: Storage; + onIsLockedUpdate: OnIsLockedUpdate; + closeNav: () => void; + navigateToApp: InternalApplicationStart['navigateToApp']; + navigateToUrl: InternalApplicationStart['navigateToUrl']; + customNavLink$: Rx.Observable; + logos: Logos; + navGroupsMap$: Rx.Observable>; + onNavGroupSelected: HeaderProps['onNavGroupSelected']; + currentNavgroup$: Rx.Observable; +} + +interface NavGroupsProps { + navLinks: ChromeNavLink[]; + suffix?: React.ReactElement; + style?: React.CSSProperties; + appId?: string; + navigateToApp: InternalApplicationStart['navigateToApp']; + onClick: () => void; +} + +function NavGroups({ navLinks, suffix, style, appId, navigateToApp, onClick }: NavGroupsProps) { + const createNavItem = ({ link }: { link: ChromeNavLink }) => { + const euiListItem = createEuiListItem({ + link, + appId, + dataTestSubj: 'collapsibleNavAppLink', + navigateToApp, + onClick, + }); + + return { + id: link.id, + name:
{link.title}
, + onClick: euiListItem.onClick, + href: euiListItem.href, + className: 'no-margin-top', + isSelected: euiListItem.isActive, + }; + }; + const orderedLinksOrCategories = getOrderedLinksOrCategories(navLinks); + const createSideNavItem = (navLink: LinkItem): EuiSideNavItemType<{}> => { + if (navLink.itemType === LinkItemType.LINK) { + return createNavItem({ + link: navLink.link, + }); + } + + if (navLink.itemType === LinkItemType.PARENT_LINK && navLink.link) { + return { + ...createNavItem({ link: navLink.link }), + items: navLink.links.map((subNavLink) => createSideNavItem(subNavLink)), + }; + } + + if (navLink.itemType === LinkItemType.CATEGORY) { + return { + id: navLink.category?.id ?? '', + name:
{navLink.category?.label ?? ''}
, + items: navLink.links?.map((link) => createSideNavItem(link)), + }; + } + + return {} as EuiSideNavItemType<{}>; + }; + const sideNavItems = orderedLinksOrCategories + .map((navLink) => createSideNavItem(navLink)) + .filter((item): item is EuiSideNavItemType<{}> => !!item); + return ( + + + {suffix} + + ); +} + +export function CollapsibleNavGroupEnabled({ + basePath, + collapsibleNavHeaderRender, + id, + isLocked, + isNavOpen, + storage = window.localStorage, + onIsLockedUpdate, + closeNav, + navigateToApp, + navigateToUrl, + logos, + onNavGroupSelected, + ...observables +}: Props) { + const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden); + const customNavLink = useObservable(observables.customNavLink$, undefined); + const appId = useObservable(observables.appId$, ''); + const navGroupsMap = useObservable(observables.navGroupsMap$, {}); + const lockRef = useRef(null); + const focusGroup = useObservable(observables.currentNavgroup$, undefined); + + const [shouldShrinkSecondNavigation, setShouldShrinkSecondNavigation] = useState(false); + + useEffect(() => { + if (!focusGroup && appId) { + const orderedGroups = sortBy(Object.values(navGroupsMap), (group) => group.order); + const findMatchedGroup = orderedGroups.find( + (group) => !!group.navLinks.find((navLink) => navLink.id === appId) + ); + onNavGroupSelected(findMatchedGroup?.id); + } + }, [appId, navGroupsMap, focusGroup, onNavGroupSelected]); + + const secondNavigation = focusGroup ? ( + <> + {shouldShrinkSecondNavigation ? ( +
+ setShouldShrinkSecondNavigation(false)} + /> +
+ ) : null} + {!shouldShrinkSecondNavigation && ( + <> +
+ + +

+ {focusGroup.title} +

+
+ + setShouldShrinkSecondNavigation(true)} + /> + +
+
+ + + )} + + ) : null; + + const secondNavigationWidth = useMemo(() => { + if (shouldShrinkSecondNavigation) { + return 48; + } + + return 320; + }, [shouldShrinkSecondNavigation]); + + const flyoutSize = useMemo(() => { + if (focusGroup) { + return 320 + secondNavigationWidth; + } + + return 320; + }, [focusGroup, secondNavigationWidth]); + + const onGroupClick = ( + e: React.MouseEvent, + group: ChromeNavGroup + ) => { + const fulfilledLinks = fulfillRegistrationLinksToChromeNavLinks( + navGroupsMap[group.id]?.navLinks, + navLinks + ); + onNavGroupSelected(group.id); + + // the `navGroupsMap[group.id]?.navLinks` has already been sorted + const firstLink = fulfilledLinks[0]; + if (firstLink) { + const propsForEui = createEuiListItem({ + link: firstLink, + appId, + dataTestSubj: 'collapsibleNavAppLink', + navigateToApp, + }); + propsForEui.onClick(e); + } + }; + + const allLinksWithNavGroup = Object.values(navGroupsMap).reduce( + (total, navGroup) => [...total, ...navGroup.navLinks.map((navLink) => navLink.id)], + [] as string[] + ); + + return ( + <> + {isNavOpen || isLocked ? ( + +
+
+ {customNavLink && ( + + + + + + + + + + )} + + !allLinksWithNavGroup.includes(link.id))} + suffix={ +
+ + + {sortBy( + Object.values(navGroupsMap).filter( + (item) => item.type === NavGroupType.SYSTEM + ), + (navGroup) => navGroup.order + ).map((group) => { + return ( + { + if (focusGroup?.id === group.id) { + onNavGroupSelected(undefined); + } else { + onGroupClick(e, group); + } + }} + /> + ); + })} + + + {collapsibleNavHeaderRender && collapsibleNavHeaderRender()} + + + {sortBy( + Object.values(navGroupsMap).filter((item) => !item.type), + (navGroup) => navGroup.order + ).map((group) => { + return ( + { + if (focusGroup?.id === group.id) { + onNavGroupSelected(undefined); + } else { + onGroupClick(e, group); + } + }} + /> + ); + })} + + + {/* Docking button only for larger screens that can support it*/} + + + + { + onIsLockedUpdate(!isLocked); + if (lockRef.current) { + lockRef.current.focus(); + } + }} + iconType={isLocked ? 'lock' : 'lockOpen'} + /> + + + +
+ } + navigateToApp={navigateToApp} + onClick={closeNav} + appId={appId} + /> +
+ {secondNavigation && ( +
+ {secondNavigation} +
+ )} +
+
+ ) : null} + {secondNavigation && !isLocked ? ( + {}} + size={secondNavigationWidth} + side="left" + hideCloseButton + > + {secondNavigation} + + ) : null} + + ); +} diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx index 0d3bc7e70d4..7b9890d0379 100644 --- a/src/core/public/chrome/ui/header/header.test.tsx +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -77,6 +77,11 @@ function mockProps() { dockedMode: SIDECAR_DOCKED_MODE.RIGHT, paddingSize: 640, }), + navGroupsMap$: new BehaviorSubject({}), + navGroupEnabled: false, + currentNavgroup$: new BehaviorSubject(undefined), + onNavGroupSelected: jest.fn(), + prependCurrentNavgroupToBreadcrumbs: jest.fn(), }; } diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index b8b40fa6c39..e4f3c7f1257 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -44,6 +44,7 @@ import classnames from 'classnames'; import React, { createRef, useMemo, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; +import { ChromeNavGroup } from 'opensearch-dashboards/public'; import { LoadingIndicator } from '../'; import { ChromeBadge, @@ -95,6 +96,11 @@ export interface HeaderProps { logos: Logos; survey: string | undefined; sidecarConfig$: Observable; + navGroupsMap$: Observable>; + navGroupEnabled: boolean; + onNavGroupSelected: (groupId: string | undefined) => void; + currentNavgroup$: Observable; + prependCurrentNavgroupToBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[]) => ChromeBreadcrumb[]; } export function Header({ @@ -108,6 +114,9 @@ export function Header({ survey, logos, collapsibleNavHeaderRender, + navGroupEnabled, + onNavGroupSelected, + prependCurrentNavgroupToBreadcrumbs, ...observables }: HeaderProps) { const isVisible = useObservable(observables.isVisible$, false); @@ -221,6 +230,9 @@ export function Header({ diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx index 2008a3f6c49..0b84baf0121 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx @@ -38,7 +38,13 @@ describe('HeaderBreadcrumbs', () => { it('renders updates to the breadcrumbs$ observable', () => { const breadcrumbs$ = new BehaviorSubject([{ text: 'First' }]); const wrapper = mount( - + ); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx index ca50b15d5af..250f9ce3701 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx @@ -33,14 +33,24 @@ import classNames from 'classnames'; import React from 'react'; import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; +import { ChromeNavGroup } from 'opensearch-dashboards/public'; import { ChromeBreadcrumb } from '../../chrome_service'; interface Props { appTitle$: Observable; breadcrumbs$: Observable; + navgroupEnabled: boolean; + currentNavgroup$: Observable; + prependCurrentNavgroupToBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[]) => ChromeBreadcrumb[]; } -export function HeaderBreadcrumbs({ appTitle$, breadcrumbs$ }: Props) { +export function HeaderBreadcrumbs({ + appTitle$, + breadcrumbs$, + navgroupEnabled, + currentNavgroup$, + prependCurrentNavgroupToBreadcrumbs, +}: Props) { const appTitle = useObservable(appTitle$, 'OpenSearch Dashboards'); const breadcrumbs = useObservable(breadcrumbs$, []); let crumbs = breadcrumbs; @@ -49,6 +59,11 @@ export function HeaderBreadcrumbs({ appTitle$, breadcrumbs$ }: Props) { crumbs = [{ text: appTitle }]; } + const currentNavgroup = useObservable(currentNavgroup$, undefined); + if (navgroupEnabled && currentNavgroup) { + crumbs = prependCurrentNavgroupToBreadcrumbs(crumbs); + } + crumbs = crumbs.map((breadcrumb, i) => ({ ...breadcrumb, 'data-test-subj': classNames( diff --git a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap index a4236dfb66e..ffa8c561454 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap @@ -220,8 +220,11 @@ exports[`dashboard listing hideWriteControls 1`] = ` "registerRight": [MockFunction], }, "navGroup": Object { + "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], + "prependNavgroupToBreadcrumbs": [MockFunction], + "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { "enableForcedAppSwitcherNavigation": [MockFunction], @@ -1164,11 +1167,1481 @@ exports[`dashboard listing hideWriteControls 1`] = ` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "", + "onChange": [Function], + "toolsLeft": undefined, + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +
@@ -1400,8 +2873,11 @@ exports[`dashboard listing render table listing with initial filters from URL 1` "registerRight": [MockFunction], }, "navGroup": Object { + "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], + "prependNavgroupToBreadcrumbs": [MockFunction], + "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { "enableForcedAppSwitcherNavigation": [MockFunction], @@ -2405,11 +3881,2151 @@ exports[`dashboard listing render table listing with initial filters from URL 1` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+ + +
+ + + + + +
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "dashboard", + "onChange": [Function], + "toolsLeft": undefined, + } + } + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + Actions + + + + + +
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +
@@ -2641,8 +6257,11 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = "registerRight": [MockFunction], }, "navGroup": Object { + "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], + "prependNavgroupToBreadcrumbs": [MockFunction], + "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { "enableForcedAppSwitcherNavigation": [MockFunction], @@ -3646,11 +7265,274 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = data-test-subj="dashboardLandingPage" >
+ > + + +
+ + + + } + body={ + +

+ +

+

+ + + , + } + } + /> +

+
+ } + iconType="dashboardApp" + title={ +

+ +

+ } + > +
+ + + + +
+ + +

+ + Create your first dashboard + +

+
+ + + +
+ + +
+

+ + You can combine data views from any OpenSearch Dashboards app into one dashboard and see everything in one place. + +

+

+ + + , + } + } + > + New to OpenSearch Dashboards? + + + + to take a test drive. + +

+
+
+ + + +
+ + + + + + +
+ +
+ + +
@@ -3882,8 +7764,11 @@ exports[`dashboard listing renders table rows 1`] = ` "registerRight": [MockFunction], }, "navGroup": Object { + "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], + "prependNavgroupToBreadcrumbs": [MockFunction], + "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { "enableForcedAppSwitcherNavigation": [MockFunction], @@ -4887,11 +8772,2111 @@ exports[`dashboard listing renders table rows 1`] = ` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+ + +
+ + + + + +
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "", + "onChange": [Function], + "toolsLeft": undefined, + } + } + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + Actions + + + + + +
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +
@@ -5123,8 +11108,11 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` "registerRight": [MockFunction], }, "navGroup": Object { + "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], + "prependNavgroupToBreadcrumbs": [MockFunction], + "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { "enableForcedAppSwitcherNavigation": [MockFunction], @@ -6128,11 +12116,2231 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+ + +
+ + + + + +
+
+
+
+
+ +
+ + + } + > +
+
+ + + + Listing limit exceeded + + +
+ +
+ +
+

+ + + , + "entityNamePlural": "dashboards", + "listingLimitText": + listingLimit + , + "listingLimitValue": 1, + "totalItems": 2, + } + } + > + You have 2 dashboards, but your + + listingLimit + + setting prevents the table below from displaying more than 1. You can change this setting under + + + + Advanced Settings + + + + . + +

+
+
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "", + "onChange": [Function], + "toolsLeft": undefined, + } + } + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + Actions + + + + + +
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +
diff --git a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap index b2711069a51..ff9452bf525 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap @@ -208,8 +208,11 @@ exports[`Dashboard top nav render in embed mode 1`] = ` "registerRight": [MockFunction], }, "navGroup": Object { + "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], + "prependNavgroupToBreadcrumbs": [MockFunction], + "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { "enableForcedAppSwitcherNavigation": [MockFunction], @@ -1214,8 +1217,11 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = "registerRight": [MockFunction], }, "navGroup": Object { + "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], + "prependNavgroupToBreadcrumbs": [MockFunction], + "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { "enableForcedAppSwitcherNavigation": [MockFunction], @@ -2220,8 +2226,11 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b "registerRight": [MockFunction], }, "navGroup": Object { + "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], + "prependNavgroupToBreadcrumbs": [MockFunction], + "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { "enableForcedAppSwitcherNavigation": [MockFunction], @@ -3226,8 +3235,11 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu "registerRight": [MockFunction], }, "navGroup": Object { + "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], + "prependNavgroupToBreadcrumbs": [MockFunction], + "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { "enableForcedAppSwitcherNavigation": [MockFunction], @@ -4232,8 +4244,11 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be "registerRight": [MockFunction], }, "navGroup": Object { + "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], + "prependNavgroupToBreadcrumbs": [MockFunction], + "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { "enableForcedAppSwitcherNavigation": [MockFunction], @@ -5238,8 +5253,11 @@ exports[`Dashboard top nav render with all components 1`] = ` "registerRight": [MockFunction], }, "navGroup": Object { + "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], + "prependNavgroupToBreadcrumbs": [MockFunction], + "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { "enableForcedAppSwitcherNavigation": [MockFunction], diff --git a/src/plugins/home/public/application/application.tsx b/src/plugins/home/public/application/application.tsx index 7b383b53ae9..5df126c6168 100644 --- a/src/plugins/home/public/application/application.tsx +++ b/src/plugins/home/public/application/application.tsx @@ -57,6 +57,7 @@ export const renderApp = async ( .filter(({ id }) => navLinks.find(({ category, hidden }) => !hidden && category?.id === id)); chrome.setBreadcrumbs([{ text: homeTitle }]); + chrome.navGroup.setCurrentNavGroup(undefined); // dispatch synthetic hash change event to update hash history objects // this is necessary because hash updates triggered by using popState won't trigger this event naturally. diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 146b9558248..2fdb385b37c 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -344,7 +344,9 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> this.filterNavLinks(core); }); - this.addWorkspaceToBreadcrumbs(core); + if (!core.chrome.navGroup.getNavGroupEnabled()) { + this.addWorkspaceToBreadcrumbs(core); + } return {}; } From 6f9de985b409e30fe0516484f0847acc827acc4e Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Wed, 3 Jul 2024 11:38:47 +0800 Subject: [PATCH 2/9] prepend nav group into breadcrumbs Signed-off-by: Hailong Cui --- changelogs/fragments/7130.yml | 2 - src/core/public/chrome/chrome_service.mock.ts | 5 +- src/core/public/chrome/chrome_service.tsx | 4 +- .../chrome/nav_group/nav_group_service.ts | 10 +- .../header/collapsible_nav_group_enabled.tsx | 427 ------------------ src/core/public/chrome/ui/header/header.tsx | 4 +- .../ui/header/header_breadcrumbs.test.tsx | 1 + .../chrome/ui/header/header_breadcrumbs.tsx | 10 +- 8 files changed, 19 insertions(+), 444 deletions(-) delete mode 100644 changelogs/fragments/7130.yml delete mode 100644 src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx diff --git a/changelogs/fragments/7130.yml b/changelogs/fragments/7130.yml deleted file mode 100644 index 0e39759fa65..00000000000 --- a/changelogs/fragments/7130.yml +++ /dev/null @@ -1,2 +0,0 @@ -feat: -- 1/ Add current nav group into chrome service 2/ prepend current nav group into breadcrumbs ([#7130](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7130)) \ No newline at end of file diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 68ed367776e..735a3c01abe 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -37,12 +37,9 @@ const createSetupContractMock = () => { return { registerCollapsibleNavHeader: jest.fn(), navGroup: { + addNavLinksToGroup: jest.fn(), getNavGroupEnabled: jest.fn(), registerNavGroupUpdater: jest.fn(), - addNavLinksToGroup: jest.fn(), - prependNavgroupToBreadcrumbs: jest.fn(), - getCurrentNavGroup$: jest.fn(() => new BehaviorSubject(undefined)), - setCurrentNavGroup: jest.fn(), }, }; }; diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 91d1c5fc263..022cd8ff584 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -109,7 +109,7 @@ export interface StartDeps { overlays: OverlayStart; } -export type CollapsibleNavHeaderRender = () => JSX.Element | null; +type CollapsibleNavHeaderRender = () => JSX.Element | null; /** @internal */ export class ChromeService { @@ -299,9 +299,7 @@ export class ChromeService { survey={injectedMetadata.getSurvey()} collapsibleNavHeaderRender={this.collapsibleNavHeaderRender} sidecarConfig$={sidecarConfig$} - navGroupsMap$={navGroup.getNavGroupsMap$()} navGroupEnabled={navGroup.getNavGroupEnabled()} - onNavGroupSelected={navGroup.setCurrentNavGroup} currentNavgroup$={navGroup.getCurrentNavGroup$()} prependCurrentNavgroupToBreadcrumbs={navGroup.prependCurrentNavgroupToBreadcrumbs} /> diff --git a/src/core/public/chrome/nav_group/nav_group_service.ts b/src/core/public/chrome/nav_group/nav_group_service.ts index f13c5f433c1..b46213f2600 100644 --- a/src/core/public/chrome/nav_group/nav_group_service.ts +++ b/src/core/public/chrome/nav_group/nav_group_service.ts @@ -69,9 +69,13 @@ export interface ChromeNavGroupServiceStartContract { /** * prepend Home & NavGroup into current breadcrumbs * @param breadcrumbs current breadcrumbs + * @param appId current application id * @returns new prepend breadcrumbs */ - prependCurrentNavgroupToBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[]) => ChromeBreadcrumb[]; + prependCurrentNavgroupToBreadcrumbs: ( + breadcrumbs: ChromeBreadcrumb[], + appId?: string + ) => ChromeBreadcrumb[]; } /** @internal */ @@ -225,11 +229,11 @@ export class ChromeNavGroupService { } }, - prependCurrentNavgroupToBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[]) => { + prependCurrentNavgroupToBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[], appId?: string) => { const navGroupId = this.currentNavGroup$.getValue()?.id; const homeTitle = i18n.translate('core.breadcrumbs.homeTitle', { defaultMessage: 'Home' }); // home page will not have nav group information - const isHome = breadcrumbs && breadcrumbs.length === 1 && breadcrumbs[0].text === homeTitle; + const isHome = appId === 'home'; if (this.navGroupEnabled && navGroupId && !isHome) { const currentNavGroup = this.navGroupsMap$.getValue()[navGroupId]; diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx deleted file mode 100644 index d6a69a3ca35..00000000000 --- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx +++ /dev/null @@ -1,427 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import './collapsible_nav_group_enabled.scss'; -import { - EuiCollapsibleNavGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiListGroup, - EuiListGroupItem, - EuiShowFor, - EuiFlyout, - EuiButtonIcon, - EuiFlexGroup, - EuiSideNavItemType, - EuiSideNav, -} from '@elastic/eui'; -import { i18n } from '@osd/i18n'; -import { sortBy } from 'lodash'; -import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react'; -import useObservable from 'react-use/lib/useObservable'; -import * as Rx from 'rxjs'; -import { ChromeNavLink } from '../..'; -import { ChromeNavGroup, NavGroupType } from '../../../../types'; -import { InternalApplicationStart } from '../../../application/types'; -import { HttpStart } from '../../../http'; -import { HeaderProps, OnIsLockedUpdate } from './'; -import { createEuiListItem } from './nav_link'; -import type { Logos } from '../../../../common/types'; -import { CollapsibleNavHeaderRender } from '../../chrome_service'; -import { NavGroupItemInMap } from '../../nav_group'; -import { - fulfillRegistrationLinksToChromeNavLinks, - getOrderedLinksOrCategories, - LinkItem, - LinkItemType, -} from '../../utils'; - -interface Props { - appId$: InternalApplicationStart['currentAppId$']; - basePath: HttpStart['basePath']; - collapsibleNavHeaderRender?: CollapsibleNavHeaderRender; - id: string; - isLocked: boolean; - isNavOpen: boolean; - navLinks$: Rx.Observable; - storage?: Storage; - onIsLockedUpdate: OnIsLockedUpdate; - closeNav: () => void; - navigateToApp: InternalApplicationStart['navigateToApp']; - navigateToUrl: InternalApplicationStart['navigateToUrl']; - customNavLink$: Rx.Observable; - logos: Logos; - navGroupsMap$: Rx.Observable>; - onNavGroupSelected: HeaderProps['onNavGroupSelected']; - currentNavgroup$: Rx.Observable; -} - -interface NavGroupsProps { - navLinks: ChromeNavLink[]; - suffix?: React.ReactElement; - style?: React.CSSProperties; - appId?: string; - navigateToApp: InternalApplicationStart['navigateToApp']; - onClick: () => void; -} - -function NavGroups({ navLinks, suffix, style, appId, navigateToApp, onClick }: NavGroupsProps) { - const createNavItem = ({ link }: { link: ChromeNavLink }) => { - const euiListItem = createEuiListItem({ - link, - appId, - dataTestSubj: 'collapsibleNavAppLink', - navigateToApp, - onClick, - }); - - return { - id: link.id, - name:
{link.title}
, - onClick: euiListItem.onClick, - href: euiListItem.href, - className: 'no-margin-top', - isSelected: euiListItem.isActive, - }; - }; - const orderedLinksOrCategories = getOrderedLinksOrCategories(navLinks); - const createSideNavItem = (navLink: LinkItem): EuiSideNavItemType<{}> => { - if (navLink.itemType === LinkItemType.LINK) { - return createNavItem({ - link: navLink.link, - }); - } - - if (navLink.itemType === LinkItemType.PARENT_LINK && navLink.link) { - return { - ...createNavItem({ link: navLink.link }), - items: navLink.links.map((subNavLink) => createSideNavItem(subNavLink)), - }; - } - - if (navLink.itemType === LinkItemType.CATEGORY) { - return { - id: navLink.category?.id ?? '', - name:
{navLink.category?.label ?? ''}
, - items: navLink.links?.map((link) => createSideNavItem(link)), - }; - } - - return {} as EuiSideNavItemType<{}>; - }; - const sideNavItems = orderedLinksOrCategories - .map((navLink) => createSideNavItem(navLink)) - .filter((item): item is EuiSideNavItemType<{}> => !!item); - return ( - - - {suffix} - - ); -} - -export function CollapsibleNavGroupEnabled({ - basePath, - collapsibleNavHeaderRender, - id, - isLocked, - isNavOpen, - storage = window.localStorage, - onIsLockedUpdate, - closeNav, - navigateToApp, - navigateToUrl, - logos, - onNavGroupSelected, - ...observables -}: Props) { - const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden); - const customNavLink = useObservable(observables.customNavLink$, undefined); - const appId = useObservable(observables.appId$, ''); - const navGroupsMap = useObservable(observables.navGroupsMap$, {}); - const lockRef = useRef(null); - const focusGroup = useObservable(observables.currentNavgroup$, undefined); - - const [shouldShrinkSecondNavigation, setShouldShrinkSecondNavigation] = useState(false); - - useEffect(() => { - if (!focusGroup && appId) { - const orderedGroups = sortBy(Object.values(navGroupsMap), (group) => group.order); - const findMatchedGroup = orderedGroups.find( - (group) => !!group.navLinks.find((navLink) => navLink.id === appId) - ); - onNavGroupSelected(findMatchedGroup?.id); - } - }, [appId, navGroupsMap, focusGroup, onNavGroupSelected]); - - const secondNavigation = focusGroup ? ( - <> - {shouldShrinkSecondNavigation ? ( -
- setShouldShrinkSecondNavigation(false)} - /> -
- ) : null} - {!shouldShrinkSecondNavigation && ( - <> -
- - -

- {focusGroup.title} -

-
- - setShouldShrinkSecondNavigation(true)} - /> - -
-
- - - )} - - ) : null; - - const secondNavigationWidth = useMemo(() => { - if (shouldShrinkSecondNavigation) { - return 48; - } - - return 320; - }, [shouldShrinkSecondNavigation]); - - const flyoutSize = useMemo(() => { - if (focusGroup) { - return 320 + secondNavigationWidth; - } - - return 320; - }, [focusGroup, secondNavigationWidth]); - - const onGroupClick = ( - e: React.MouseEvent, - group: ChromeNavGroup - ) => { - const fulfilledLinks = fulfillRegistrationLinksToChromeNavLinks( - navGroupsMap[group.id]?.navLinks, - navLinks - ); - onNavGroupSelected(group.id); - - // the `navGroupsMap[group.id]?.navLinks` has already been sorted - const firstLink = fulfilledLinks[0]; - if (firstLink) { - const propsForEui = createEuiListItem({ - link: firstLink, - appId, - dataTestSubj: 'collapsibleNavAppLink', - navigateToApp, - }); - propsForEui.onClick(e); - } - }; - - const allLinksWithNavGroup = Object.values(navGroupsMap).reduce( - (total, navGroup) => [...total, ...navGroup.navLinks.map((navLink) => navLink.id)], - [] as string[] - ); - - return ( - <> - {isNavOpen || isLocked ? ( - -
-
- {customNavLink && ( - - - - - - - - - - )} - - !allLinksWithNavGroup.includes(link.id))} - suffix={ -
- - - {sortBy( - Object.values(navGroupsMap).filter( - (item) => item.type === NavGroupType.SYSTEM - ), - (navGroup) => navGroup.order - ).map((group) => { - return ( - { - if (focusGroup?.id === group.id) { - onNavGroupSelected(undefined); - } else { - onGroupClick(e, group); - } - }} - /> - ); - })} - - - {collapsibleNavHeaderRender && collapsibleNavHeaderRender()} - - - {sortBy( - Object.values(navGroupsMap).filter((item) => !item.type), - (navGroup) => navGroup.order - ).map((group) => { - return ( - { - if (focusGroup?.id === group.id) { - onNavGroupSelected(undefined); - } else { - onGroupClick(e, group); - } - }} - /> - ); - })} - - - {/* Docking button only for larger screens that can support it*/} - - - - { - onIsLockedUpdate(!isLocked); - if (lockRef.current) { - lockRef.current.focus(); - } - }} - iconType={isLocked ? 'lock' : 'lockOpen'} - /> - - - -
- } - navigateToApp={navigateToApp} - onClick={closeNav} - appId={appId} - /> -
- {secondNavigation && ( -
- {secondNavigation} -
- )} -
-
- ) : null} - {secondNavigation && !isLocked ? ( - {}} - size={secondNavigationWidth} - side="left" - hideCloseButton - > - {secondNavigation} - - ) : null} - - ); -} diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index e4f3c7f1257..3cd4cdecb60 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -96,9 +96,7 @@ export interface HeaderProps { logos: Logos; survey: string | undefined; sidecarConfig$: Observable; - navGroupsMap$: Observable>; navGroupEnabled: boolean; - onNavGroupSelected: (groupId: string | undefined) => void; currentNavgroup$: Observable; prependCurrentNavgroupToBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[]) => ChromeBreadcrumb[]; } @@ -115,7 +113,6 @@ export function Header({ logos, collapsibleNavHeaderRender, navGroupEnabled, - onNavGroupSelected, prependCurrentNavgroupToBreadcrumbs, ...observables }: HeaderProps) { @@ -232,6 +229,7 @@ export function Header({ breadcrumbs$={observables.breadcrumbs$} currentNavgroup$={observables.currentNavgroup$} navgroupEnabled={navGroupEnabled} + appId$={application.currentAppId$} prependCurrentNavgroupToBreadcrumbs={prependCurrentNavgroupToBreadcrumbs} /> diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx index 0b84baf0121..5aa4ebfb784 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx @@ -44,6 +44,7 @@ describe('HeaderBreadcrumbs', () => { prependCurrentNavgroupToBreadcrumbs={jest.fn()} appTitle$={new BehaviorSubject('')} breadcrumbs$={breadcrumbs$} + appId$={new BehaviorSubject(undefined)} /> ); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx index 250f9ce3701..c254f7ed61e 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx @@ -40,8 +40,12 @@ interface Props { appTitle$: Observable; breadcrumbs$: Observable; navgroupEnabled: boolean; + appId$: Observable; currentNavgroup$: Observable; - prependCurrentNavgroupToBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[]) => ChromeBreadcrumb[]; + prependCurrentNavgroupToBreadcrumbs: ( + breadcrumbs: ChromeBreadcrumb[], + appId?: string + ) => ChromeBreadcrumb[]; } export function HeaderBreadcrumbs({ @@ -50,6 +54,7 @@ export function HeaderBreadcrumbs({ navgroupEnabled, currentNavgroup$, prependCurrentNavgroupToBreadcrumbs, + appId$, }: Props) { const appTitle = useObservable(appTitle$, 'OpenSearch Dashboards'); const breadcrumbs = useObservable(breadcrumbs$, []); @@ -60,8 +65,9 @@ export function HeaderBreadcrumbs({ } const currentNavgroup = useObservable(currentNavgroup$, undefined); + const appId = useObservable(appId$, undefined); if (navgroupEnabled && currentNavgroup) { - crumbs = prependCurrentNavgroupToBreadcrumbs(crumbs); + crumbs = prependCurrentNavgroupToBreadcrumbs(crumbs, appId); } crumbs = crumbs.map((breadcrumb, i) => ({ From beb2c3f0f384f77687fcb146ff6f4e31eefd991b Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Wed, 3 Jul 2024 15:31:10 +0800 Subject: [PATCH 3/9] add unit test Signed-off-by: Hailong Cui --- src/core/public/chrome/chrome_service.mock.ts | 2 +- src/core/public/chrome/chrome_service.tsx | 2 +- .../nav_group/nav_group_service.test.ts | 121 +++++++++- .../chrome/nav_group/nav_group_service.ts | 45 ++-- .../header/__snapshots__/header.test.tsx.snap | 210 +++++++++++++++++- .../header_breadcrumbs.test.tsx.snap | 40 ++++ .../public/chrome/ui/header/header.test.tsx | 1 - src/core/public/chrome/ui/header/header.tsx | 1 - .../ui/header/header_breadcrumbs.test.tsx | 36 ++- .../chrome/ui/header/header_breadcrumbs.tsx | 5 +- .../dashboard_listing.test.tsx.snap | 10 +- .../dashboard_top_nav.test.tsx.snap | 12 +- .../home/public/application/application.tsx | 1 - 13 files changed, 441 insertions(+), 45 deletions(-) diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 735a3c01abe..80d160c2459 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -78,7 +78,7 @@ const createStartContractMock = () => { navGroup: { getNavGroupsMap$: jest.fn(() => new BehaviorSubject({})), getNavGroupEnabled: jest.fn(), - prependCurrentNavgroupToBreadcrumbs: jest.fn(), + prependCurrentNavGroupToBreadcrumbs: jest.fn(), getCurrentNavGroup$: jest.fn(() => new BehaviorSubject(undefined)), setCurrentNavGroup: jest.fn(), }, diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 022cd8ff584..4c962a4f497 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -301,7 +301,7 @@ export class ChromeService { sidecarConfig$={sidecarConfig$} navGroupEnabled={navGroup.getNavGroupEnabled()} currentNavgroup$={navGroup.getCurrentNavGroup$()} - prependCurrentNavgroupToBreadcrumbs={navGroup.prependCurrentNavgroupToBreadcrumbs} + prependCurrentNavgroupToBreadcrumbs={navGroup.prependCurrentNavGroupToBreadcrumbs} /> ), diff --git a/src/core/public/chrome/nav_group/nav_group_service.test.ts b/src/core/public/chrome/nav_group/nav_group_service.test.ts index ab4d24c990c..b4cfcfe44d2 100644 --- a/src/core/public/chrome/nav_group/nav_group_service.test.ts +++ b/src/core/public/chrome/nav_group/nav_group_service.test.ts @@ -5,7 +5,11 @@ import * as Rx from 'rxjs'; import { first } from 'rxjs/operators'; -import { ChromeNavGroupService, ChromeRegistrationNavLink } from './nav_group_service'; +import { + ChromeNavGroupService, + ChromeRegistrationNavLink, + CURRENT_NAV_GROUP_ID, +} from './nav_group_service'; import { uiSettingsServiceMock } from '../../ui_settings/ui_settings_service.mock'; import { NavLinksService } from '../nav_links'; import { applicationServiceMock, httpServiceMock } from '../../mocks'; @@ -82,6 +86,31 @@ mockedGetNavLinks.mockReturnValue( ]) ); +interface LooseObject { + [key: string]: any; +} + +// Mock sessionStorage +const sessionStorageMock = (() => { + let store = {} as LooseObject; + return { + getItem(key: string) { + return store[key] || null; + }, + setItem(key: string, value: string) { + store[key] = value.toString(); + }, + removeItem(key: string) { + delete store[key]; + }, + clear() { + store = {}; + }, + }; +})(); + +Object.defineProperty(window, 'sessionStorage', { value: sessionStorageMock }); + describe('ChromeNavGroupService#setup()', () => { it('should be able to `addNavLinksToGroup`', async () => { const warnMock = jest.fn(); @@ -226,6 +255,96 @@ describe('ChromeNavGroupService#start()', () => { navGroupEnabled$.next(true); expect(chromeNavGroupServiceStart.getNavGroupEnabled()).toBe(false); }); + + it('should able to set current nav group', async () => { + const uiSettings = uiSettingsServiceMock.createSetupContract(); + const navGroupEnabled$ = new Rx.BehaviorSubject(true); + uiSettings.get$.mockImplementation(() => navGroupEnabled$); + + const chromeNavGroupService = new ChromeNavGroupService(); + const chromeNavGroupServiceSetup = chromeNavGroupService.setup({ uiSettings }); + + chromeNavGroupServiceSetup.addNavLinksToGroup( + { + id: 'foo', + title: 'foo title', + description: 'foo description', + }, + [mockedNavLinkFoo] + ); + + const chromeNavGroupServiceStart = await chromeNavGroupService.start({ + navLinks: mockedNavLinkService, + application: mockedApplicationService, + }); + + // set an existing nav group id + chromeNavGroupServiceStart.setCurrentNavGroup('foo'); + + expect(sessionStorageMock.getItem(CURRENT_NAV_GROUP_ID)).toEqual('foo'); + + let currentNavGroup = await chromeNavGroupServiceStart + .getCurrentNavGroup$() + .pipe(first()) + .toPromise(); + + expect(currentNavGroup?.id).toEqual('foo'); + expect(currentNavGroup?.title).toEqual('foo title'); + + // set a invalid nav group id + chromeNavGroupServiceStart.setCurrentNavGroup('bar'); + currentNavGroup = await chromeNavGroupServiceStart + .getCurrentNavGroup$() + .pipe(first()) + .toPromise(); + + expect(sessionStorageMock.getItem(CURRENT_NAV_GROUP_ID)).toBeFalsy(); + expect(currentNavGroup).toBeUndefined(); + + // reset current nav group + chromeNavGroupServiceStart.setCurrentNavGroup(undefined); + currentNavGroup = await chromeNavGroupServiceStart + .getCurrentNavGroup$() + .pipe(first()) + .toPromise(); + + expect(sessionStorageMock.getItem(CURRENT_NAV_GROUP_ID)).toBeFalsy(); + expect(currentNavGroup).toBeUndefined(); + }); + + it('shoud able to prepend current nav group into breadcrumbs', async () => { + const uiSettings = uiSettingsServiceMock.createSetupContract(); + const navGroupEnabled$ = new Rx.BehaviorSubject(true); + uiSettings.get$.mockImplementation(() => navGroupEnabled$); + + const chromeNavGroupService = new ChromeNavGroupService(); + const chromeNavGroupServiceSetup = chromeNavGroupService.setup({ uiSettings }); + + chromeNavGroupServiceSetup.addNavLinksToGroup( + { + id: 'foo', + title: 'Foo title', + description: 'Foo description', + }, + [mockedNavLinkFoo] + ); + + const chromeNavGroupServiceStart = await chromeNavGroupService.start({ + navLinks: mockedNavLinkService, + application: mockedApplicationService, + }); + + chromeNavGroupServiceStart.setCurrentNavGroup('foo'); + + const existingBreadcrumbs = [{ text: 'First' }]; + const newBreadcrumbs = chromeNavGroupServiceStart.prependCurrentNavGroupToBreadcrumbs( + existingBreadcrumbs + ); + expect(newBreadcrumbs.length).toEqual(3); + expect(newBreadcrumbs[0].text).toEqual('Home'); + expect(newBreadcrumbs[1].text).toEqual('Foo title'); + expect(newBreadcrumbs[2].text).toEqual('First'); + }); }); describe('nav group updater', () => { diff --git a/src/core/public/chrome/nav_group/nav_group_service.ts b/src/core/public/chrome/nav_group/nav_group_service.ts index b46213f2600..f4ca78fb0ef 100644 --- a/src/core/public/chrome/nav_group/nav_group_service.ts +++ b/src/core/public/chrome/nav_group/nav_group_service.ts @@ -12,7 +12,6 @@ import { } from 'opensearch-dashboards/public'; import { map, switchMap, takeUntil } from 'rxjs/operators'; import { i18n } from '@osd/i18n'; -import _ from 'lodash'; import { IUiSettingsClient } from '../../ui_settings'; import { flattenLinksOrCategories, @@ -22,7 +21,7 @@ import { import { ChromeNavLinks } from '../nav_links'; import { InternalApplicationStart } from '../../application'; -const CURRENT_NAV_GROUP_ID = 'core.chrome.currentNavGroupId'; +export const CURRENT_NAV_GROUP_ID = 'core.chrome.currentNavGroupId'; /** @public */ export interface ChromeRegistrationNavLink { @@ -72,7 +71,7 @@ export interface ChromeNavGroupServiceStartContract { * @param appId current application id * @returns new prepend breadcrumbs */ - prependCurrentNavgroupToBreadcrumbs: ( + prependCurrentNavGroupToBreadcrumbs: ( breadcrumbs: ChromeBreadcrumb[], appId?: string ) => ChromeBreadcrumb[]; @@ -213,39 +212,47 @@ export class ChromeNavGroupService { currentNavGroupId ? this.navGroupsMap$.getValue()[currentNavGroupId] : undefined ); + const setCurrentNavGroup = (navGroupId: string | undefined) => { + const navGroup = navGroupId ? this.navGroupsMap$.getValue()[navGroupId] : undefined; + if (navGroup) { + this.currentNavGroup$.next(navGroup); + sessionStorage.setItem(CURRENT_NAV_GROUP_ID, navGroup.id); + } else { + this.currentNavGroup$.next(undefined); + sessionStorage.removeItem(CURRENT_NAV_GROUP_ID); + } + }; + + // erase current nav group when switch to home page + application.currentAppId$.subscribe((appId) => { + if (appId === 'home') { + setCurrentNavGroup(undefined); + } + }); + return { getNavGroupsMap$: () => this.getSortedNavGroupsMap$(), getNavGroupEnabled: () => this.navGroupEnabled, getCurrentNavGroup$: () => this.currentNavGroup$, - setCurrentNavGroup: (navGroupId: string | undefined) => { - const navGroup = navGroupId ? this.navGroupsMap$.getValue()[navGroupId] : undefined; - if (navGroup) { - this.currentNavGroup$.next(navGroup); - sessionStorage.setItem(CURRENT_NAV_GROUP_ID, navGroup.id); - } else { - this.currentNavGroup$.next(undefined); - sessionStorage.removeItem(CURRENT_NAV_GROUP_ID); - } - }, + setCurrentNavGroup, - prependCurrentNavgroupToBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[], appId?: string) => { + prependCurrentNavGroupToBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[], appId?: string) => { const navGroupId = this.currentNavGroup$.getValue()?.id; const homeTitle = i18n.translate('core.breadcrumbs.homeTitle', { defaultMessage: 'Home' }); // home page will not have nav group information - const isHome = appId === 'home'; - if (this.navGroupEnabled && navGroupId && !isHome) { + if (this.navGroupEnabled && navGroupId) { const currentNavGroup = this.navGroupsMap$.getValue()[navGroupId]; // breadcrumb order is home > navgroup > application, navgroup will be second one const navGroupInBreadcrumbs = breadcrumbs.length > 1 && breadcrumbs[1]?.text === currentNavGroup.title; if (!navGroupInBreadcrumbs) { - const navgroupBreadcrumb: ChromeBreadcrumb = { + const navGroupBreadcrumb: ChromeBreadcrumb = { text: currentNavGroup.title, onClick: () => { - if (currentNavGroup.navLinks) { + if (currentNavGroup.navLinks && currentNavGroup.navLinks.length) { const orderedLinks = this.sortNavGroupNavLinks( currentNavGroup, navLinks.getAll() @@ -260,7 +267,7 @@ export class ChromeNavGroupService { application.navigateToApp('home'); }, }; - return [homeBreadcrumb, navgroupBreadcrumb, ...breadcrumbs]; + return [homeBreadcrumb, navGroupBreadcrumb, ...breadcrumbs]; } } return breadcrumbs; diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 741edc68db1..871ec274779 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -311,7 +311,45 @@ exports[`Header handles visibility and lock changes 1`] = ` "closed": false, "hasError": false, "isStopped": false, - "observers": Array [], + "observers": Array [ + Subscriber { + "_parentOrParents": null, + "_subscriptions": Array [ + SubjectSubscription { + "_parentOrParents": [Circular], + "_subscriptions": null, + "closed": false, + "subject": [Circular], + "subscriber": [Circular], + }, + ], + "closed": false, + "destination": SafeSubscriber { + "_complete": undefined, + "_context": [Circular], + "_error": undefined, + "_next": [Function], + "_parentOrParents": null, + "_parentSubscriber": [Circular], + "_subscriptions": null, + "closed": false, + "destination": Object { + "closed": true, + "complete": [Function], + "error": [Function], + "next": [Function], + }, + "isStopped": false, + "syncErrorThrowable": false, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + "isStopped": false, + "syncErrorThrowable": true, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + ], "thrownError": null, } } @@ -1750,6 +1788,18 @@ exports[`Header handles visibility and lock changes 1`] = ` "thrownError": null, } } + navGroupEnabled={false} + navGroupsMap$={ + BehaviorSubject { + "_isScalar": false, + "_value": Object {}, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + } + } navLinks$={ BehaviorSubject { "_isScalar": false, @@ -1881,9 +1931,9 @@ exports[`Header handles visibility and lock changes 1`] = ` } } onIsLockedUpdate={[Function]} - onNavGroupSelected={[Function]} opensearchDashboardsDocLink="/docs" opensearchDashboardsVersion="1.0.0" + prependCurrentNavgroupToBreadcrumbs={[MockFunction]} recentlyAccessed$={ BehaviorSubject { "_isScalar": false, @@ -3953,6 +4003,57 @@ exports[`Header handles visibility and lock changes 1`] = ` "thrownError": null, } } + currentNavgroup$={ + BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ + Subscriber { + "_parentOrParents": null, + "_subscriptions": Array [ + SubjectSubscription { + "_parentOrParents": [Circular], + "_subscriptions": null, + "closed": false, + "subject": [Circular], + "subscriber": [Circular], + }, + ], + "closed": false, + "destination": SafeSubscriber { + "_complete": undefined, + "_context": [Circular], + "_error": undefined, + "_next": [Function], + "_parentOrParents": null, + "_parentSubscriber": [Circular], + "_subscriptions": null, + "closed": false, + "destination": Object { + "closed": true, + "complete": [Function], + "error": [Function], + "next": [Function], + }, + "isStopped": false, + "syncErrorThrowable": false, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + "isStopped": false, + "syncErrorThrowable": true, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + ], + "thrownError": null, + } + } + navgroupEnabled={false} + prependCurrentNavgroupToBreadcrumbs={[MockFunction]} > + Analytics + , + + First + , + + Second + , +] +`; + +exports[`HeaderBreadcrumbs prepend current nav group into existing breadcrumbs when nav group is enabled 2`] = ` + + Analytics + +`; + exports[`HeaderBreadcrumbs renders updates to the breadcrumbs$ observable 1`] = ` diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx index 5aa4ebfb784..c0fe8eaf311 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx @@ -44,7 +44,6 @@ describe('HeaderBreadcrumbs', () => { prependCurrentNavgroupToBreadcrumbs={jest.fn()} appTitle$={new BehaviorSubject('')} breadcrumbs$={breadcrumbs$} - appId$={new BehaviorSubject(undefined)} /> ); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); @@ -57,4 +56,39 @@ describe('HeaderBreadcrumbs', () => { wrapper.update(); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); }); + + it('prepend current nav group into existing breadcrumbs when nav group is enabled', () => { + const breadcrumbs$ = new BehaviorSubject([{ text: 'First' }]); + const currentNavgroup$ = new BehaviorSubject({ + id: 'analytics', + title: 'Analytics', + description: '', + }); + const wrapper = mount( + [ + { text: currentNavgroup$.getValue().title }, + ...breadcrumbs, + ]} + appTitle$={new BehaviorSubject('')} + breadcrumbs$={breadcrumbs$} + /> + ); + const breadcrumbs = wrapper.find('.euiBreadcrumb'); + expect(breadcrumbs).toHaveLength(2); + expect(breadcrumbs.at(0).text()).toBe('Analytics'); + expect(breadcrumbs.at(1).text()).toBe('First'); + + act(() => breadcrumbs$.next([{ text: 'First' }, { text: 'Second' }])); + wrapper.update(); + expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); + expect(wrapper.find('.euiBreadcrumb')).toHaveLength(3); + + act(() => breadcrumbs$.next([])); + wrapper.update(); + expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); + expect(wrapper.find('.euiBreadcrumb')).toHaveLength(1); + }); }); diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx index c254f7ed61e..ff056a95b67 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx @@ -40,7 +40,6 @@ interface Props { appTitle$: Observable; breadcrumbs$: Observable; navgroupEnabled: boolean; - appId$: Observable; currentNavgroup$: Observable; prependCurrentNavgroupToBreadcrumbs: ( breadcrumbs: ChromeBreadcrumb[], @@ -54,7 +53,6 @@ export function HeaderBreadcrumbs({ navgroupEnabled, currentNavgroup$, prependCurrentNavgroupToBreadcrumbs, - appId$, }: Props) { const appTitle = useObservable(appTitle$, 'OpenSearch Dashboards'); const breadcrumbs = useObservable(breadcrumbs$, []); @@ -65,9 +63,8 @@ export function HeaderBreadcrumbs({ } const currentNavgroup = useObservable(currentNavgroup$, undefined); - const appId = useObservable(appId$, undefined); if (navgroupEnabled && currentNavgroup) { - crumbs = prependCurrentNavgroupToBreadcrumbs(crumbs, appId); + crumbs = prependCurrentNavgroupToBreadcrumbs(crumbs); } crumbs = crumbs.map((breadcrumb, i) => ({ diff --git a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap index ffa8c561454..5ee60008e03 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap @@ -223,7 +223,7 @@ exports[`dashboard listing hideWriteControls 1`] = ` "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependNavgroupToBreadcrumbs": [MockFunction], + "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { @@ -2876,7 +2876,7 @@ exports[`dashboard listing render table listing with initial filters from URL 1` "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependNavgroupToBreadcrumbs": [MockFunction], + "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { @@ -6260,7 +6260,7 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependNavgroupToBreadcrumbs": [MockFunction], + "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { @@ -7767,7 +7767,7 @@ exports[`dashboard listing renders table rows 1`] = ` "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependNavgroupToBreadcrumbs": [MockFunction], + "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { @@ -11111,7 +11111,7 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependNavgroupToBreadcrumbs": [MockFunction], + "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { diff --git a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap index ff9452bf525..5a3643f7f59 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap @@ -211,7 +211,7 @@ exports[`Dashboard top nav render in embed mode 1`] = ` "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependNavgroupToBreadcrumbs": [MockFunction], + "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { @@ -1220,7 +1220,7 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependNavgroupToBreadcrumbs": [MockFunction], + "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { @@ -2229,7 +2229,7 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependNavgroupToBreadcrumbs": [MockFunction], + "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { @@ -3238,7 +3238,7 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependNavgroupToBreadcrumbs": [MockFunction], + "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { @@ -4247,7 +4247,7 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependNavgroupToBreadcrumbs": [MockFunction], + "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { @@ -5256,7 +5256,7 @@ exports[`Dashboard top nav render with all components 1`] = ` "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependNavgroupToBreadcrumbs": [MockFunction], + "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { diff --git a/src/plugins/home/public/application/application.tsx b/src/plugins/home/public/application/application.tsx index 5df126c6168..7b383b53ae9 100644 --- a/src/plugins/home/public/application/application.tsx +++ b/src/plugins/home/public/application/application.tsx @@ -57,7 +57,6 @@ export const renderApp = async ( .filter(({ id }) => navLinks.find(({ category, hidden }) => !hidden && category?.id === id)); chrome.setBreadcrumbs([{ text: homeTitle }]); - chrome.navGroup.setCurrentNavGroup(undefined); // dispatch synthetic hash change event to update hash history objects // this is necessary because hash updates triggered by using popState won't trigger this event naturally. From c1dc023d071af6b8e9dba1621e670fb218836359 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Wed, 3 Jul 2024 15:42:02 +0800 Subject: [PATCH 4/9] remove lodash import Signed-off-by: Hailong Cui --- .../public/chrome/nav_group/nav_group_service.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/core/public/chrome/nav_group/nav_group_service.ts b/src/core/public/chrome/nav_group/nav_group_service.ts index f4ca78fb0ef..2c7e59f314b 100644 --- a/src/core/public/chrome/nav_group/nav_group_service.ts +++ b/src/core/public/chrome/nav_group/nav_group_service.ts @@ -6,6 +6,7 @@ import { BehaviorSubject, combineLatest, Observable, of, ReplaySubject, Subscription } from 'rxjs'; import { AppCategory, + ApplicationStart, ChromeBreadcrumb, ChromeNavGroup, ChromeNavLink, @@ -19,7 +20,6 @@ import { getOrderedLinksOrCategories, } from '../utils'; import { ChromeNavLinks } from '../nav_links'; -import { InternalApplicationStart } from '../../application'; export const CURRENT_NAV_GROUP_ID = 'core.chrome.currentNavGroupId'; @@ -68,13 +68,9 @@ export interface ChromeNavGroupServiceStartContract { /** * prepend Home & NavGroup into current breadcrumbs * @param breadcrumbs current breadcrumbs - * @param appId current application id * @returns new prepend breadcrumbs */ - prependCurrentNavGroupToBreadcrumbs: ( - breadcrumbs: ChromeBreadcrumb[], - appId?: string - ) => ChromeBreadcrumb[]; + prependCurrentNavGroupToBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[]) => ChromeBreadcrumb[]; } /** @internal */ @@ -203,7 +199,7 @@ export class ChromeNavGroupService { application, }: { navLinks: ChromeNavLinks; - application: InternalApplicationStart; + application: ApplicationStart; }): Promise { this.navLinks$ = navLinks.getNavLinks$(); @@ -237,7 +233,7 @@ export class ChromeNavGroupService { getCurrentNavGroup$: () => this.currentNavGroup$, setCurrentNavGroup, - prependCurrentNavGroupToBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[], appId?: string) => { + prependCurrentNavGroupToBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[]) => { const navGroupId = this.currentNavGroup$.getValue()?.id; const homeTitle = i18n.translate('core.breadcrumbs.homeTitle', { defaultMessage: 'Home' }); // home page will not have nav group information From bad5da303bc79e297dd18a6dedba82502edc260c Mon Sep 17 00:00:00 2001 From: "opensearch-changeset-bot[bot]" <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Date: Wed, 3 Jul 2024 07:56:38 +0000 Subject: [PATCH 5/9] Changeset file for PR #7166 created/updated --- changelogs/fragments/7166.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/7166.yml diff --git a/changelogs/fragments/7166.yml b/changelogs/fragments/7166.yml new file mode 100644 index 00000000000..937c8545c0b --- /dev/null +++ b/changelogs/fragments/7166.yml @@ -0,0 +1,2 @@ +feat: +- 1. Add current nav group into chrome service 2. Prepend current nav group into breadcrumb ([#7166](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7166)) \ No newline at end of file From bd373bf3722e7781c4717e57e170323bffda0e86 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Wed, 3 Jul 2024 16:15:32 +0800 Subject: [PATCH 6/9] fix bootstrap error Signed-off-by: Hailong Cui --- src/core/public/chrome/nav_group/nav_group_service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/public/chrome/nav_group/nav_group_service.ts b/src/core/public/chrome/nav_group/nav_group_service.ts index 2c7e59f314b..2a892206204 100644 --- a/src/core/public/chrome/nav_group/nav_group_service.ts +++ b/src/core/public/chrome/nav_group/nav_group_service.ts @@ -6,7 +6,6 @@ import { BehaviorSubject, combineLatest, Observable, of, ReplaySubject, Subscription } from 'rxjs'; import { AppCategory, - ApplicationStart, ChromeBreadcrumb, ChromeNavGroup, ChromeNavLink, @@ -20,6 +19,7 @@ import { getOrderedLinksOrCategories, } from '../utils'; import { ChromeNavLinks } from '../nav_links'; +import { InternalApplicationStart } from '../../application'; export const CURRENT_NAV_GROUP_ID = 'core.chrome.currentNavGroupId'; @@ -199,7 +199,7 @@ export class ChromeNavGroupService { application, }: { navLinks: ChromeNavLinks; - application: ApplicationStart; + application: InternalApplicationStart; }): Promise { this.navLinks$ = navLinks.getNavLinks$(); From 5f0ec5b5acbbca8a1f818e368cf47ab293a6e5b8 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Wed, 3 Jul 2024 16:40:16 +0800 Subject: [PATCH 7/9] add nav group status check Signed-off-by: Hailong Cui --- .../public/chrome/nav_group/nav_group_service.test.ts | 10 ++++++++-- src/core/public/chrome/nav_group/nav_group_service.ts | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/core/public/chrome/nav_group/nav_group_service.test.ts b/src/core/public/chrome/nav_group/nav_group_service.test.ts index b4cfcfe44d2..af261eb06ae 100644 --- a/src/core/public/chrome/nav_group/nav_group_service.test.ts +++ b/src/core/public/chrome/nav_group/nav_group_service.test.ts @@ -359,7 +359,10 @@ describe('nav group updater', () => { id: 'foo', }, ]); - const navGroupStart = await navGroup.start({ navLinks: mockedNavLinkService }); + const navGroupStart = await navGroup.start({ + navLinks: mockedNavLinkService, + application: mockedApplicationService, + }); expect(await navGroupStart.getNavGroupsMap$().pipe(first()).toPromise()).toEqual({ dataAdministration: expect.not.objectContaining({ @@ -393,7 +396,10 @@ describe('nav group updater', () => { status: 2, })); const unregister = navGroupSetup.registerNavGroupUpdater(appUpdater$); - const navGroupStart = await navGroup.start({ navLinks: mockedNavLinkService }); + const navGroupStart = await navGroup.start({ + navLinks: mockedNavLinkService, + application: mockedApplicationService, + }); expect(await navGroupStart.getNavGroupsMap$().pipe(first()).toPromise()).toEqual({ dataAdministration: expect.objectContaining({ status: 2, diff --git a/src/core/public/chrome/nav_group/nav_group_service.ts b/src/core/public/chrome/nav_group/nav_group_service.ts index 2a892206204..1b8bad9e68f 100644 --- a/src/core/public/chrome/nav_group/nav_group_service.ts +++ b/src/core/public/chrome/nav_group/nav_group_service.ts @@ -20,6 +20,7 @@ import { } from '../utils'; import { ChromeNavLinks } from '../nav_links'; import { InternalApplicationStart } from '../../application'; +import { NavGroupStatus } from '../../../../core/types'; export const CURRENT_NAV_GROUP_ID = 'core.chrome.currentNavGroupId'; @@ -210,7 +211,7 @@ export class ChromeNavGroupService { const setCurrentNavGroup = (navGroupId: string | undefined) => { const navGroup = navGroupId ? this.navGroupsMap$.getValue()[navGroupId] : undefined; - if (navGroup) { + if (navGroup && navGroup.status !== NavGroupStatus.Hidden) { this.currentNavGroup$.next(navGroup); sessionStorage.setItem(CURRENT_NAV_GROUP_ID, navGroup.id); } else { From 1727254c57a9989d69f251ba8a51b0ea0e4d8794 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Thu, 4 Jul 2024 12:07:51 +0800 Subject: [PATCH 8/9] address review comments Signed-off-by: Hailong Cui --- src/core/public/chrome/chrome_service.mock.ts | 1 - src/core/public/chrome/chrome_service.tsx | 1 - .../nav_group/nav_group_service.test.ts | 37 ++- .../chrome/nav_group/nav_group_service.ts | 77 ++---- .../header_breadcrumbs.test.tsx.snap | 219 +++++++++++++++--- src/core/public/chrome/ui/header/header.tsx | 10 +- .../ui/header/header_breadcrumbs.test.tsx | 33 ++- .../chrome/ui/header/header_breadcrumbs.tsx | 60 ++++- 8 files changed, 306 insertions(+), 132 deletions(-) diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 80d160c2459..dd595c31f45 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -78,7 +78,6 @@ const createStartContractMock = () => { navGroup: { getNavGroupsMap$: jest.fn(() => new BehaviorSubject({})), getNavGroupEnabled: jest.fn(), - prependCurrentNavGroupToBreadcrumbs: jest.fn(), getCurrentNavGroup$: jest.fn(() => new BehaviorSubject(undefined)), setCurrentNavGroup: jest.fn(), }, diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 4c962a4f497..56cc2da79ea 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -301,7 +301,6 @@ export class ChromeService { sidecarConfig$={sidecarConfig$} navGroupEnabled={navGroup.getNavGroupEnabled()} currentNavgroup$={navGroup.getCurrentNavGroup$()} - prependCurrentNavgroupToBreadcrumbs={navGroup.prependCurrentNavGroupToBreadcrumbs} /> ), diff --git a/src/core/public/chrome/nav_group/nav_group_service.test.ts b/src/core/public/chrome/nav_group/nav_group_service.test.ts index af261eb06ae..bc18483178f 100644 --- a/src/core/public/chrome/nav_group/nav_group_service.test.ts +++ b/src/core/public/chrome/nav_group/nav_group_service.test.ts @@ -312,7 +312,7 @@ describe('ChromeNavGroupService#start()', () => { expect(currentNavGroup).toBeUndefined(); }); - it('shoud able to prepend current nav group into breadcrumbs', async () => { + it('should reset current nav group if app not belongs to any nav group', async () => { const uiSettings = uiSettingsServiceMock.createSetupContract(); const navGroupEnabled$ = new Rx.BehaviorSubject(true); uiSettings.get$.mockImplementation(() => navGroupEnabled$); @@ -323,10 +323,10 @@ describe('ChromeNavGroupService#start()', () => { chromeNavGroupServiceSetup.addNavLinksToGroup( { id: 'foo', - title: 'Foo title', - description: 'Foo description', + title: 'foo title', + description: 'foo description', }, - [mockedNavLinkFoo] + [{ id: 'foo-app1' }] ); const chromeNavGroupServiceStart = await chromeNavGroupService.start({ @@ -334,16 +334,29 @@ describe('ChromeNavGroupService#start()', () => { application: mockedApplicationService, }); + // set an existing nav group id chromeNavGroupServiceStart.setCurrentNavGroup('foo'); - const existingBreadcrumbs = [{ text: 'First' }]; - const newBreadcrumbs = chromeNavGroupServiceStart.prependCurrentNavGroupToBreadcrumbs( - existingBreadcrumbs - ); - expect(newBreadcrumbs.length).toEqual(3); - expect(newBreadcrumbs[0].text).toEqual('Home'); - expect(newBreadcrumbs[1].text).toEqual('Foo title'); - expect(newBreadcrumbs[2].text).toEqual('First'); + expect(sessionStorageMock.getItem(CURRENT_NAV_GROUP_ID)).toEqual('foo'); + + let currentNavGroup = await chromeNavGroupServiceStart + .getCurrentNavGroup$() + .pipe(first()) + .toPromise(); + + expect(currentNavGroup?.id).toEqual('foo'); + + // navigate to app don't belongs to any nav group + mockedApplicationService.navigateToApp('bar-app'); + + currentNavGroup = await chromeNavGroupServiceStart + .getCurrentNavGroup$() + .pipe(first()) + .toPromise(); + + // verify current nav group been reset + expect(currentNavGroup).toBeFalsy(); + expect(sessionStorageMock.getItem(CURRENT_NAV_GROUP_ID)).toBeFalsy(); }); }); diff --git a/src/core/public/chrome/nav_group/nav_group_service.ts b/src/core/public/chrome/nav_group/nav_group_service.ts index 1b8bad9e68f..d8c4263911d 100644 --- a/src/core/public/chrome/nav_group/nav_group_service.ts +++ b/src/core/public/chrome/nav_group/nav_group_service.ts @@ -4,14 +4,8 @@ */ import { BehaviorSubject, combineLatest, Observable, of, ReplaySubject, Subscription } from 'rxjs'; -import { - AppCategory, - ChromeBreadcrumb, - ChromeNavGroup, - ChromeNavLink, -} from 'opensearch-dashboards/public'; +import { AppCategory, ChromeNavGroup, ChromeNavLink } from 'opensearch-dashboards/public'; import { map, switchMap, takeUntil } from 'rxjs/operators'; -import { i18n } from '@osd/i18n'; import { IUiSettingsClient } from '../../ui_settings'; import { flattenLinksOrCategories, @@ -58,20 +52,13 @@ export interface ChromeNavGroupServiceStartContract { /** * Get an observable of the current selected nav group */ - getCurrentNavGroup$: () => Observable; + getCurrentNavGroup$: () => Observable; /** * Set current selected nav group * @param navGroupId The id of the nav group to be set as current */ setCurrentNavGroup: (navGroupId: string | undefined) => void; - - /** - * prepend Home & NavGroup into current breadcrumbs - * @param breadcrumbs current breadcrumbs - * @returns new prepend breadcrumbs - */ - prependCurrentNavGroupToBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[]) => ChromeBreadcrumb[]; } /** @internal */ @@ -220,55 +207,37 @@ export class ChromeNavGroupService { } }; - // erase current nav group when switch to home page + // erase current nav group when switch app don't belongs to any nav group application.currentAppId$.subscribe((appId) => { - if (appId === 'home') { + const navGroupMap = this.navGroupsMap$.getValue(); + const appIdsWithNavGroup = Object.values(navGroupMap).flatMap(({ navLinks: links }) => + links.map(({ id }) => id) + ); + + if (appId && !appIdsWithNavGroup.includes(appId)) { setCurrentNavGroup(undefined); } }); + const setCurrentNavGroupSorted$ = combineLatest([ + this.getSortedNavGroupsMap$(), + this.currentNavGroup$, + ]) + .pipe(takeUntil(this.stop$)) + .pipe( + map(([navGroupsMapSorted, currentNavGroup]) => { + if (currentNavGroup) { + return navGroupsMapSorted[currentNavGroup.id]; + } + }) + ); + return { getNavGroupsMap$: () => this.getSortedNavGroupsMap$(), getNavGroupEnabled: () => this.navGroupEnabled, - getCurrentNavGroup$: () => this.currentNavGroup$, + getCurrentNavGroup$: () => setCurrentNavGroupSorted$, setCurrentNavGroup, - - prependCurrentNavGroupToBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[]) => { - const navGroupId = this.currentNavGroup$.getValue()?.id; - const homeTitle = i18n.translate('core.breadcrumbs.homeTitle', { defaultMessage: 'Home' }); - // home page will not have nav group information - - if (this.navGroupEnabled && navGroupId) { - const currentNavGroup = this.navGroupsMap$.getValue()[navGroupId]; - - // breadcrumb order is home > navgroup > application, navgroup will be second one - const navGroupInBreadcrumbs = - breadcrumbs.length > 1 && breadcrumbs[1]?.text === currentNavGroup.title; - if (!navGroupInBreadcrumbs) { - const navGroupBreadcrumb: ChromeBreadcrumb = { - text: currentNavGroup.title, - onClick: () => { - if (currentNavGroup.navLinks && currentNavGroup.navLinks.length) { - const orderedLinks = this.sortNavGroupNavLinks( - currentNavGroup, - navLinks.getAll() - ); - application.navigateToApp(orderedLinks[0].id); - } - }, - }; - const homeBreadcrumb: ChromeBreadcrumb = { - text: homeTitle, - onClick: () => { - application.navigateToApp('home'); - }, - }; - return [homeBreadcrumb, navGroupBreadcrumb, ...breadcrumbs]; - } - } - return breadcrumbs; - }, }; } diff --git a/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap index 7345d89ab58..f1c4606939e 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap @@ -2,42 +2,203 @@ exports[`HeaderBreadcrumbs prepend current nav group into existing breadcrumbs when nav group is enabled 1`] = ` Array [ - - Analytics - , - + + + + + , +
- First - , - + + + + +
, +
- Second - , + + + First + + +
, ] `; exports[`HeaderBreadcrumbs prepend current nav group into existing breadcrumbs when nav group is enabled 2`] = ` - - Analytics - +Array [ +
+ + + + + +
, +
+ + + + + +
, +
+ + + First + + +
, +
+ + + Second + + +
, +] +`; + +exports[`HeaderBreadcrumbs prepend current nav group into existing breadcrumbs when nav group is enabled 3`] = ` +Array [ +
+ + + + + +
, +
+ + + + + +
, +] `; exports[`HeaderBreadcrumbs renders updates to the breadcrumbs$ observable 1`] = ` diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 3ef001fc02f..7fd2b8d8208 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -44,7 +44,6 @@ import classnames from 'classnames'; import React, { createRef, useMemo, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; -import { ChromeNavGroup } from 'opensearch-dashboards/public'; import { LoadingIndicator } from '../'; import { ChromeBadge, @@ -67,6 +66,7 @@ import { HeaderActionMenu } from './header_action_menu'; import { HeaderLogo } from './header_logo'; import type { Logos } from '../../../../common/types'; import { ISidecarConfig, getOsdSidecarPaddingStyle } from '../../../overlays'; +import { NavGroupItemInMap } from '../../nav_group'; export interface HeaderProps { opensearchDashboardsVersion: string; application: InternalApplicationStart; @@ -97,8 +97,7 @@ export interface HeaderProps { survey: string | undefined; sidecarConfig$: Observable; navGroupEnabled: boolean; - currentNavgroup$: Observable; - prependCurrentNavgroupToBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[]) => ChromeBreadcrumb[]; + currentNavgroup$: Observable; } export function Header({ @@ -113,7 +112,6 @@ export function Header({ logos, collapsibleNavHeaderRender, navGroupEnabled, - prependCurrentNavgroupToBreadcrumbs, ...observables }: HeaderProps) { const isVisible = useObservable(observables.isVisible$, false); @@ -228,8 +226,8 @@ export function Header({ appTitle$={observables.appTitle$} breadcrumbs$={observables.breadcrumbs$} currentNavgroup$={observables.currentNavgroup$} - navgroupEnabled={navGroupEnabled} - prependCurrentNavgroupToBreadcrumbs={prependCurrentNavgroupToBreadcrumbs} + navGroupEnabled={navGroupEnabled} + navigateToApp={application.navigateToApp} /> diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx index c0fe8eaf311..2826ea644e0 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx @@ -39,11 +39,11 @@ describe('HeaderBreadcrumbs', () => { const breadcrumbs$ = new BehaviorSubject([{ text: 'First' }]); const wrapper = mount( ); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); @@ -63,32 +63,31 @@ describe('HeaderBreadcrumbs', () => { id: 'analytics', title: 'Analytics', description: '', + navLinks: [], }); const wrapper = mount( [ - { text: currentNavgroup$.getValue().title }, - ...breadcrumbs, - ]} appTitle$={new BehaviorSubject('')} breadcrumbs$={breadcrumbs$} + navGroupEnabled={true} + currentNavgroup$={currentNavgroup$} + navigateToApp={jest.fn()} /> ); - const breadcrumbs = wrapper.find('.euiBreadcrumb'); - expect(breadcrumbs).toHaveLength(2); - expect(breadcrumbs.at(0).text()).toBe('Analytics'); - expect(breadcrumbs.at(1).text()).toBe('First'); + const breadcrumbs = wrapper.find('.euiBreadcrumbWrapper'); + expect(breadcrumbs).toHaveLength(3); + expect(breadcrumbs.at(0).text()).toBe('Home'); + expect(breadcrumbs.at(1).text()).toBe('Analytics'); + expect(breadcrumbs.at(2).text()).toBe('First'); act(() => breadcrumbs$.next([{ text: 'First' }, { text: 'Second' }])); wrapper.update(); - expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); - expect(wrapper.find('.euiBreadcrumb')).toHaveLength(3); + expect(wrapper.find('.euiBreadcrumbWrapper')).toMatchSnapshot(); + expect(wrapper.find('.euiBreadcrumbWrapper')).toHaveLength(4); act(() => breadcrumbs$.next([])); wrapper.update(); - expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); - expect(wrapper.find('.euiBreadcrumb')).toHaveLength(1); + expect(wrapper.find('.euiBreadcrumbWrapper')).toMatchSnapshot(); + expect(wrapper.find('.euiBreadcrumbWrapper')).toHaveLength(2); }); }); diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx index ff056a95b67..9f466970697 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx @@ -31,28 +31,64 @@ import { EuiHeaderBreadcrumbs } from '@elastic/eui'; import classNames from 'classnames'; import React from 'react'; +import { i18n } from '@osd/i18n'; import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; -import { ChromeNavGroup } from 'opensearch-dashboards/public'; +import { ApplicationStart } from 'src/core/public/application'; import { ChromeBreadcrumb } from '../../chrome_service'; +import { NavGroupItemInMap } from '../../nav_group'; interface Props { appTitle$: Observable; breadcrumbs$: Observable; - navgroupEnabled: boolean; - currentNavgroup$: Observable; - prependCurrentNavgroupToBreadcrumbs: ( - breadcrumbs: ChromeBreadcrumb[], - appId?: string - ) => ChromeBreadcrumb[]; + navGroupEnabled: boolean; + currentNavgroup$: Observable; + navigateToApp: ApplicationStart['navigateToApp']; +} + +/** + * prepend current nav group into existing breadcrumbs and return new breadcrumbs, the new breadcrumbs will looks like + * Home > Search > Visusalization + * @param breadcrumbs existing breadcrumbs + * @param currentNavGroup current nav group object + * @param navigateToApp + * @returns new breadcrumbs array + */ +function prependCurrentNavGroupToBreadcrumbs( + breadcrumbs: ChromeBreadcrumb[], + currentNavGroup: NavGroupItemInMap, + navigateToApp: ApplicationStart['navigateToApp'] +) { + // breadcrumb order is home > navgroup > application, navgroup will be second one + const navGroupInBreadcrumbs = + breadcrumbs.length > 1 && breadcrumbs[1]?.text === currentNavGroup.title; + if (!navGroupInBreadcrumbs) { + const navGroupBreadcrumb: ChromeBreadcrumb = { + text: currentNavGroup.title, + onClick: () => { + if (currentNavGroup.navLinks && currentNavGroup.navLinks.length) { + navigateToApp(currentNavGroup.navLinks[0].id); + } + }, + }; + const homeBreadcrumb: ChromeBreadcrumb = { + text: i18n.translate('core.breadcrumbs.homeTitle', { defaultMessage: 'Home' }), + onClick: () => { + navigateToApp('home'); + }, + }; + return [homeBreadcrumb, navGroupBreadcrumb, ...breadcrumbs]; + } + + return breadcrumbs; } export function HeaderBreadcrumbs({ appTitle$, breadcrumbs$, - navgroupEnabled, + navGroupEnabled, currentNavgroup$, - prependCurrentNavgroupToBreadcrumbs, + navigateToApp, }: Props) { const appTitle = useObservable(appTitle$, 'OpenSearch Dashboards'); const breadcrumbs = useObservable(breadcrumbs$, []); @@ -63,8 +99,8 @@ export function HeaderBreadcrumbs({ } const currentNavgroup = useObservable(currentNavgroup$, undefined); - if (navgroupEnabled && currentNavgroup) { - crumbs = prependCurrentNavgroupToBreadcrumbs(crumbs); + if (navGroupEnabled && currentNavgroup) { + crumbs = prependCurrentNavGroupToBreadcrumbs(crumbs, currentNavgroup, navigateToApp); } crumbs = crumbs.map((breadcrumb, i) => ({ @@ -73,7 +109,7 @@ export function HeaderBreadcrumbs({ 'breadcrumb', breadcrumb['data-test-subj'], i === 0 && 'first', - i === breadcrumbs.length - 1 && 'last' + i === crumbs.length - 1 && 'last' ), })); From b296c399b5f9207d6f1a8b72fec5f23c257634aa Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Thu, 4 Jul 2024 13:17:32 +0800 Subject: [PATCH 9/9] update snapshot Signed-off-by: Hailong Cui --- .../chrome/nav_group/nav_group_service.ts | 4 +- .../header/__snapshots__/header.test.tsx.snap | 38 +- .../header_breadcrumbs.test.tsx.snap | 69 +- .../public/chrome/ui/header/header.test.tsx | 2 - .../dashboard_listing.test.tsx.snap | 8208 +---------------- .../dashboard_top_nav.test.tsx.snap | 6 - 6 files changed, 15 insertions(+), 8312 deletions(-) diff --git a/src/core/public/chrome/nav_group/nav_group_service.ts b/src/core/public/chrome/nav_group/nav_group_service.ts index d8c4263911d..72d8ef2deb0 100644 --- a/src/core/public/chrome/nav_group/nav_group_service.ts +++ b/src/core/public/chrome/nav_group/nav_group_service.ts @@ -219,7 +219,7 @@ export class ChromeNavGroupService { } }); - const setCurrentNavGroupSorted$ = combineLatest([ + const currentNavGroupSorted$ = combineLatest([ this.getSortedNavGroupsMap$(), this.currentNavGroup$, ]) @@ -236,7 +236,7 @@ export class ChromeNavGroupService { getNavGroupsMap$: () => this.getSortedNavGroupsMap$(), getNavGroupEnabled: () => this.navGroupEnabled, - getCurrentNavGroup$: () => setCurrentNavGroupSorted$, + getCurrentNavGroup$: () => currentNavGroupSorted$, setCurrentNavGroup, }; } diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 871ec274779..8fa25680f3a 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -1789,17 +1789,6 @@ exports[`Header handles visibility and lock changes 1`] = ` } } navGroupEnabled={false} - navGroupsMap$={ - BehaviorSubject { - "_isScalar": false, - "_value": Object {}, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [], - "thrownError": null, - } - } navLinks$={ BehaviorSubject { "_isScalar": false, @@ -1933,7 +1922,6 @@ exports[`Header handles visibility and lock changes 1`] = ` onIsLockedUpdate={[Function]} opensearchDashboardsDocLink="/docs" opensearchDashboardsVersion="1.0.0" - prependCurrentNavgroupToBreadcrumbs={[MockFunction]} recentlyAccessed$={ BehaviorSubject { "_isScalar": false, @@ -4052,8 +4040,8 @@ exports[`Header handles visibility and lock changes 1`] = ` "thrownError": null, } } - navgroupEnabled={false} - prependCurrentNavgroupToBreadcrumbs={[MockFunction]} + navGroupEnabled={false} + navigateToApp={[MockFunction]} > test diff --git a/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap index f1c4606939e..93aee40a7d4 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap @@ -1,73 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`HeaderBreadcrumbs prepend current nav group into existing breadcrumbs when nav group is enabled 1`] = ` -Array [ -
- - - - - -
, -
- - - - - -
, -
- - - First - - -
, -] -`; - -exports[`HeaderBreadcrumbs prepend current nav group into existing breadcrumbs when nav group is enabled 2`] = ` Array [
- - -
-
- -
- -
- -

- Dashboards -

-
-
-
-
-
- -
- - - } - pagination={ - Object { - "initialPageIndex": 0, - "initialPageSize": 10, - "pageSizeOptions": Array [ - 10, - 20, - 50, - ], - } - } - responsive={true} - search={ - Object { - "box": Object { - "incremental": true, - }, - "defaultQuery": "", - "onChange": [Function], - "toolsLeft": undefined, - } - } - sorting={true} - tableLayout="fixed" - > -
- - -
- -
- - - -
-
- - - - -
- - - - - -
-
-
-
-
-
-
-
-
-
-
-
- -
- - - } - onChange={[Function]} - pagination={ - Object { - "hidePerPageOptions": undefined, - "pageIndex": 0, - "pageSize": 10, - "pageSizeOptions": Array [ - 10, - 20, - 50, - ], - "totalItemCount": 2, - } - } - responsive={true} - sorting={ - Object { - "allowNeutralSort": true, - "sort": undefined, - } - } - tableLayout="fixed" - > -
-
- -
- -
- -
- - -
- -
- - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
-
-
-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - -
-
- Title -
-
- - - -
-
-
- Type -
-
- - dashboardSavedObjects - -
-
-
- Description -
-
- - dashboard0 desc - -
-
-
- Last updated -
-
-
-
- Title -
-
- - - -
-
-
- Type -
-
- - dashboardSavedObjects - -
-
-
- Description -
-
- - dashboard1 desc - -
-
-
- Last updated -
-
-
-
-
- -
- -
- - - -
- -
- - - : - 10 - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
-
- -
- - - -
-
-
-
-
-
- -
- -
- -
-
- - -
+ />
@@ -2876,7 +1405,6 @@ exports[`dashboard listing render table listing with initial filters from URL 1` "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { @@ -3881,2151 +2409,11 @@ exports[`dashboard listing render table listing with initial filters from URL 1` data-test-subj="dashboardLandingPage" >
- - -
-
- -
- -
- -

- Dashboards -

-
-
-
- - -
- - - - - -
-
-
-
-
- -
- - - } - pagination={ - Object { - "initialPageIndex": 0, - "initialPageSize": 10, - "pageSizeOptions": Array [ - 10, - 20, - 50, - ], - } - } - responsive={true} - search={ - Object { - "box": Object { - "incremental": true, - }, - "defaultQuery": "dashboard", - "onChange": [Function], - "toolsLeft": undefined, - } - } - selection={ - Object { - "onSelectionChange": [Function], - } - } - sorting={true} - tableLayout="fixed" - > -
- - -
- -
- - - -
-
- - - - -
- - - - - -
-
- - - - - -
-
-
-
-
-
-
-
-
-
-
-
- -
- - - } - onChange={[Function]} - pagination={ - Object { - "hidePerPageOptions": undefined, - "pageIndex": 0, - "pageSize": 10, - "pageSizeOptions": Array [ - 10, - 20, - 50, - ], - "totalItemCount": 2, - } - } - responsive={true} - selection={ - Object { - "onSelectionChange": [Function], - } - } - sorting={ - Object { - "allowNeutralSort": true, - "sort": undefined, - } - } - tableLayout="fixed" - > -
-
- -
- -
- -
- - -
- -
- -
- - -
- - -
- -
- - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
-
-
-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- - -
- -
-
- - -
-
- - - - - - - - - - - - - - Actions - - - - - -
-
- - -
- -
-
- - -
-
-
- Title -
-
- - - -
-
-
- Type -
-
- - dashboardSavedObjects - -
-
-
- Description -
-
- - dashboard0 desc - -
-
-
- Last updated -
-
-
-
- - - - - - - - - - Edit - - - - - - -
-
-
- - -
- -
-
- - -
-
-
- Title -
-
- - - -
-
-
- Type -
-
- - dashboardSavedObjects - -
-
-
- Description -
-
- - dashboard1 desc - -
-
-
- Last updated -
-
-
-
- - - - - - - - - - Edit - - - - - - -
-
-
-
- -
- -
- - - -
- -
- - - : - 10 - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
-
- -
- - - -
-
-
-
-
-
- -
- -
- -
-
- - -
+ />
@@ -6260,7 +2648,6 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { @@ -7265,274 +3652,11 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = data-test-subj="dashboardLandingPage" >
- - -
- - - - } - body={ - -

- -

-

- - - , - } - } - /> -

-
- } - iconType="dashboardApp" - title={ -

- -

- } - > -
- - - - -
- - -

- - Create your first dashboard - -

-
- - - -
- - -
-

- - You can combine data views from any OpenSearch Dashboards app into one dashboard and see everything in one place. - -

-

- - - , - } - } - > - New to OpenSearch Dashboards? - - - - to take a test drive. - -

-
-
- - - -
- - - - - - -
- -
- - -
+ />
@@ -7767,7 +3891,6 @@ exports[`dashboard listing renders table rows 1`] = ` "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { @@ -8772,2111 +4895,11 @@ exports[`dashboard listing renders table rows 1`] = ` data-test-subj="dashboardLandingPage" >
- - -
-
- -
- -
- -

- Dashboards -

-
-
-
- - -
- - - - - -
-
-
-
-
- -
- - - } - pagination={ - Object { - "initialPageIndex": 0, - "initialPageSize": 10, - "pageSizeOptions": Array [ - 10, - 20, - 50, - ], - } - } - responsive={true} - search={ - Object { - "box": Object { - "incremental": true, - }, - "defaultQuery": "", - "onChange": [Function], - "toolsLeft": undefined, - } - } - selection={ - Object { - "onSelectionChange": [Function], - } - } - sorting={true} - tableLayout="fixed" - > -
- - -
- -
- - - -
-
- - - - -
- - - - - -
-
-
-
-
-
-
-
-
-
-
-
- -
- - - } - onChange={[Function]} - pagination={ - Object { - "hidePerPageOptions": undefined, - "pageIndex": 0, - "pageSize": 10, - "pageSizeOptions": Array [ - 10, - 20, - 50, - ], - "totalItemCount": 2, - } - } - responsive={true} - selection={ - Object { - "onSelectionChange": [Function], - } - } - sorting={ - Object { - "allowNeutralSort": true, - "sort": undefined, - } - } - tableLayout="fixed" - > -
-
- -
- -
- -
- - -
- -
- -
- - -
- - -
- -
- - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
-
-
-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- - -
- -
-
- - -
-
- - - - - - - - - - - - - - Actions - - - - - -
-
- - -
- -
-
- - -
-
-
- Title -
-
- - - -
-
-
- Type -
-
- - dashboardSavedObjects - -
-
-
- Description -
-
- - dashboard0 desc - -
-
-
- Last updated -
-
-
-
- - - - - - - - - - Edit - - - - - - -
-
-
- - -
- -
-
- - -
-
-
- Title -
-
- - - -
-
-
- Type -
-
- - dashboardSavedObjects - -
-
-
- Description -
-
- - dashboard1 desc - -
-
-
- Last updated -
-
-
-
- - - - - - - - - - Edit - - - - - - -
-
-
-
- -
- -
- - - -
- -
- - - : - 10 - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
-
- -
- - - -
-
-
-
-
-
- -
- -
- -
-
- - -
+ />
@@ -11111,7 +5134,6 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { @@ -12116,2231 +6138,11 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` data-test-subj="dashboardLandingPage" >
- - -
-
- -
- -
- -

- Dashboards -

-
-
-
- - -
- - - - - -
-
-
-
-
- -
- - - } - > -
-
- - - - Listing limit exceeded - - -
- -
- -
-

- - - , - "entityNamePlural": "dashboards", - "listingLimitText": - listingLimit - , - "listingLimitValue": 1, - "totalItems": 2, - } - } - > - You have 2 dashboards, but your - - listingLimit - - setting prevents the table below from displaying more than 1. You can change this setting under - - - - Advanced Settings - - - - . - -

-
-
-
-
-
-
- -
- - - } - pagination={ - Object { - "initialPageIndex": 0, - "initialPageSize": 10, - "pageSizeOptions": Array [ - 10, - 20, - 50, - ], - } - } - responsive={true} - search={ - Object { - "box": Object { - "incremental": true, - }, - "defaultQuery": "", - "onChange": [Function], - "toolsLeft": undefined, - } - } - selection={ - Object { - "onSelectionChange": [Function], - } - } - sorting={true} - tableLayout="fixed" - > -
- - -
- -
- - - -
-
- - - - -
- - - - - -
-
-
-
-
-
-
-
-
-
-
-
- -
- - - } - onChange={[Function]} - pagination={ - Object { - "hidePerPageOptions": undefined, - "pageIndex": 0, - "pageSize": 10, - "pageSizeOptions": Array [ - 10, - 20, - 50, - ], - "totalItemCount": 2, - } - } - responsive={true} - selection={ - Object { - "onSelectionChange": [Function], - } - } - sorting={ - Object { - "allowNeutralSort": true, - "sort": undefined, - } - } - tableLayout="fixed" - > -
-
- -
- -
- -
- - -
- -
- -
- - -
- - -
- -
- - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
-
-
-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- - -
- -
-
- - -
-
- - - - - - - - - - - - - - Actions - - - - - -
-
- - -
- -
-
- - -
-
-
- Title -
-
- - - -
-
-
- Type -
-
- - dashboardSavedObjects - -
-
-
- Description -
-
- - dashboard0 desc - -
-
-
- Last updated -
-
-
-
- - - - - - - - - - Edit - - - - - - -
-
-
- - -
- -
-
- - -
-
-
- Title -
-
- - - -
-
-
- Type -
-
- - dashboardSavedObjects - -
-
-
- Description -
-
- - dashboard1 desc - -
-
-
- Last updated -
-
-
-
- - - - - - - - - - Edit - - - - - - -
-
-
-
- -
- -
- - - -
- -
- - - : - 10 - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
-
- -
- - - -
-
-
-
-
-
- -
- -
- -
-
- - -
+ />
diff --git a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap index 5a3643f7f59..72303e9ca32 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap @@ -211,7 +211,6 @@ exports[`Dashboard top nav render in embed mode 1`] = ` "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { @@ -1220,7 +1219,6 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { @@ -2229,7 +2227,6 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { @@ -3238,7 +3235,6 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { @@ -4247,7 +4243,6 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object { @@ -5256,7 +5251,6 @@ exports[`Dashboard top nav render with all components 1`] = ` "getCurrentNavGroup$": [MockFunction], "getNavGroupEnabled": [MockFunction], "getNavGroupsMap$": [MockFunction], - "prependCurrentNavGroupToBreadcrumbs": [MockFunction], "setCurrentNavGroup": [MockFunction], }, "navLinks": Object {