Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Navigation-Next] Add current nav group into chrome service #7166

Merged
merged 10 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
navGroup: {
getNavGroupsMap$: jest.fn(() => new BehaviorSubject({})),
getNavGroupEnabled: jest.fn(),
getCurrentNavGroup$: jest.fn(() => new BehaviorSubject(undefined)),

Check warning on line 81 in src/core/public/chrome/chrome_service.mock.ts

View check run for this annotation

Codecov / codecov/patch

src/core/public/chrome/chrome_service.mock.ts#L81

Added line #L81 was not covered by tests
setCurrentNavGroup: jest.fn(),
},
setAppTitle: jest.fn(),
setIsVisible: jest.fn(),
Expand Down
4 changes: 3 additions & 1 deletion src/core/public/chrome/chrome_service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,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 @@ -301,6 +301,8 @@ export class ChromeService {
survey={injectedMetadata.getSurvey()}
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 @@ -45,6 +49,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 @@ -55,6 +69,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 @@ -81,19 +97,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 @@ -159,15 +184,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);
Hailong-am marked this conversation as resolved.
Show resolved Hide resolved
}
});

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
Loading