Skip to content

Commit

Permalink
[Navigation-next] Enrich breadcrumbs by workspace and use case (#7360)
Browse files Browse the repository at this point in the history
* breadcrumbs for workspace

Signed-off-by: Hailong Cui <ihailong@amazon.com>

* add unit test

Signed-off-by: Hailong Cui <ihailong@amazon.com>

* Changeset file for PR #7360 created/updated

* add unit test

Signed-off-by: Hailong Cui <ihailong@amazon.com>

---------

Signed-off-by: Hailong Cui <ihailong@amazon.com>
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
  • Loading branch information
1 parent 9348bd4 commit 7ff8544
Show file tree
Hide file tree
Showing 16 changed files with 646 additions and 223 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/7360.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
feat:
- Enrich breadcrumbs by workspace and use case ([#7360](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7360))
2 changes: 2 additions & 0 deletions src/core/public/chrome/chrome_service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
30 changes: 29 additions & 1 deletion src/core/public/chrome/chrome_service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export interface ChromeBadge {
/** @public */
export type ChromeBreadcrumb = EuiBreadcrumb;

/** @public */
export type ChromeBreadcrumbEnricher = (breadcrumbs: ChromeBreadcrumb[]) => ChromeBreadcrumb[];

/** @public */
export type ChromeBranding = Branding;

Expand Down Expand Up @@ -190,6 +193,9 @@ export class ChromeService {
const applicationClasses$ = new BehaviorSubject<Set<string>>(new Set());
const helpExtension$ = new BehaviorSubject<ChromeHelpExtension | undefined>(undefined);
const breadcrumbs$ = new BehaviorSubject<ChromeBreadcrumb[]>([]);
const breadcrumbsEnricher$ = new BehaviorSubject<ChromeBreadcrumbEnricher | undefined>(
undefined
);
const badge$ = new BehaviorSubject<ChromeBadge | undefined>(undefined);
const customNavLink$ = new BehaviorSubject<ChromeNavLink | undefined>(undefined);
const helpSupportUrl$ = new BehaviorSubject<string>(OPENSEARCH_DASHBOARDS_ASK_OPENSEARCH_LINK);
Expand All @@ -200,7 +206,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(() => {
Expand Down Expand Up @@ -279,6 +290,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$()}
Expand Down Expand Up @@ -346,6 +358,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) => {
Expand Down Expand Up @@ -482,6 +500,16 @@ export interface ChromeStart {
*/
setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void;

/**
* Get an observable of the current breadcrumbs enricher
*/
getBreadcrumbsEnricher$(): Observable<ChromeBreadcrumbEnricher | undefined>;

/**
* Override the current ChromeBreadcrumbEnricher
*/
setBreadcrumbsEnricher(newBreadcrumbsEnricher: ChromeBreadcrumbEnricher | undefined): void;

/**
* Get an observable of the current custom nav link
*/
Expand Down
214 changes: 213 additions & 1 deletion src/core/public/chrome/nav_group/nav_group_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -124,6 +126,8 @@ describe('ChromeNavGroupService#setup()', () => {
const chromeNavGroupServiceStart = await chromeNavGroupService.start({
navLinks: mockedNavLinkService,
application: mockedApplicationService,
breadcrumbsEnricher$: new Rx.BehaviorSubject<ChromeBreadcrumbEnricher | undefined>(undefined),
workspaces: workspacesServiceMock.createStartContract(),
});
const groupsMap = await chromeNavGroupServiceStart.getNavGroupsMap$().pipe(first()).toPromise();
expect(groupsMap[mockedGroupFoo.id].navLinks.length).toEqual(2);
Expand All @@ -147,6 +151,8 @@ describe('ChromeNavGroupService#setup()', () => {
const chromeNavGroupServiceStart = await chromeNavGroupService.start({
navLinks: mockedNavLinkService,
application: mockedApplicationService,
breadcrumbsEnricher$: new Rx.BehaviorSubject<ChromeBreadcrumbEnricher | undefined>(undefined),
workspaces: workspacesServiceMock.createStartContract(),
});
const groupsMap = await chromeNavGroupServiceStart.getNavGroupsMap$().pipe(first()).toPromise();
expect(groupsMap[mockedGroupFoo.id].navLinks.length).toEqual(1);
Expand Down Expand Up @@ -206,6 +212,8 @@ describe('ChromeNavGroupService#start()', () => {
const chromeStart = await chromeNavGroupService.start({
navLinks: mockedNavLinkService,
application: mockedApplicationService,
breadcrumbsEnricher$: new Rx.BehaviorSubject<ChromeBreadcrumbEnricher | undefined>(undefined),
workspaces: workspacesServiceMock.createStartContract(),
});

const groupsMap = await chromeStart.getNavGroupsMap$().pipe(first()).toPromise();
Expand All @@ -231,6 +239,8 @@ describe('ChromeNavGroupService#start()', () => {
const chromeNavGroupServiceStart = await chromeNavGroupService.start({
navLinks: mockedNavLinkService,
application: mockedApplicationService,
breadcrumbsEnricher$: new Rx.BehaviorSubject<ChromeBreadcrumbEnricher | undefined>(undefined),
workspaces: workspacesServiceMock.createStartContract(),
});

expect(chromeNavGroupServiceStart.getNavGroupEnabled()).toBe(true);
Expand All @@ -246,6 +256,8 @@ describe('ChromeNavGroupService#start()', () => {
const chromeNavGroupServiceStart = await chromeNavGroupService.start({
navLinks: mockedNavLinkService,
application: mockedApplicationService,
breadcrumbsEnricher$: new Rx.BehaviorSubject<ChromeBreadcrumbEnricher | undefined>(undefined),
workspaces: workspacesServiceMock.createStartContract(),
});

navGroupEnabled$.next(false);
Expand Down Expand Up @@ -276,6 +288,8 @@ describe('ChromeNavGroupService#start()', () => {
const chromeNavGroupServiceStart = await chromeNavGroupService.start({
navLinks: mockedNavLinkService,
application: mockedApplicationService,
breadcrumbsEnricher$: new Rx.BehaviorSubject<ChromeBreadcrumbEnricher | undefined>(undefined),
workspaces: workspacesServiceMock.createStartContract(),
});

// set an existing nav group id
Expand Down Expand Up @@ -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<ChromeBreadcrumbEnricher | undefined>(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<ChromeBreadcrumbEnricher | undefined>(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<ChromeBreadcrumbEnricher | undefined>(
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<ChromeBreadcrumbEnricher | undefined>(
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', () => {
Expand All @@ -328,6 +536,8 @@ describe('nav group updater', () => {
const navGroupStart = await navGroup.start({
navLinks: mockedNavLinkService,
application: mockedApplicationService,
breadcrumbsEnricher$: new Rx.BehaviorSubject<ChromeBreadcrumbEnricher | undefined>(undefined),
workspaces: workspacesServiceMock.createStartContract(),
});

expect(await navGroupStart.getNavGroupsMap$().pipe(first()).toPromise()).toEqual({
Expand Down Expand Up @@ -365,6 +575,8 @@ describe('nav group updater', () => {
const navGroupStart = await navGroup.start({
navLinks: mockedNavLinkService,
application: mockedApplicationService,
breadcrumbsEnricher$: new Rx.BehaviorSubject<ChromeBreadcrumbEnricher | undefined>(undefined),
workspaces: workspacesServiceMock.createStartContract(),
});
expect(await navGroupStart.getNavGroupsMap$().pipe(first()).toPromise()).toEqual({
dataAdministration: expect.objectContaining({
Expand Down
Loading

0 comments on commit 7ff8544

Please sign in to comment.