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

feat: Introduce registerCollapsibleNavHeader to ChromeService #5244

Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- [Workspace] Setup workspace skeleton and implement basic CRUD API ([#5075](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5075))
- [Decouple] Add new cross compatibility check core service which export functionality for plugins to verify if their OpenSearch plugin counterpart is installed on the cluster or has incompatible version to configure the plugin behavior([#4710](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4710))
- [Discover] Display inner properties in the left navigation bar [#5429](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5429)
- [Chrome] Introduce registerCollapsibleNavHeader to allow plugins to customize the rendering of nav menu header ([#5244](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5244))

### 🐛 Bug Fixes

Expand Down Expand Up @@ -941,4 +942,4 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)

### 🔩 Tests

- Update caniuse to fix failed integration tests ([#2322](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2322))
- Update caniuse to fix failed integration tests ([#2322](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2322))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: unnecessary change on this line I suppose.

8 changes: 8 additions & 0 deletions src/core/public/chrome/chrome_service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ import type { PublicMethodsOf } from '@osd/utility-types';
import { ChromeBadge, ChromeBreadcrumb, ChromeService, InternalChromeStart } from './';
import { getLogosMock } from '../../common/mocks';

const createSetupContractMock = () => {
return {
registerCollapsibleNavHeader: jest.fn(),
};
};

const createStartContractMock = () => {
const startContract: DeeplyMockedKeys<InternalChromeStart> = {
getHeaderComponent: jest.fn(),
Expand Down Expand Up @@ -95,6 +101,7 @@ const createStartContractMock = () => {
type ChromeServiceContract = PublicMethodsOf<ChromeService>;
const createMock = () => {
const mocked: jest.Mocked<ChromeServiceContract> = {
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
Expand All @@ -105,4 +112,5 @@ const createMock = () => {
export const chromeServiceMock = {
create: createMock,
createStartContract: createStartContractMock,
createSetupContract: createSetupContractMock,
};
38 changes: 38 additions & 0 deletions src/core/public/chrome/chrome_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,44 @@ afterAll(() => {
(window as any).localStorage = originalLocalStorage;
});

describe('setup', () => {
afterEach(() => {
jest.restoreAllMocks();
});

it('register custom Nav Header render', async () => {
const customHeaderMock = React.createElement('TestCustomNavHeader');
const renderMock = jest.fn().mockReturnValue(customHeaderMock);
const chrome = new ChromeService({ browserSupportsCsp: true });

const chromeSetup = chrome.setup();
chromeSetup.registerCollapsibleNavHeader(renderMock);

const chromeStart = await chrome.start(defaultStartDeps());
const wrapper = shallow(React.createElement(() => chromeStart.getHeaderComponent()));
expect(wrapper.prop('collapsibleNavHeaderRender')).toBeDefined();
expect(wrapper.prop('collapsibleNavHeaderRender')()).toEqual(customHeaderMock);
});

it('should output warning message if calling `registerCollapsibleNavHeader` more than once', () => {
const warnMock = jest.fn();
jest.spyOn(console, 'warn').mockImplementation(warnMock);
const customHeaderMock = React.createElement('TestCustomNavHeader');
const renderMock = jest.fn().mockReturnValue(customHeaderMock);
const chrome = new ChromeService({ browserSupportsCsp: true });

const chromeSetup = chrome.setup();
// call 1st time
chromeSetup.registerCollapsibleNavHeader(renderMock);
// call 2nd time
chromeSetup.registerCollapsibleNavHeader(renderMock);
expect(warnMock).toHaveBeenCalledTimes(1);
expect(warnMock).toHaveBeenCalledWith(
'[ChromeService] An existing custom collapsible navigation bar header render has been overridden.'
);
});
});

describe('start', () => {
it('adds legacy browser warning if browserSupportsCsp is disabled and warnLegacyBrowsers is enabled', async () => {
const { startDeps } = await start({ options: { browserSupportsCsp: false } });
Expand Down
32 changes: 32 additions & 0 deletions src/core/public/chrome/chrome_service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ export interface StartDeps {
uiSettings: IUiSettingsClient;
}

type CollapsibleNavHeaderRender = () => JSX.Element | null;

/** @internal */
export class ChromeService {
private isVisible$!: Observable<boolean>;
Expand All @@ -107,6 +109,7 @@ export class ChromeService {
private readonly navLinks = new NavLinksService();
private readonly recentlyAccessed = new RecentlyAccessedService();
private readonly docTitle = new DocTitleService();
private collapsibleNavHeaderRender?: CollapsibleNavHeaderRender;

constructor(private readonly params: ConstructorParams) {}

Expand Down Expand Up @@ -142,6 +145,20 @@ export class ChromeService {
);
}

public setup() {
return {
registerCollapsibleNavHeader: (render: CollapsibleNavHeaderRender) => {
if (this.collapsibleNavHeaderRender) {
// eslint-disable-next-line no-console
console.warn(
'[ChromeService] An existing custom collapsible navigation bar header render has been overridden.'
);
}
this.collapsibleNavHeaderRender = render;
},
};
}

public async start({
application,
docLinks,
Expand Down Expand Up @@ -262,6 +279,7 @@ export class ChromeService {
branding={injectedMetadata.getBranding()}
logos={logos}
survey={injectedMetadata.getSurvey()}
collapsibleNavHeaderRender={this.collapsibleNavHeaderRender}
/>
),

Expand Down Expand Up @@ -325,6 +343,20 @@ export class ChromeService {
}
}

/**
* ChromeSetup allows plugins to customize the global chrome header UI rendering
* before the header UI is mounted.
*
* @example
* Customize the Collapsible Nav's (left nav menu) header section:
* ```ts
* core.chrome.registerCollapsibleNavHeader(() => <CustomNavHeader />)
* ```
*/
export interface ChromeSetup {
registerCollapsibleNavHeader: (render: CollapsibleNavHeaderRender) => void;
}

/**
* ChromeStart allows plugins to customize the global chrome header UI and
* enrich the UX with additional information about the current location of the
Expand Down
1 change: 1 addition & 0 deletions src/core/public/chrome/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export {
ChromeBadge,
ChromeBreadcrumb,
ChromeService,
ChromeSetup,
ChromeStart,
InternalChromeStart,
ChromeHelpExtension,
Expand Down
3 changes: 3 additions & 0 deletions src/core/public/chrome/ui/header/collapsible_nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ function setIsCategoryOpen(id: string, isOpen: boolean, storage: Storage) {
interface Props {
appId$: InternalApplicationStart['currentAppId$'];
basePath: HttpStart['basePath'];
collapsibleNavHeaderRender?: () => JSX.Element | null;
id: string;
isLocked: boolean;
isNavOpen: boolean;
Expand All @@ -106,6 +107,7 @@ interface Props {

export function CollapsibleNav({
basePath,
collapsibleNavHeaderRender,
id,
isLocked,
isNavOpen,
Expand Down Expand Up @@ -150,6 +152,7 @@ export function CollapsibleNav({
onClose={closeNav}
outsideClickCloses={false}
>
{collapsibleNavHeaderRender && collapsibleNavHeaderRender()}
{customNavLink && (
<Fragment>
<EuiFlexItem grow={false} style={{ flexShrink: 0 }}>
Expand Down
3 changes: 3 additions & 0 deletions src/core/public/chrome/ui/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export interface HeaderProps {
appTitle$: Observable<string>;
badge$: Observable<ChromeBadge | undefined>;
breadcrumbs$: Observable<ChromeBreadcrumb[]>;
collapsibleNavHeaderRender?: () => JSX.Element | null;
customNavLink$: Observable<ChromeNavLink | undefined>;
homeHref: string;
isVisible$: Observable<boolean>;
Expand Down Expand Up @@ -105,6 +106,7 @@ export function Header({
branding,
survey,
logos,
collapsibleNavHeaderRender,
...observables
}: HeaderProps) {
const isVisible = useObservable(observables.isVisible$, false);
Expand Down Expand Up @@ -246,6 +248,7 @@ export function Header({

<CollapsibleNav
appId$={application.currentAppId$}
collapsibleNavHeaderRender={collapsibleNavHeaderRender}
id={navId}
isLocked={isLocked}
navLinks$={observables.navLinks$}
Expand Down
2 changes: 2 additions & 0 deletions src/core/public/core_system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,12 @@ export class CoreSystem {
});
const application = this.application.setup({ context, http });
this.coreApp.setup({ application, http, injectedMetadata, notifications });
const chrome = this.chrome.setup();

const core: InternalCoreSetup = {
application,
context,
chrome,
fatalErrors: this.fatalErrorsSetup,
http,
injectedMetadata,
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 @@ -63,6 +63,7 @@ import {
ChromeNavLinks,
ChromeNavLinkUpdateableFields,
ChromeDocTitle,
ChromeSetup,
ChromeStart,
ChromeRecentlyAccessed,
ChromeRecentlyAccessedHistoryItem,
Expand Down Expand Up @@ -221,6 +222,8 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
* @deprecated
*/
context: ContextSetup;
/** {@link ChromeSetup} */
chrome: ChromeSetup;
/** {@link FatalErrorsSetup} */
fatalErrors: FatalErrorsSetup;
/** {@link HttpSetup} */
Expand Down
1 change: 1 addition & 0 deletions src/core/public/plugins/plugin_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export function createPluginSetupContext<
registerMountContext: (contextName, provider) =>
deps.application.registerMountContext(plugin.opaqueId, contextName, provider),
},
chrome: deps.chrome,
context: deps.context,
fatalErrors: deps.fatalErrors,
http: deps.http,
Expand Down
1 change: 1 addition & 0 deletions src/core/public/plugins/plugins_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ describe('PluginsService', () => {
];
mockSetupDeps = {
application: applicationServiceMock.createInternalSetupContract(),
chrome: chromeServiceMock.createSetupContract(),
context: contextServiceMock.createSetupContract(),
fatalErrors: fatalErrorsServiceMock.createSetupContract(),
http: httpServiceMock.createSetupContract(),
Expand Down
Loading