Skip to content

Commit

Permalink
[Navigation-Next] Add current nav group into chrome service (#7166) (#…
Browse files Browse the repository at this point in the history
…7229)

* Add current nav group into chrome service

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

breadcrumbs

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

* prepend nav group into breadcrumbs

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

* add unit test

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

* remove lodash import

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

* Changeset file for PR #7166 created/updated

* fix bootstrap error

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

* add nav group status check

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

* address review comments

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

* update snapshot

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>
(cherry picked from commit 0215e32)
  • Loading branch information
Hailong-am authored Jul 16, 2024
1 parent 6e5ee91 commit cf4d114
Show file tree
Hide file tree
Showing 14 changed files with 704 additions and 17 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/7166.yml
Original file line number Diff line number Diff line change
@@ -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))
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 @@ -78,6 +78,8 @@ const createStartContractMock = () => {
navGroup: {
getNavGroupsMap$: jest.fn(() => new BehaviorSubject({})),
getNavGroupEnabled: jest.fn(),
getCurrentNavGroup$: jest.fn(() => new BehaviorSubject(undefined)),
setCurrentNavGroup: jest.fn(),
},
setAppTitle: jest.fn(),
setIsVisible: jest.fn(),
Expand Down
6 changes: 4 additions & 2 deletions src/core/public/chrome/chrome_service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ 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 });
const navGroup = await this.navGroup.start({ navLinks, application });

// erase chrome fields from a previous app while switching to a next app
application.currentAppId$.subscribe(() => {
Expand Down Expand Up @@ -300,8 +300,10 @@ export class ChromeService {
branding={injectedMetadata.getBranding()}
logos={logos}
survey={injectedMetadata.getSurvey()}
sidecarConfig$={sidecarConfig$}
collapsibleNavHeaderRender={this.collapsibleNavHeaderRender}
sidecarConfig$={sidecarConfig$}
navGroupEnabled={navGroup.getNavGroupEnabled()}
currentNavgroup$={navGroup.getCurrentNavGroup$()}
/>
),

Expand Down
153 changes: 149 additions & 4 deletions src/core/public/chrome/nav_group/nav_group_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -94,6 +123,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);
Expand All @@ -116,6 +146,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);
Expand Down Expand Up @@ -172,7 +203,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();

Expand All @@ -196,6 +230,7 @@ describe('ChromeNavGroupService#start()', () => {
chromeNavGroupService.setup({ uiSettings });
const chromeNavGroupServiceStart = await chromeNavGroupService.start({
navLinks: mockedNavLinkService,
application: mockedApplicationService,
});

expect(chromeNavGroupServiceStart.getNavGroupEnabled()).toBe(true);
Expand All @@ -210,6 +245,7 @@ describe('ChromeNavGroupService#start()', () => {
chromeNavGroupService.setup({ uiSettings });
const chromeNavGroupServiceStart = await chromeNavGroupService.start({
navLinks: mockedNavLinkService,
application: mockedApplicationService,
});

navGroupEnabled$.next(false);
Expand All @@ -219,6 +255,109 @@ 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('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$);

const chromeNavGroupService = new ChromeNavGroupService();
const chromeNavGroupServiceSetup = chromeNavGroupService.setup({ uiSettings });

chromeNavGroupServiceSetup.addNavLinksToGroup(
{
id: 'foo',
title: 'foo title',
description: 'foo description',
},
[{ id: 'foo-app1' }]
);

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');

// 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();
});
});

describe('nav group updater', () => {
Expand All @@ -233,7 +372,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({
Expand Down Expand Up @@ -267,7 +409,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,
Expand Down
81 changes: 77 additions & 4 deletions src/core/public/chrome/nav_group/nav_group_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import {
getOrderedLinksOrCategories,
} 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';

/** @public */
export interface ChromeRegistrationNavLink {
Expand Down Expand Up @@ -50,6 +54,16 @@ export interface ChromeNavGroupServiceSetupContract {
export interface ChromeNavGroupServiceStartContract {
getNavGroupsMap$: () => Observable<Record<string, NavGroupItemInMap>>;
getNavGroupEnabled: ChromeNavGroupServiceSetupContract['getNavGroupEnabled'];
/**
* Get an observable of the current selected nav group
*/
getCurrentNavGroup$: () => Observable<NavGroupItemInMap | undefined>;

/**
* Set current selected nav group
* @param navGroupId The id of the nav group to be set as current
*/
setCurrentNavGroup: (navGroupId: string | undefined) => void;
}

/** @internal */
Expand All @@ -60,6 +74,8 @@ export class ChromeNavGroupService {
private navGroupEnabled: boolean = false;
private navGroupEnabledUiSettingsSubscription: Subscription | undefined;
private navGroupUpdaters$$ = new BehaviorSubject<Array<Observable<ChromeNavGroupUpdater>>>([]);
private currentNavGroup$ = new BehaviorSubject<ChromeNavGroup | undefined>(undefined);

private addNavLinkToGroup(
currentGroupsMap: Record<string, NavGroupItemInMap>,
navGroup: ChromeNavGroup,
Expand All @@ -86,19 +102,28 @@ export class ChromeNavGroupService {

return currentGroupsMap;
}

private sortNavGroupNavLinks(
navGroup: NavGroupItemInMap,
allVaildNavLinks: Array<Readonly<ChromeNavLink>>
) {
return flattenLinksOrCategories(
getOrderedLinksOrCategories(
fulfillRegistrationLinksToChromeNavLinks(navGroup.navLinks, allVaildNavLinks)
)
);
}

private getSortedNavGroupsMap$() {
return combineLatest([this.getUpdatedNavGroupsMap$(), this.navLinks$])
.pipe(takeUntil(this.stop$))
.pipe(
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<string, NavGroupItemInMap>);
Expand Down Expand Up @@ -164,15 +189,63 @@ export class ChromeNavGroupService {
}
async start({
navLinks,
application,
}: {
navLinks: ChromeNavLinks;
application: InternalApplicationStart;
}): Promise<ChromeNavGroupServiceStartContract> {
this.navLinks$ = navLinks.getNavLinks$();

const currentNavGroupId = sessionStorage.getItem(CURRENT_NAV_GROUP_ID);
this.currentNavGroup$ = new BehaviorSubject<ChromeNavGroup | undefined>(
currentNavGroupId ? this.navGroupsMap$.getValue()[currentNavGroupId] : undefined
);

const setCurrentNavGroup = (navGroupId: string | undefined) => {
const navGroup = navGroupId ? this.navGroupsMap$.getValue()[navGroupId] : undefined;
if (navGroup && navGroup.status !== NavGroupStatus.Hidden) {
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 app don't belongs to any nav group
application.currentAppId$.subscribe((appId) => {
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 currentNavGroupSorted$ = 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$: () => currentNavGroupSorted$,
setCurrentNavGroup,
};
}

async stop() {
this.stop$.next();
this.navGroupEnabledUiSettingsSubscription?.unsubscribe();
Expand Down
Loading

0 comments on commit cf4d114

Please sign in to comment.