From 5922b6acd586aa918c71a018cf5330831b130e77 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Tue, 23 Jul 2024 15:43:02 +0800 Subject: [PATCH] [Navigation-next] Enrich breadcrumbs by workspace and use case (#7360) (#7393) * breadcrumbs for workspace * add unit test * Changeset file for PR #7360 created/updated * add unit test --------- (cherry picked from commit 7ff854413eea1c17a98a79b2a57682194207e45f) Signed-off-by: Hailong Cui Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/7360.yml | 2 + src/core/public/chrome/chrome_service.mock.ts | 2 + src/core/public/chrome/chrome_service.tsx | 30 ++- .../nav_group/nav_group_service.test.ts | 214 +++++++++++++++++- .../chrome/nav_group/nav_group_service.ts | 96 +++++++- .../header/__snapshots__/header.test.tsx.snap | 34 ++- .../header_breadcrumbs.test.tsx.snap | 134 ----------- .../public/chrome/ui/header/header.test.tsx | 1 + src/core/public/chrome/ui/header/header.tsx | 11 +- .../ui/header/header_breadcrumbs.test.tsx | 22 +- .../chrome/ui/header/header_breadcrumbs.tsx | 72 ++---- .../dashboard_listing.test.tsx.snap | 10 + .../dashboard_top_nav.test.tsx.snap | 12 + src/plugins/workspace/public/plugin.ts | 4 + src/plugins/workspace/public/utils.test.ts | 146 +++++++++++- src/plugins/workspace/public/utils.ts | 79 ++++++- 16 files changed, 646 insertions(+), 223 deletions(-) create mode 100644 changelogs/fragments/7360.yml diff --git a/changelogs/fragments/7360.yml b/changelogs/fragments/7360.yml new file mode 100644 index 000000000000..16f7cd2cdb4a --- /dev/null +++ b/changelogs/fragments/7360.yml @@ -0,0 +1,2 @@ +feat: +- Enrich breadcrumbs by workspace and use case ([#7360](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7360)) \ 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 446b04d4d8b1..8e8d8c893cc9 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -93,6 +93,8 @@ const createStartContractMock = () => { setBadge: jest.fn(), getBreadcrumbs$: jest.fn(), setBreadcrumbs: jest.fn(), + getBreadcrumbsEnricher$: jest.fn(), + setBreadcrumbsEnricher: jest.fn(), getHelpExtension$: jest.fn(), setHelpExtension: jest.fn(), setHelpSupportUrl: jest.fn(), diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 8365247201e5..7c4a33769b8e 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -73,6 +73,9 @@ export interface ChromeBadge { /** @public */ export type ChromeBreadcrumb = EuiBreadcrumb; +/** @public */ +export type ChromeBreadcrumbEnricher = (breadcrumbs: ChromeBreadcrumb[]) => ChromeBreadcrumb[]; + /** @public */ export type ChromeBranding = Branding; @@ -191,6 +194,9 @@ export class ChromeService { const applicationClasses$ = new BehaviorSubject>(new Set()); const helpExtension$ = new BehaviorSubject(undefined); const breadcrumbs$ = new BehaviorSubject([]); + const breadcrumbsEnricher$ = new BehaviorSubject( + undefined + ); const badge$ = new BehaviorSubject(undefined); const customNavLink$ = new BehaviorSubject(undefined); const helpSupportUrl$ = new BehaviorSubject(OPENSEARCH_DASHBOARDS_ASK_OPENSEARCH_LINK); @@ -201,7 +207,12 @@ export class ChromeService { const navLinks = this.navLinks.start({ application, http }); const recentlyAccessed = await this.recentlyAccessed.start({ http, workspaces }); const docTitle = this.docTitle.start({ document: window.document }); - const navGroup = await this.navGroup.start({ navLinks, application }); + const navGroup = await this.navGroup.start({ + navLinks, + application, + breadcrumbsEnricher$, + workspaces, + }); // erase chrome fields from a previous app while switching to a next app application.currentAppId$.subscribe(() => { @@ -280,6 +291,7 @@ export class ChromeService { badge$={badge$.pipe(takeUntil(this.stop$))} basePath={http.basePath} breadcrumbs$={breadcrumbs$.pipe(takeUntil(this.stop$))} + breadcrumbsEnricher$={breadcrumbsEnricher$.pipe(takeUntil(this.stop$))} customNavLink$={customNavLink$.pipe(takeUntil(this.stop$))} opensearchDashboardsDocLink={docLinks.links.opensearchDashboards.introduction} forceAppSwitcherNavigation$={navLinks.getForceAppSwitcherNavigation$()} @@ -347,6 +359,12 @@ export class ChromeService { breadcrumbs$.next(newBreadcrumbs); }, + getBreadcrumbsEnricher$: () => breadcrumbsEnricher$.pipe(takeUntil(this.stop$)), + + setBreadcrumbsEnricher: (enricher: ChromeBreadcrumbEnricher) => { + breadcrumbsEnricher$.next(enricher); + }, + getHelpExtension$: () => helpExtension$.pipe(takeUntil(this.stop$)), setHelpExtension: (helpExtension?: ChromeHelpExtension) => { @@ -483,6 +501,16 @@ export interface ChromeStart { */ setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void; + /** + * Get an observable of the current breadcrumbs enricher + */ + getBreadcrumbsEnricher$(): Observable; + + /** + * Override the current ChromeBreadcrumbEnricher + */ + setBreadcrumbsEnricher(newBreadcrumbsEnricher: ChromeBreadcrumbEnricher | undefined): void; + /** * Get an observable of the current custom nav link */ 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 90911309ff9a..91a6b2a0a6de 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 @@ -12,9 +12,10 @@ import { } from './nav_group_service'; import { uiSettingsServiceMock } from '../../ui_settings/ui_settings_service.mock'; import { NavLinksService } from '../nav_links'; -import { applicationServiceMock, httpServiceMock } from '../../mocks'; +import { applicationServiceMock, httpServiceMock, workspacesServiceMock } from '../../mocks'; import { AppCategory } from 'opensearch-dashboards/public'; import { DEFAULT_NAV_GROUPS } from '../../'; +import { ChromeBreadcrumbEnricher } from '../chrome_service'; const mockedGroupFoo = { id: 'foo', @@ -52,6 +53,7 @@ const mockedCategoryBar: AppCategory = { const mockedHttpService = httpServiceMock.createStartContract(); const mockedApplicationService = applicationServiceMock.createInternalStartContract(); +const mockWorkspaceService = workspacesServiceMock.createStartContract(); const mockedNavLink = new NavLinksService(); const mockedNavLinkService = mockedNavLink.start({ http: mockedHttpService, @@ -124,6 +126,8 @@ describe('ChromeNavGroupService#setup()', () => { const chromeNavGroupServiceStart = await chromeNavGroupService.start({ navLinks: mockedNavLinkService, application: mockedApplicationService, + breadcrumbsEnricher$: new Rx.BehaviorSubject(undefined), + workspaces: workspacesServiceMock.createStartContract(), }); const groupsMap = await chromeNavGroupServiceStart.getNavGroupsMap$().pipe(first()).toPromise(); expect(groupsMap[mockedGroupFoo.id].navLinks.length).toEqual(2); @@ -147,6 +151,8 @@ describe('ChromeNavGroupService#setup()', () => { const chromeNavGroupServiceStart = await chromeNavGroupService.start({ navLinks: mockedNavLinkService, application: mockedApplicationService, + breadcrumbsEnricher$: new Rx.BehaviorSubject(undefined), + workspaces: workspacesServiceMock.createStartContract(), }); const groupsMap = await chromeNavGroupServiceStart.getNavGroupsMap$().pipe(first()).toPromise(); expect(groupsMap[mockedGroupFoo.id].navLinks.length).toEqual(1); @@ -206,6 +212,8 @@ describe('ChromeNavGroupService#start()', () => { const chromeStart = await chromeNavGroupService.start({ navLinks: mockedNavLinkService, application: mockedApplicationService, + breadcrumbsEnricher$: new Rx.BehaviorSubject(undefined), + workspaces: workspacesServiceMock.createStartContract(), }); const groupsMap = await chromeStart.getNavGroupsMap$().pipe(first()).toPromise(); @@ -231,6 +239,8 @@ describe('ChromeNavGroupService#start()', () => { const chromeNavGroupServiceStart = await chromeNavGroupService.start({ navLinks: mockedNavLinkService, application: mockedApplicationService, + breadcrumbsEnricher$: new Rx.BehaviorSubject(undefined), + workspaces: workspacesServiceMock.createStartContract(), }); expect(chromeNavGroupServiceStart.getNavGroupEnabled()).toBe(true); @@ -246,6 +256,8 @@ describe('ChromeNavGroupService#start()', () => { const chromeNavGroupServiceStart = await chromeNavGroupService.start({ navLinks: mockedNavLinkService, application: mockedApplicationService, + breadcrumbsEnricher$: new Rx.BehaviorSubject(undefined), + workspaces: workspacesServiceMock.createStartContract(), }); navGroupEnabled$.next(false); @@ -276,6 +288,8 @@ describe('ChromeNavGroupService#start()', () => { const chromeNavGroupServiceStart = await chromeNavGroupService.start({ navLinks: mockedNavLinkService, application: mockedApplicationService, + breadcrumbsEnricher$: new Rx.BehaviorSubject(undefined), + workspaces: workspacesServiceMock.createStartContract(), }); // set an existing nav group id @@ -311,6 +325,200 @@ describe('ChromeNavGroupService#start()', () => { expect(sessionStorageMock.getItem(CURRENT_NAV_GROUP_ID)).toBeFalsy(); expect(currentNavGroup).toBeUndefined(); }); + + it('should set current nav group automatically if application only belongs 1 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-group', + title: 'fooGroupTitle', + description: 'foo description', + }, + [mockedNavLinkFoo] + ); + + chromeNavGroupServiceSetup.addNavLinksToGroup( + { + id: 'bar-group', + title: 'barGroupTitle', + description: 'bar description', + }, + [mockedNavLinkFoo, mockedNavLinkBar] + ); + + const chromeNavGroupServiceStart = await chromeNavGroupService.start({ + navLinks: mockedNavLinkService, + application: mockedApplicationService, + breadcrumbsEnricher$: new Rx.BehaviorSubject(undefined), + workspaces: workspacesServiceMock.createStartContract(), + }); + + mockedApplicationService.navigateToApp(mockedNavLinkFoo.id); + let currentNavGroup = await chromeNavGroupServiceStart + .getCurrentNavGroup$() + .pipe(first()) + .toPromise(); + + expect(currentNavGroup).toBeFalsy(); + + // reset + chromeNavGroupServiceStart.setCurrentNavGroup(undefined); + + mockedApplicationService.navigateToApp(mockedNavLinkBar.id); + + currentNavGroup = await chromeNavGroupServiceStart + .getCurrentNavGroup$() + .pipe(first()) + .toPromise(); + + expect(currentNavGroup?.id).toEqual('bar-group'); + expect(currentNavGroup?.title).toEqual('barGroupTitle'); + }); + + it('should erase current nav group if application is home', 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-group', + title: 'fooGroupTitle', + description: 'foo description', + }, + [mockedNavLinkFoo] + ); + + chromeNavGroupServiceSetup.addNavLinksToGroup( + { + id: 'bar-group', + title: 'barGroupTitle', + description: 'bar description', + }, + [mockedNavLinkFoo, mockedNavLinkBar] + ); + + const chromeNavGroupServiceStart = await chromeNavGroupService.start({ + navLinks: mockedNavLinkService, + application: mockedApplicationService, + breadcrumbsEnricher$: new Rx.BehaviorSubject(undefined), + workspaces: workspacesServiceMock.createStartContract(), + }); + + chromeNavGroupServiceStart.setCurrentNavGroup('foo-group'); + + mockedApplicationService.navigateToApp('home'); + const currentNavGroup = await chromeNavGroupServiceStart + .getCurrentNavGroup$() + .pipe(first()) + .toPromise(); + + expect(currentNavGroup).toBeFalsy(); + }); + + it('should set breadcrumbs enricher when nav group is enabled', 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-group', + title: 'fooGroupTitle', + description: 'foo description', + }, + [mockedNavLinkFoo] + ); + + chromeNavGroupServiceSetup.addNavLinksToGroup( + { + id: 'bar-group', + title: 'barGroupTitle', + description: 'bar description', + }, + [mockedNavLinkFoo, mockedNavLinkBar] + ); + + const breadcrumbsEnricher$ = new Rx.BehaviorSubject( + undefined + ); + + const chromeNavGroupServiceStart = await chromeNavGroupService.start({ + navLinks: mockedNavLinkService, + application: mockedApplicationService, + breadcrumbsEnricher$, + workspaces: mockWorkspaceService, + }); + + chromeNavGroupServiceStart.setCurrentNavGroup('bar-group'); + + expect(breadcrumbsEnricher$.getValue()).toBeTruthy(); + + const breadcrumbs = [{ text: 'test' }]; + const enrichedBreadcrumbs = breadcrumbsEnricher$.getValue()?.(breadcrumbs); + + // home -> bar-group -> test + expect(enrichedBreadcrumbs).toHaveLength(3); + + // reset current nav group + chromeNavGroupServiceStart.setCurrentNavGroup(undefined); + expect(breadcrumbsEnricher$.getValue()).toBeFalsy(); + }); + + it('should NOT set breadcrumbs enricher when in a workspace', 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-group', + title: 'fooGroupTitle', + description: 'foo description', + }, + [mockedNavLinkFoo] + ); + + chromeNavGroupServiceSetup.addNavLinksToGroup( + { + id: 'bar-group', + title: 'barGroupTitle', + description: 'bar description', + }, + [mockedNavLinkFoo, mockedNavLinkBar] + ); + + const breadcrumbsEnricher$ = new Rx.BehaviorSubject( + undefined + ); + mockWorkspaceService.currentWorkspace$.next({ id: 'test', name: 'test workspace' }); + + const chromeNavGroupServiceStart = await chromeNavGroupService.start({ + navLinks: mockedNavLinkService, + application: mockedApplicationService, + breadcrumbsEnricher$, + workspaces: mockWorkspaceService, + }); + + chromeNavGroupServiceStart.setCurrentNavGroup('bar-group'); + + expect(breadcrumbsEnricher$.getValue()).toBeFalsy(); + }); }); describe('nav group updater', () => { @@ -328,6 +536,8 @@ describe('nav group updater', () => { const navGroupStart = await navGroup.start({ navLinks: mockedNavLinkService, application: mockedApplicationService, + breadcrumbsEnricher$: new Rx.BehaviorSubject(undefined), + workspaces: workspacesServiceMock.createStartContract(), }); expect(await navGroupStart.getNavGroupsMap$().pipe(first()).toPromise()).toEqual({ @@ -365,6 +575,8 @@ describe('nav group updater', () => { const navGroupStart = await navGroup.start({ navLinks: mockedNavLinkService, application: mockedApplicationService, + breadcrumbsEnricher$: new Rx.BehaviorSubject(undefined), + workspaces: workspacesServiceMock.createStartContract(), }); expect(await navGroupStart.getNavGroupsMap$().pipe(first()).toPromise()).toEqual({ dataAdministration: expect.objectContaining({ 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 bdf69b151da9..883eceacb871 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, + ApplicationStart, + ChromeNavGroup, + ChromeNavLink, + WorkspacesStart, +} from 'opensearch-dashboards/public'; import { map, switchMap, takeUntil } from 'rxjs/operators'; +import { i18n } from '@osd/i18n'; import { IUiSettingsClient } from '../../ui_settings'; import { flattenLinksOrCategories, @@ -15,6 +22,7 @@ import { import { ChromeNavLinks } from '../nav_links'; import { InternalApplicationStart } from '../../application'; import { NavGroupStatus } from '../../../../core/types'; +import { ChromeBreadcrumb, ChromeBreadcrumbEnricher } from '../chrome_service'; export const CURRENT_NAV_GROUP_ID = 'core.chrome.currentNavGroupId'; @@ -75,6 +83,8 @@ export class ChromeNavGroupService { private navGroupEnabledUiSettingsSubscription: Subscription | undefined; private navGroupUpdaters$$ = new BehaviorSubject>>([]); private currentNavGroup$ = new BehaviorSubject(undefined); + private currentNavGroupSubscription: Subscription | undefined; + private currentAppIdSubscription: Subscription | undefined; private addNavLinkToGroup( currentGroupsMap: Record, @@ -105,11 +115,11 @@ export class ChromeNavGroupService { private sortNavGroupNavLinks( navGroup: NavGroupItemInMap, - allVaildNavLinks: Array> + allValidNavLinks: Array> ) { return flattenLinksOrCategories( getOrderedLinksOrCategories( - fulfillRegistrationLinksToChromeNavLinks(navGroup.navLinks, allVaildNavLinks) + fulfillRegistrationLinksToChromeNavLinks(navGroup.navLinks, allValidNavLinks) ) ); } @@ -190,9 +200,13 @@ export class ChromeNavGroupService { async start({ navLinks, application, + breadcrumbsEnricher$, + workspaces, }: { navLinks: ChromeNavLinks; application: InternalApplicationStart; + breadcrumbsEnricher$: BehaviorSubject; + workspaces: WorkspacesStart; }): Promise { this.navLinks$ = navLinks.getNavLinks$(); @@ -225,6 +239,50 @@ export class ChromeNavGroupService { }) ); + // when we not in any workspace or workspace is disabled + if (this.navGroupEnabled && !workspaces.currentWorkspace$.getValue()) { + this.currentNavGroupSubscription = currentNavGroupSorted$.subscribe((currentNavGroup) => { + if (currentNavGroup) { + breadcrumbsEnricher$.next((breadcrumbs) => + this.prependCurrentNavGroupToBreadcrumbs( + breadcrumbs, + currentNavGroup, + application.navigateToApp + ) + ); + } else { + breadcrumbsEnricher$.next(undefined); + } + }); + } + + this.currentAppIdSubscription = combineLatest([ + application.currentAppId$, + this.getSortedNavGroupsMap$(), + ]).subscribe(([appId, navGroupMap]) => { + if (appId === 'home') { + setCurrentNavGroup(undefined); + return; + } + if (appId && navGroupMap) { + const appIdNavGroupMap = new Map>(); + // iterate navGroupMap + Object.keys(navGroupMap).forEach((navGroupId) => { + navGroupMap[navGroupId].navLinks.forEach((navLink) => { + const navLinkId = navLink.id; + const navGroupSet = appIdNavGroupMap.get(navLinkId) || new Set(); + navGroupSet.add(navGroupId); + appIdNavGroupMap.set(navLinkId, navGroupSet); + }); + }); + + const navGroups = appIdNavGroupMap.get(appId); + if (navGroups && navGroups.size === 1) { + setCurrentNavGroup(navGroups.values().next().value); + } + } + }); + return { getNavGroupsMap$: () => this.getSortedNavGroupsMap$(), getNavGroupEnabled: () => this.navGroupEnabled, @@ -234,8 +292,40 @@ export class ChromeNavGroupService { }; } + /** + * prepend current nav group into existing breadcrumbs and return new breadcrumbs, the new breadcrumbs will looks like + * Home > Search > Visualization + * @param breadcrumbs existing breadcrumbs + * @param currentNavGroup current nav group object + * @param navigateToApp + * @returns new breadcrumbs array + */ + private prependCurrentNavGroupToBreadcrumbs( + breadcrumbs: ChromeBreadcrumb[], + currentNavGroup: NavGroupItemInMap, + navigateToApp: ApplicationStart['navigateToApp'] + ) { + 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]; + } + async stop() { this.stop$.next(); this.navGroupEnabledUiSettingsSubscription?.unsubscribe(); + this.currentAppIdSubscription?.unsubscribe(); + this.currentNavGroupSubscription?.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 4cc46fd921db..2e6256fe2778 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,7 +304,7 @@ exports[`Header handles visibility and lock changes 1`] = ` "thrownError": null, } } - currentNavGroup$={ + breadcrumbsEnricher$={ BehaviorSubject { "_isScalar": false, "_value": undefined, @@ -353,6 +353,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, @@ -4025,7 +4036,7 @@ exports[`Header handles visibility and lock changes 1`] = ` "thrownError": null, } } - currentNavgroup$={ + breadcrumbsEnricher$={ BehaviorSubject { "_isScalar": false, "_value": undefined, @@ -4074,8 +4085,6 @@ exports[`Header handles visibility and lock changes 1`] = ` "thrownError": null, } } - navGroupEnabled={false} - navigateToApp={[MockFunction]} > - - - - - - , -
- - - - - -
, -
- - - First - - -
, -
- - - Second - - -
, -] -`; - -exports[`HeaderBreadcrumbs prepend current nav group into existing breadcrumbs when nav group is enabled 2`] = ` -Array [ -
- - - - - -
, -
- - - - - -
, -] -`; - exports[`HeaderBreadcrumbs renders updates to the breadcrumbs$ observable 1`] = ` ; badge$: Observable; breadcrumbs$: Observable; + breadcrumbsEnricher$: Observable; collapsibleNavHeaderRender?: () => JSX.Element | null; customNavLink$: Observable; homeHref: string; @@ -246,9 +251,7 @@ 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 2826ea644e0c..ec82658efa21 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx @@ -33,6 +33,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { BehaviorSubject } from 'rxjs'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; +import { ChromeBreadcrumb } from '../../chrome_service'; describe('HeaderBreadcrumbs', () => { it('renders updates to the breadcrumbs$ observable', () => { @@ -41,9 +42,7 @@ describe('HeaderBreadcrumbs', () => { ); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); @@ -59,19 +58,16 @@ describe('HeaderBreadcrumbs', () => { 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: '', - navLinks: [], - }); + const breadcrumbsEnricher$ = new BehaviorSubject((crumbs: ChromeBreadcrumb[]) => [ + { text: 'Home' }, + { text: 'Analytics' }, + ...crumbs, + ]); const wrapper = mount( ); const breadcrumbs = wrapper.find('.euiBreadcrumbWrapper'); @@ -82,12 +78,10 @@ describe('HeaderBreadcrumbs', () => { act(() => breadcrumbs$.next([{ text: 'First' }, { text: 'Second' }])); wrapper.update(); - expect(wrapper.find('.euiBreadcrumbWrapper')).toMatchSnapshot(); expect(wrapper.find('.euiBreadcrumbWrapper')).toHaveLength(4); act(() => breadcrumbs$.next([])); wrapper.update(); - 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 9f466970697f..25f337f56d20 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx @@ -30,77 +30,39 @@ import { EuiHeaderBreadcrumbs } from '@elastic/eui'; import classNames from 'classnames'; -import React from 'react'; -import { i18n } from '@osd/i18n'; +import React, { useEffect, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; -import { ApplicationStart } from 'src/core/public/application'; -import { ChromeBreadcrumb } from '../../chrome_service'; -import { NavGroupItemInMap } from '../../nav_group'; +import { ChromeBreadcrumb, ChromeBreadcrumbEnricher } from '../../chrome_service'; interface Props { appTitle$: Observable; breadcrumbs$: Observable; - navGroupEnabled: boolean; - currentNavgroup$: Observable; - navigateToApp: ApplicationStart['navigateToApp']; + breadcrumbsEnricher$: Observable; } -/** - * 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, - currentNavgroup$, - navigateToApp, -}: Props) { +export function HeaderBreadcrumbs({ appTitle$, breadcrumbs$, breadcrumbsEnricher$ }: Props) { const appTitle = useObservable(appTitle$, 'OpenSearch Dashboards'); const breadcrumbs = useObservable(breadcrumbs$, []); + const [breadcrumbEnricher, setBreadcrumbEnricher] = useState< + ChromeBreadcrumbEnricher | undefined + >(undefined); + + useEffect(() => { + const sub = breadcrumbsEnricher$.subscribe((enricher) => { + setBreadcrumbEnricher(() => enricher); + }); + return () => sub.unsubscribe(); + }); + let crumbs = breadcrumbs; if (breadcrumbs.length === 0 && appTitle) { crumbs = [{ text: appTitle }]; } - const currentNavgroup = useObservable(currentNavgroup$, undefined); - if (navGroupEnabled && currentNavgroup) { - crumbs = prependCurrentNavGroupToBreadcrumbs(crumbs, currentNavgroup, navigateToApp); + if (breadcrumbEnricher) { + crumbs = breadcrumbEnricher(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 68f84dc3394e..1726e357eebf 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 @@ -143,6 +143,7 @@ exports[`dashboard listing hideWriteControls 1`] = ` "getApplicationClasses$": [MockFunction], "getBadge$": [MockFunction], "getBreadcrumbs$": [MockFunction], + "getBreadcrumbsEnricher$": [MockFunction], "getCustomNavLink$": [MockFunction], "getHeaderComponent": [MockFunction], "getHelpExtension$": [MockFunction], @@ -262,6 +263,7 @@ exports[`dashboard listing hideWriteControls 1`] = ` }, ], }, + "setBreadcrumbsEnricher": [MockFunction], "setCustomNavLink": [MockFunction], "setHelpExtension": [MockFunction], "setHelpSupportUrl": [MockFunction], @@ -1331,6 +1333,7 @@ exports[`dashboard listing render table listing with initial filters from URL 1` "getApplicationClasses$": [MockFunction], "getBadge$": [MockFunction], "getBreadcrumbs$": [MockFunction], + "getBreadcrumbsEnricher$": [MockFunction], "getCustomNavLink$": [MockFunction], "getHeaderComponent": [MockFunction], "getHelpExtension$": [MockFunction], @@ -1450,6 +1453,7 @@ exports[`dashboard listing render table listing with initial filters from URL 1` }, ], }, + "setBreadcrumbsEnricher": [MockFunction], "setCustomNavLink": [MockFunction], "setHelpExtension": [MockFunction], "setHelpSupportUrl": [MockFunction], @@ -2580,6 +2584,7 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = "getApplicationClasses$": [MockFunction], "getBadge$": [MockFunction], "getBreadcrumbs$": [MockFunction], + "getBreadcrumbsEnricher$": [MockFunction], "getCustomNavLink$": [MockFunction], "getHeaderComponent": [MockFunction], "getHelpExtension$": [MockFunction], @@ -2699,6 +2704,7 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = }, ], }, + "setBreadcrumbsEnricher": [MockFunction], "setCustomNavLink": [MockFunction], "setHelpExtension": [MockFunction], "setHelpSupportUrl": [MockFunction], @@ -3829,6 +3835,7 @@ exports[`dashboard listing renders table rows 1`] = ` "getApplicationClasses$": [MockFunction], "getBadge$": [MockFunction], "getBreadcrumbs$": [MockFunction], + "getBreadcrumbsEnricher$": [MockFunction], "getCustomNavLink$": [MockFunction], "getHeaderComponent": [MockFunction], "getHelpExtension$": [MockFunction], @@ -3948,6 +3955,7 @@ exports[`dashboard listing renders table rows 1`] = ` }, ], }, + "setBreadcrumbsEnricher": [MockFunction], "setCustomNavLink": [MockFunction], "setHelpExtension": [MockFunction], "setHelpSupportUrl": [MockFunction], @@ -5078,6 +5086,7 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` "getApplicationClasses$": [MockFunction], "getBadge$": [MockFunction], "getBreadcrumbs$": [MockFunction], + "getBreadcrumbsEnricher$": [MockFunction], "getCustomNavLink$": [MockFunction], "getHeaderComponent": [MockFunction], "getHelpExtension$": [MockFunction], @@ -5197,6 +5206,7 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` }, ], }, + "setBreadcrumbsEnricher": [MockFunction], "setCustomNavLink": [MockFunction], "setHelpExtension": [MockFunction], "setHelpSupportUrl": [MockFunction], 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 a48e917ce8e3..65849cc5c453 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 @@ -131,6 +131,7 @@ exports[`Dashboard top nav render in embed mode 1`] = ` "getApplicationClasses$": [MockFunction], "getBadge$": [MockFunction], "getBreadcrumbs$": [MockFunction], + "getBreadcrumbsEnricher$": [MockFunction], "getCustomNavLink$": [MockFunction], "getHeaderComponent": [MockFunction], "getHelpExtension$": [MockFunction], @@ -234,6 +235,7 @@ exports[`Dashboard top nav render in embed mode 1`] = ` "setAppTitle": [MockFunction], "setBadge": [MockFunction], "setBreadcrumbs": [MockFunction], + "setBreadcrumbsEnricher": [MockFunction], "setCustomNavLink": [MockFunction], "setHelpExtension": [MockFunction], "setHelpSupportUrl": [MockFunction], @@ -1141,6 +1143,7 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = "getApplicationClasses$": [MockFunction], "getBadge$": [MockFunction], "getBreadcrumbs$": [MockFunction], + "getBreadcrumbsEnricher$": [MockFunction], "getCustomNavLink$": [MockFunction], "getHeaderComponent": [MockFunction], "getHelpExtension$": [MockFunction], @@ -1244,6 +1247,7 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = "setAppTitle": [MockFunction], "setBadge": [MockFunction], "setBreadcrumbs": [MockFunction], + "setBreadcrumbsEnricher": [MockFunction], "setCustomNavLink": [MockFunction], "setHelpExtension": [MockFunction], "setHelpSupportUrl": [MockFunction], @@ -2151,6 +2155,7 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b "getApplicationClasses$": [MockFunction], "getBadge$": [MockFunction], "getBreadcrumbs$": [MockFunction], + "getBreadcrumbsEnricher$": [MockFunction], "getCustomNavLink$": [MockFunction], "getHeaderComponent": [MockFunction], "getHelpExtension$": [MockFunction], @@ -2254,6 +2259,7 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b "setAppTitle": [MockFunction], "setBadge": [MockFunction], "setBreadcrumbs": [MockFunction], + "setBreadcrumbsEnricher": [MockFunction], "setCustomNavLink": [MockFunction], "setHelpExtension": [MockFunction], "setHelpSupportUrl": [MockFunction], @@ -3161,6 +3167,7 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu "getApplicationClasses$": [MockFunction], "getBadge$": [MockFunction], "getBreadcrumbs$": [MockFunction], + "getBreadcrumbsEnricher$": [MockFunction], "getCustomNavLink$": [MockFunction], "getHeaderComponent": [MockFunction], "getHelpExtension$": [MockFunction], @@ -3264,6 +3271,7 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu "setAppTitle": [MockFunction], "setBadge": [MockFunction], "setBreadcrumbs": [MockFunction], + "setBreadcrumbsEnricher": [MockFunction], "setCustomNavLink": [MockFunction], "setHelpExtension": [MockFunction], "setHelpSupportUrl": [MockFunction], @@ -4171,6 +4179,7 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be "getApplicationClasses$": [MockFunction], "getBadge$": [MockFunction], "getBreadcrumbs$": [MockFunction], + "getBreadcrumbsEnricher$": [MockFunction], "getCustomNavLink$": [MockFunction], "getHeaderComponent": [MockFunction], "getHelpExtension$": [MockFunction], @@ -4274,6 +4283,7 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be "setAppTitle": [MockFunction], "setBadge": [MockFunction], "setBreadcrumbs": [MockFunction], + "setBreadcrumbsEnricher": [MockFunction], "setCustomNavLink": [MockFunction], "setHelpExtension": [MockFunction], "setHelpSupportUrl": [MockFunction], @@ -5181,6 +5191,7 @@ exports[`Dashboard top nav render with all components 1`] = ` "getApplicationClasses$": [MockFunction], "getBadge$": [MockFunction], "getBreadcrumbs$": [MockFunction], + "getBreadcrumbsEnricher$": [MockFunction], "getCustomNavLink$": [MockFunction], "getHeaderComponent": [MockFunction], "getHelpExtension$": [MockFunction], @@ -5284,6 +5295,7 @@ exports[`Dashboard top nav render with all components 1`] = ` "setAppTitle": [MockFunction], "setBadge": [MockFunction], "setBreadcrumbs": [MockFunction], + "setBreadcrumbsEnricher": [MockFunction], "setCustomNavLink": [MockFunction], "setHelpExtension": [MockFunction], "setHelpSupportUrl": [MockFunction], diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 1be221b546ad..846e13cd14ad 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -41,6 +41,7 @@ import { WorkspaceMenu } from './components/workspace_menu/workspace_menu'; import { getWorkspaceColumn } from './components/workspace_column'; import { DataSourceManagementPluginSetup } from '../../../plugins/data_source_management/public'; import { + enrichBreadcrumbsWithWorkspace, filterWorkspaceConfigurableApps, getFirstUseCaseOfFeatureConfigs, isAppAccessibleInWorkspace, @@ -456,6 +457,9 @@ export class WorkspacePlugin // register get started card in new home page this.registerGetStartedCardToNewHome(core, contentManagement); + + // set breadcrumbs enricher for workspace + this.breadcrumbsSubscription = enrichBreadcrumbsWithWorkspace(core); } return {}; } diff --git a/src/plugins/workspace/public/utils.test.ts b/src/plugins/workspace/public/utils.test.ts index 4763a4455746..5c91effc4146 100644 --- a/src/plugins/workspace/public/utils.test.ts +++ b/src/plugins/workspace/public/utils.test.ts @@ -13,10 +13,12 @@ import { getDataSourcesList, convertNavGroupToWorkspaceUseCase, isEqualWorkspaceUseCase, + USE_CASE_PREFIX, + prependWorkspaceToBreadcrumbs, } from './utils'; import { WorkspaceAvailability } from '../../../core/public'; import { coreMock } from '../../../core/public/mocks'; -import { WORKSPACE_USE_CASES } from '../common/constants'; +import { WORKSPACE_DETAIL_APP_ID, WORKSPACE_USE_CASES } from '../common/constants'; const startMock = coreMock.createStart(); const STATIC_USE_CASES = [ @@ -496,3 +498,145 @@ describe('workspace utils: isEqualWorkspaceUseCase', () => { ).toEqual(true); }); }); + +describe('workspace utils: prependWorkspaceToBreadcrumbs', () => { + const workspace = { + id: 'workspace-1', + name: 'test workspace 1', + features: [`${USE_CASE_PREFIX}search`], + }; + + it('should not enrich breadcrumbs for workspace detail page', () => { + const coreStart = coreMock.createStart(); + prependWorkspaceToBreadcrumbs(coreStart, workspace, WORKSPACE_DETAIL_APP_ID, undefined, {}); + expect(coreStart.chrome.setBreadcrumbsEnricher).toHaveBeenCalledWith(undefined); + }); + + it('should not enrich breadcrumbs when out a workspace', async () => { + const coreStart = coreMock.createStart(); + prependWorkspaceToBreadcrumbs(coreStart, null, 'app1', undefined, {}); + expect(coreStart.chrome.setBreadcrumbsEnricher).toHaveBeenCalledWith(undefined); + }); + + it('should enrich breadcrumbs when in a workspace and use workspace use case as current nav group', async () => { + const navGroupSearch = { + id: 'search', + title: 'Search', + description: 'search desc', + navLinks: [], + }; + const navGroupDashboards = { + id: 'ds', + title: 'Dashboards', + description: 'Dashboards desc', + navLinks: [], + }; + + const coreStart = coreMock.createStart(); + prependWorkspaceToBreadcrumbs(coreStart, workspace, 'app1', undefined, { + search: navGroupSearch, + ds: navGroupDashboards, + }); + expect(coreStart.chrome.setBreadcrumbsEnricher).toHaveBeenCalledTimes(1); + let calls = coreStart.chrome.setBreadcrumbsEnricher.mock.calls; + // calls is an array of arrays, where each inner array represents the arguments for a single call + // get the actual enricher + let enricher = calls[0][0]; + + const breadcrumbs = [{ text: 'test app' }]; + let enrichedBreadcrumbs = enricher?.(breadcrumbs); + expect(enrichedBreadcrumbs).toHaveLength(3); + expect(enrichedBreadcrumbs?.[1].text).toEqual('Search'); + + // ignore current nav group + prependWorkspaceToBreadcrumbs(coreStart, workspace, 'app1', navGroupDashboards, { + search: navGroupSearch, + ds: navGroupDashboards, + }); + expect(coreStart.chrome.setBreadcrumbsEnricher).toHaveBeenCalledTimes(2); + calls = coreStart.chrome.setBreadcrumbsEnricher.mock.calls; + // calls is an array of arrays, where each inner array represents the arguments for a single call + // get the actual enricher + enricher = calls[0][0]; + + enrichedBreadcrumbs = enricher?.(breadcrumbs); + expect(enrichedBreadcrumbs).toHaveLength(3); + expect(enrichedBreadcrumbs?.[1].text).toEqual('Search'); + }); + + it('should enrich breadcrumbs when in a workspace with all use case and use selected nav group', async () => { + const workspaceWithAllUseCase = { + id: 'workspace-all', + name: 'test workspace 1', + features: [`${USE_CASE_PREFIX}all`], + }; + + const navGroupSearch = { + id: 'search', + title: 'Search', + description: 'search desc', + navLinks: [], + }; + const navGroupDashboards = { + id: 'ds', + title: 'Dashboards', + description: 'Dashboards desc', + navLinks: [], + }; + + const coreStart = coreMock.createStart(); + prependWorkspaceToBreadcrumbs(coreStart, workspaceWithAllUseCase, 'app1', navGroupDashboards, { + search: navGroupSearch, + ds: navGroupDashboards, + }); + expect(coreStart.chrome.setBreadcrumbsEnricher).toHaveBeenCalledTimes(1); + + const calls = coreStart.chrome.setBreadcrumbsEnricher.mock.calls; + // calls is an array of arrays, where each inner array represents the arguments for a single call + // get the actual enricher + const enricher = calls[0][0]; + + const breadcrumbs = [{ text: 'test app' }]; + const enrichedBreadcrumbs = enricher?.(breadcrumbs); + expect(enrichedBreadcrumbs).toHaveLength(4); + expect(enrichedBreadcrumbs?.[1].text).toEqual(workspaceWithAllUseCase.name); + expect(enrichedBreadcrumbs?.[2].text).toEqual(navGroupDashboards.title); + }); + + it('should enrich breadcrumbs when in a workspace with all use case and current nav group is null', async () => { + const workspaceWithAllUseCase = { + id: 'workspace-all', + name: 'test workspace 1', + features: [`${USE_CASE_PREFIX}all`], + }; + + const navGroupSearch = { + id: 'search', + title: 'Search', + description: 'search desc', + navLinks: [], + }; + const navGroupDashboards = { + id: 'ds', + title: 'Dashboards', + description: 'Dashboards desc', + navLinks: [], + }; + + const coreStart = coreMock.createStart(); + prependWorkspaceToBreadcrumbs(coreStart, workspaceWithAllUseCase, 'app1', undefined, { + search: navGroupSearch, + ds: navGroupDashboards, + }); + expect(coreStart.chrome.setBreadcrumbsEnricher).toHaveBeenCalledTimes(1); + + const calls = coreStart.chrome.setBreadcrumbsEnricher.mock.calls; + // calls is an array of arrays, where each inner array represents the arguments for a single call + // get the actual enricher + const enricher = calls[0][0]; + + const enrichedBreadcrumbs = enricher?.([{ text: 'overview' }]); + expect(enrichedBreadcrumbs).toHaveLength(3); + expect(enrichedBreadcrumbs?.[1].text).toEqual(workspaceWithAllUseCase.name); + }); +}); diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index d4bc61638744..e1093495c049 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -3,11 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { combineLatest } from 'rxjs'; import { NavGroupType, SavedObjectsStart, NavGroupItemInMap, ALL_USE_CASE_ID, + CoreStart, + ChromeBreadcrumb, } from '../../../core/public'; import { App, @@ -18,10 +21,10 @@ import { WorkspaceObject, WorkspaceAvailability, } from '../../../core/public'; +import { DEFAULT_SELECTED_FEATURES_IDS, WORKSPACE_DETAIL_APP_ID } from '../common/constants'; import { WorkspaceUseCase } from './types'; -import { DEFAULT_SELECTED_FEATURES_IDS } from '../common/constants'; -const USE_CASE_PREFIX = 'use-case-'; +export const USE_CASE_PREFIX = 'use-case-'; export const getUseCaseFeatureConfig = (useCaseId: string) => `${USE_CASE_PREFIX}${useCaseId}`; @@ -265,3 +268,75 @@ const isNotNull = (value: T | null): value is T => !!value; export const getFirstUseCaseOfFeatureConfigs = (featureConfigs: string[]): string | undefined => featureConfigs.map(getUseCaseFromFeatureConfig).filter(isNotNull)[0]; + +export function enrichBreadcrumbsWithWorkspace(core: CoreStart) { + return combineLatest([ + core.workspaces.currentWorkspace$, + core.application.currentAppId$, + core.chrome.navGroup.getCurrentNavGroup$(), + core.chrome.navGroup.getNavGroupsMap$(), + ]).subscribe(([currentWorkspace, appId, currentNavGroup, navGroupsMap]) => { + prependWorkspaceToBreadcrumbs(core, currentWorkspace, appId, currentNavGroup, navGroupsMap); + }); +} + +/** + * prepend workspace or its use case to breadcrumbs + * @param core CoreStart + */ +export function prependWorkspaceToBreadcrumbs( + core: CoreStart, + currentWorkspace: WorkspaceObject | null, + appId: string | undefined, + currentNavGroup: NavGroupItemInMap | undefined, + navGroupsMap: Record +) { + if (appId === WORKSPACE_DETAIL_APP_ID) { + core.chrome.setBreadcrumbsEnricher(undefined); + return; + } + if (currentWorkspace) { + const useCase = getFirstUseCaseOfFeatureConfigs(currentWorkspace?.features || []); + // get workspace the only use case + if (useCase && useCase !== ALL_USE_CASE_ID) { + currentNavGroup = navGroupsMap[useCase]; + } + const navGroupBreadcrumb: ChromeBreadcrumb = { + text: currentNavGroup?.title, + onClick: () => { + // current nav group links are sorted, we don't need to sort it again here + if (currentNavGroup?.navLinks[0].id) { + core.application.navigateToApp(currentNavGroup?.navLinks[0].id); + } + }, + }; + const homeBreadcrumb: ChromeBreadcrumb = { + text: 'Home', + onClick: () => { + core.application.navigateToApp('home'); + }, + }; + + core.chrome.setBreadcrumbsEnricher((breadcrumbs) => { + if (!breadcrumbs || !breadcrumbs.length) return breadcrumbs; + + const workspaceBreadcrumb: ChromeBreadcrumb = { + text: currentWorkspace.name, + onClick: () => { + core.application.navigateToApp(WORKSPACE_DETAIL_APP_ID); + }, + }; + if (useCase === ALL_USE_CASE_ID) { + if (currentNavGroup) { + return [homeBreadcrumb, workspaceBreadcrumb, navGroupBreadcrumb, ...breadcrumbs]; + } else { + return [homeBreadcrumb, workspaceBreadcrumb, ...breadcrumbs]; + } + } else { + return [homeBreadcrumb, navGroupBreadcrumb, ...breadcrumbs]; + } + }); + } else { + core.chrome.setBreadcrumbsEnricher(undefined); + } +}