Skip to content

Commit

Permalink
[Navigation-next] Add register nav group updater to nav group service (
Browse files Browse the repository at this point in the history
…#7117) (#7167)

* Add registerNavGroupUpdater to nav group service



* Register navGroupUpdater in workspace plugin



* Changeset file for PR #7117 created/updated

---------




(cherry picked from commit 417246b)

Signed-off-by: Lin Wang <[email protected]>
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
Co-authored-by: ZilongX <[email protected]>
  • Loading branch information
4 people authored Jul 4, 2024
1 parent ed6371f commit 3ed43b2
Show file tree
Hide file tree
Showing 12 changed files with 200 additions and 5 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/7117.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
feat:
- [Navigation-next] Add register nav group updater in chrome service ([#7117](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7117))
1 change: 1 addition & 0 deletions src/core/public/chrome/chrome_service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const createSetupContractMock = () => {
navGroup: {
addNavLinksToGroup: jest.fn(),
getNavGroupEnabled: jest.fn(),
registerNavGroupUpdater: jest.fn(),
},
};
};
Expand Down
2 changes: 1 addition & 1 deletion src/core/public/chrome/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@ export { ChromeRecentlyAccessed, ChromeRecentlyAccessedHistoryItem } from './rec
export { ChromeNavControl, ChromeNavControls } from './nav_controls';
export { ChromeDocTitle } from './doc_title';
export { RightNavigationOrder } from './constants';
export { ChromeRegistrationNavLink } from './nav_group';
export { ChromeRegistrationNavLink, ChromeNavGroupUpdater } from './nav_group';
1 change: 1 addition & 0 deletions src/core/public/chrome/nav_group/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export {
ChromeNavGroupServiceStartContract,
ChromeRegistrationNavLink,
NavGroupItemInMap,
ChromeNavGroupUpdater,
} from './nav_group_service';
64 changes: 64 additions & 0 deletions src/core/public/chrome/nav_group/nav_group_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { uiSettingsServiceMock } from '../../ui_settings/ui_settings_service.moc
import { NavLinksService } from '../nav_links';
import { applicationServiceMock, httpServiceMock } from '../../mocks';
import { AppCategory } from 'opensearch-dashboards/public';
import { DEFAULT_NAV_GROUPS } from '../../';

const mockedGroupFoo = {
id: 'foo',
Expand Down Expand Up @@ -219,3 +220,66 @@ describe('ChromeNavGroupService#start()', () => {
expect(chromeNavGroupServiceStart.getNavGroupEnabled()).toBe(false);
});
});

describe('nav group updater', () => {
it('should emit updated nav group after nav group updater called', async () => {
const navGroup = new ChromeNavGroupService();
const uiSettings = uiSettingsServiceMock.createSetupContract();
uiSettings.get$.mockImplementation(() => new Rx.BehaviorSubject(true));

const navGroupSetup = navGroup.setup({ uiSettings });
navGroupSetup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.dataAdministration, [
{
id: 'foo',
},
]);
const navGroupStart = await navGroup.start({ navLinks: mockedNavLinkService });

expect(await navGroupStart.getNavGroupsMap$().pipe(first()).toPromise()).toEqual({
dataAdministration: expect.not.objectContaining({
status: expect.anything,
}),
});
navGroupSetup.registerNavGroupUpdater(
new Rx.BehaviorSubject(() => ({
status: 2,
}))
);
expect(await navGroupStart.getNavGroupsMap$().pipe(first()).toPromise()).toEqual({
dataAdministration: expect.objectContaining({
status: 2,
}),
});
});

it('should reset to original status after nav group updater unregister', async () => {
const navGroup = new ChromeNavGroupService();
const uiSettings = uiSettingsServiceMock.createSetupContract();
uiSettings.get$.mockImplementation(() => new Rx.BehaviorSubject(true));

const navGroupSetup = navGroup.setup({ uiSettings });
navGroupSetup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.dataAdministration, [
{
id: 'foo',
},
]);
const appUpdater$ = new Rx.BehaviorSubject(() => ({
status: 2,
}));
const unregister = navGroupSetup.registerNavGroupUpdater(appUpdater$);
const navGroupStart = await navGroup.start({ navLinks: mockedNavLinkService });
expect(await navGroupStart.getNavGroupsMap$().pipe(first()).toPromise()).toEqual({
dataAdministration: expect.objectContaining({
status: 2,
}),
});

unregister();

expect(await navGroupStart.getNavGroupsMap$().pipe(first()).toPromise()).toEqual({
dataAdministration: expect.not.objectContaining({
status: expect.anything,
}),
});
});
});
45 changes: 42 additions & 3 deletions src/core/public/chrome/nav_group/nav_group_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { BehaviorSubject, combineLatest, Observable, ReplaySubject, Subscription } from 'rxjs';
import { BehaviorSubject, combineLatest, Observable, of, ReplaySubject, Subscription } from 'rxjs';
import { AppCategory, ChromeNavGroup, ChromeNavLink } from 'opensearch-dashboards/public';
import { map, takeUntil } from 'rxjs/operators';
import { map, switchMap, takeUntil } from 'rxjs/operators';
import { IUiSettingsClient } from '../../ui_settings';
import {
flattenLinksOrCategories,
Expand All @@ -31,12 +31,15 @@ export type NavGroupItemInMap = ChromeNavGroup & {
navLinks: ChromeRegistrationNavLink[];
};

export type ChromeNavGroupUpdater = (navGroup: ChromeNavGroup) => Partial<ChromeNavGroup> | void;

export interface ChromeNavGroupServiceSetupContract {
addNavLinksToGroup: (navGroup: ChromeNavGroup, navLinks: ChromeRegistrationNavLink[]) => void;
/**
* Get a boolean value to indicates whether use case is enabled
*/
getNavGroupEnabled: () => boolean;
registerNavGroupUpdater: (navGroupUpdater: Observable<ChromeNavGroupUpdater>) => () => void;
}

export interface ChromeNavGroupServiceStartContract {
Expand All @@ -51,6 +54,7 @@ export class ChromeNavGroupService {
private navLinks$: Observable<Array<Readonly<ChromeNavLink>>> = new BehaviorSubject([]);
private navGroupEnabled: boolean = false;
private navGroupEnabledUiSettingsSubscription: Subscription | undefined;
private navGroupUpdaters$$ = new BehaviorSubject<Array<Observable<ChromeNavGroupUpdater>>>([]);
private addNavLinkToGroup(
currentGroupsMap: Record<string, NavGroupItemInMap>,
navGroup: ChromeNavGroup,
Expand Down Expand Up @@ -78,7 +82,7 @@ export class ChromeNavGroupService {
return currentGroupsMap;
}
private getSortedNavGroupsMap$() {
return combineLatest([this.navGroupsMap$, this.navLinks$])
return combineLatest([this.getUpdatedNavGroupsMap$(), this.navLinks$])
.pipe(takeUntil(this.stop$))
.pipe(
map(([navGroupsMap, navLinks]) => {
Expand All @@ -96,6 +100,33 @@ export class ChromeNavGroupService {
})
);
}

private getUpdatedNavGroupsMap$() {
return combineLatest([this.navGroupsMap$, this.navGroupUpdaters$$]).pipe(
switchMap(([navGroupsMap, updaters$]) => {
if (updaters$.length === 0) {
return of(navGroupsMap);
}
return combineLatest(updaters$).pipe(
map((updaters) => {
return Object.keys(navGroupsMap).reduce<Record<string, NavGroupItemInMap>>(
(previousValue, currentKey) => ({
...previousValue,
[currentKey]: updaters.reduce(
(prevNavGroup, currentUpdater) => ({
...prevNavGroup,
...currentUpdater(prevNavGroup),
}),
navGroupsMap[currentKey]
),
}),
{}
);
})
);
})
);
}
setup({ uiSettings }: { uiSettings: IUiSettingsClient }): ChromeNavGroupServiceSetupContract {
this.navGroupEnabledUiSettingsSubscription = uiSettings
.get$('home:useNewHomePage', false)
Expand All @@ -116,6 +147,14 @@ export class ChromeNavGroupService {
this.navGroupsMap$.next(navGroupsMapAfterAdd);
},
getNavGroupEnabled: () => this.navGroupEnabled,
registerNavGroupUpdater: (updater$) => {
this.navGroupUpdaters$$.next([...this.navGroupUpdaters$$.getValue(), updater$]);
return () => {
this.navGroupUpdaters$$.next(
this.navGroupUpdaters$$.getValue().filter((item) => item !== updater$)
);
};
},
};
}
async start({
Expand Down
3 changes: 3 additions & 0 deletions src/core/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import {
RightNavigationButton,
RightNavigationButtonProps,
ChromeRegistrationNavLink,
ChromeNavGroupUpdater,
} from './chrome';
import { FatalErrorsSetup, FatalErrorsStart, FatalErrorInfo } from './fatal_errors';
import { HttpSetup, HttpStart } from './http';
Expand Down Expand Up @@ -118,6 +119,7 @@ export {
WorkspaceAttribute,
ChromeNavGroup,
NavGroupType,
NavGroupStatus,
} from '../types';

export {
Expand Down Expand Up @@ -371,6 +373,7 @@ export {
RightNavigationButton,
RightNavigationButtonProps,
ChromeRegistrationNavLink,
ChromeNavGroupUpdater,
};

export { __osdBootstrap__ } from './osd_bootstrap';
Expand Down
7 changes: 7 additions & 0 deletions src/core/types/nav_group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export enum NavGroupType {
SYSTEM = 'system',
}

export enum NavGroupStatus {
Visible,
Hidden,
}

/** @public */
export interface ChromeNavGroup {
id: string;
Expand All @@ -24,4 +29,6 @@ export interface ChromeNavGroup {
order?: number;
icon?: EuiIconType;
type?: NavGroupType;

status?: NavGroupStatus;
}
34 changes: 34 additions & 0 deletions src/plugins/workspace/public/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,38 @@ describe('Workspace plugin', () => {
workspacePlugin.start(startMock);
expect(startMock.chrome.setBreadcrumbs).not.toHaveBeenCalled();
});

it('#start should call navGroupUpdater$.next after currentWorkspace set', async () => {
const workspacePlugin = new WorkspacePlugin();
const setupMock = getSetupMock();
const coreStart = coreMock.createStart();
await workspacePlugin.setup(setupMock, {});

expect(setupMock.chrome.navGroup.registerNavGroupUpdater).toHaveBeenCalled();
const navGroupUpdater$ = setupMock.chrome.navGroup.registerNavGroupUpdater.mock.calls[0][0];

expect(navGroupUpdater$).toBeTruthy();
jest.spyOn(navGroupUpdater$, 'next');

expect(navGroupUpdater$.next).not.toHaveBeenCalled();
workspacePlugin.start(coreStart);

waitFor(() => {
expect(navGroupUpdater$.next).toHaveBeenCalled();
});
});

it('#stop should call unregisterNavGroupUpdater', async () => {
const workspacePlugin = new WorkspacePlugin();
const setupMock = getSetupMock();
const unregisterNavGroupUpdater = jest.fn();
setupMock.chrome.navGroup.registerNavGroupUpdater.mockReturnValueOnce(
unregisterNavGroupUpdater
);
await workspacePlugin.setup(setupMock, {});

workspacePlugin.stop();

expect(unregisterNavGroupUpdater).toHaveBeenCalled();
});
});
26 changes: 25 additions & 1 deletion src/plugins/workspace/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
PublicAppInfo,
ChromeBreadcrumb,
WorkspaceAvailability,
ChromeNavGroupUpdater,
NavGroupStatus,
} from '../../../core/public';
import {
WORKSPACE_FATAL_ERROR_APP_ID,
Expand All @@ -33,7 +35,11 @@ import { SavedObjectsManagementPluginSetup } from '../../../plugins/saved_object
import { ManagementSetup } from '../../../plugins/management/public';
import { WorkspaceMenu } from './components/workspace_menu/workspace_menu';
import { getWorkspaceColumn } from './components/workspace_column';
import { filterWorkspaceConfigurableApps, isAppAccessibleInWorkspace } from './utils';
import {
filterWorkspaceConfigurableApps,
isAppAccessibleInWorkspace,
isNavGroupInFeatureConfigs,
} from './utils';

type WorkspaceAppType = (
params: AppMountParameters,
Expand All @@ -53,7 +59,10 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps>
private currentWorkspaceIdSubscription?: Subscription;
private managementCurrentWorkspaceIdSubscription?: Subscription;
private appUpdater$ = new BehaviorSubject<AppUpdater>(() => undefined);
private navGroupUpdater$ = new BehaviorSubject<ChromeNavGroupUpdater>(() => undefined);
private workspaceConfigurableApps$ = new BehaviorSubject<PublicAppInfo[]>([]);
private unregisterNavGroupUpdater?: () => void;

private _changeSavedObjectCurrentWorkspace() {
if (this.coreStart) {
return this.coreStart.workspaces.currentWorkspaceId$.subscribe((currentWorkspaceId) => {
Expand Down Expand Up @@ -89,6 +98,17 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps>
*/
return { status: AppStatus.inaccessible };
});

this.navGroupUpdater$.next((navGroup) => {
if (
currentWorkspace.features &&
!isNavGroupInFeatureConfigs(navGroup.id, currentWorkspace.features)
) {
return {
status: NavGroupStatus.Hidden,
};
}
});
}
});
};
Expand Down Expand Up @@ -166,6 +186,9 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps>
const workspaceClient = new WorkspaceClient(core.http, core.workspaces);
await workspaceClient.init();
core.application.registerAppUpdater(this.appUpdater$);
this.unregisterNavGroupUpdater = core.chrome.navGroup.registerNavGroupUpdater(
this.navGroupUpdater$
);

// Hide advance settings and dataSource menus and disable in setup
if (management) {
Expand Down Expand Up @@ -331,5 +354,6 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps>
this.currentWorkspaceIdSubscription?.unsubscribe();
this.managementCurrentWorkspaceIdSubscription?.unsubscribe();
this.breadcrumbsSubscription?.unsubscribe();
this.unregisterNavGroupUpdater?.();
}
}
17 changes: 17 additions & 0 deletions src/plugins/workspace/public/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
filterWorkspaceConfigurableApps,
isAppAccessibleInWorkspace,
isFeatureIdInsideUseCase,
isNavGroupInFeatureConfigs,
} from './utils';
import { WorkspaceAvailability } from '../../../core/public';

Expand Down Expand Up @@ -276,3 +277,19 @@ describe('workspace utils: isFeatureIdInsideUseCase', () => {
expect(isFeatureIdInsideUseCase('discover', 'use-case-invalid')).toBe(false);
});
});

describe('workspace utils: isNavGroupInFeatureConfigs', () => {
it('should return false if nav group not in feature configs', () => {
expect(
isNavGroupInFeatureConfigs('dataAdministration', [
'use-case-observability',
'use-case-search',
])
).toBe(false);
});
it('should return true if nav group in feature configs', () => {
expect(
isNavGroupInFeatureConfigs('observability', ['use-case-observability', 'use-case-search'])
).toBe(true);
});
});
3 changes: 3 additions & 0 deletions src/plugins/workspace/public/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export const isFeatureIdInsideUseCase = (featureId: string, featureConfig: strin
return false;
};

export const isNavGroupInFeatureConfigs = (navGroupId: string, featureConfigs: string[]) =>
featureConfigs.includes(getUseCaseFeatureConfig(navGroupId));

/**
* Checks if a given feature matches the provided feature configuration.
*
Expand Down

0 comments on commit 3ed43b2

Please sign in to comment.