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

[Backport 2.x] [Navigation-next] Add register nav group updater to nav group service #7167

Merged
merged 1 commit into from
Jul 4, 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/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 @@
PublicAppInfo,
ChromeBreadcrumb,
WorkspaceAvailability,
ChromeNavGroupUpdater,
NavGroupStatus,
} from '../../../core/public';
import {
WORKSPACE_FATAL_ERROR_APP_ID,
Expand All @@ -33,7 +35,11 @@
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 @@
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 @@
*/
return { status: AppStatus.inaccessible };
});

this.navGroupUpdater$.next((navGroup) => {
if (
currentWorkspace.features &&
!isNavGroupInFeatureConfigs(navGroup.id, currentWorkspace.features)
) {
return {

Check warning on line 107 in src/plugins/workspace/public/plugin.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/workspace/public/plugin.ts#L107

Added line #L107 was not covered by tests
status: NavGroupStatus.Hidden,
};
}
});
}
});
};
Expand Down Expand Up @@ -166,6 +186,9 @@
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 @@
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
Loading