From 49a7f2a248aa53ff89c940307f0cbd1311b38c6f Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Sat, 7 Oct 2023 16:15:31 +0800 Subject: [PATCH 1/4] feat: Introduce `registerCollapsibleNavHeader` to ChromeService This commit introduces an enhancement to the ChromeService class within our core application. It adds a new method named `registerCollapsibleNavHeader`, allowing plugins to customize the rendering of the collapsible navigation header in the global chrome UI. With this new capability, plugins can now register their own rendering logic for the collapsible navigation header. This feature enhances the extensibility of our core system, empowering plugins to provide a more tailored and user-friendly navigation experience. Key changes in this commit include: 1. The addition of the `registerCollapsibleNavHeader` method to the ChromeService class. 2. Appropriate updates to tests and typings to support the newly introduced functionality. Signed-off-by: Yulong Ruan --- src/core/public/chrome/chrome_service.mock.ts | 8 ++++++ src/core/public/chrome/chrome_service.tsx | 26 +++++++++++++++++++ src/core/public/chrome/index.ts | 1 + .../chrome/ui/header/collapsible_nav.tsx | 3 +++ src/core/public/chrome/ui/header/header.tsx | 3 +++ src/core/public/core_system.ts | 2 ++ src/core/public/index.ts | 3 +++ src/core/public/plugins/plugin_context.ts | 1 + .../public/plugins/plugins_service.test.ts | 1 + 9 files changed, 48 insertions(+) diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 14b516ff95bc..b6ce429528a7 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -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 = { getHeaderComponent: jest.fn(), @@ -95,6 +101,7 @@ const createStartContractMock = () => { type ChromeServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { + setup: jest.fn(), start: jest.fn(), stop: jest.fn(), }; @@ -105,4 +112,5 @@ const createMock = () => { export const chromeServiceMock = { create: createMock, createStartContract: createStartContractMock, + createSetupContract: createSetupContractMock, }; diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 37ac2ba508a1..1e5388891db6 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -98,6 +98,8 @@ export interface StartDeps { uiSettings: IUiSettingsClient; } +type CollapsibleNavHeaderRender = () => JSX.Element | null; + /** @internal */ export class ChromeService { private isVisible$!: Observable; @@ -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) {} @@ -142,6 +145,14 @@ export class ChromeService { ); } + public setup() { + return { + registerCollapsibleNavHeader: (render: CollapsibleNavHeaderRender) => { + this.collapsibleNavHeaderRender = render; + }, + }; + } + public async start({ application, docLinks, @@ -262,6 +273,7 @@ export class ChromeService { branding={injectedMetadata.getBranding()} logos={logos} survey={injectedMetadata.getSurvey()} + collapsibleNavHeaderRender={this.collapsibleNavHeaderRender} /> ), @@ -325,6 +337,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(() => ) + * ``` + */ +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 diff --git a/src/core/public/chrome/index.ts b/src/core/public/chrome/index.ts index 4cd43362767c..4004c2c323f9 100644 --- a/src/core/public/chrome/index.ts +++ b/src/core/public/chrome/index.ts @@ -32,6 +32,7 @@ export { ChromeBadge, ChromeBreadcrumb, ChromeService, + ChromeSetup, ChromeStart, InternalChromeStart, ChromeHelpExtension, diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index 8b178200114a..9c9223aa501b 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -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; @@ -106,6 +107,7 @@ interface Props { export function CollapsibleNav({ basePath, + collapsibleNavHeaderRender, id, isLocked, isNavOpen, @@ -150,6 +152,7 @@ export function CollapsibleNav({ onClose={closeNav} outsideClickCloses={false} > + {collapsibleNavHeaderRender && collapsibleNavHeaderRender()} {customNavLink && ( diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 8eb594802b8d..2ca0f2548942 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -72,6 +72,7 @@ export interface HeaderProps { appTitle$: Observable; badge$: Observable; breadcrumbs$: Observable; + collapsibleNavHeaderRender?: () => JSX.Element | null; customNavLink$: Observable; homeHref: string; isVisible$: Observable; @@ -105,6 +106,7 @@ export function Header({ branding, survey, logos, + collapsibleNavHeaderRender, ...observables }: HeaderProps) { const isVisible = useObservable(observables.isVisible$, false); @@ -246,6 +248,7 @@ export function Header({ deps.application.registerMountContext(plugin.opaqueId, contextName, provider), }, + chrome: deps.chrome, context: deps.context, fatalErrors: deps.fatalErrors, http: deps.http, diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index acca058fe657..2135e92d87b8 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -103,6 +103,7 @@ describe('PluginsService', () => { ]; mockSetupDeps = { application: applicationServiceMock.createInternalSetupContract(), + chrome: chromeServiceMock.createSetupContract(), context: contextServiceMock.createSetupContract(), fatalErrors: fatalErrorsServiceMock.createSetupContract(), http: httpServiceMock.createSetupContract(), From e2efe08c94f614073e069f874b913574f9843cf7 Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Thu, 12 Oct 2023 17:56:58 +0800 Subject: [PATCH 2/4] Add changelog for custom nav menu header register + tests Signed-off-by: Yulong Ruan --- CHANGELOG.md | 3 ++- src/core/public/chrome/chrome_service.test.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c3235a2cca5..7057818099d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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)) \ No newline at end of file diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index e91056ed7766..eb73405118b6 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -108,6 +108,22 @@ afterAll(() => { (window as any).localStorage = originalLocalStorage; }); +describe('setup', () => { + 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); + }); +}); + describe('start', () => { it('adds legacy browser warning if browserSupportsCsp is disabled and warnLegacyBrowsers is enabled', async () => { const { startDeps } = await start({ options: { browserSupportsCsp: false } }); From 45942885a2cc8fd9d3685bf47df7d4fee4cb80e8 Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Wed, 18 Oct 2023 11:28:42 +0800 Subject: [PATCH 3/4] Add a warning message to console when custom nav bar header been overridden Signed-off-by: Yulong Ruan --- src/core/public/chrome/chrome_service.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 1e5388891db6..57c9f11d9061 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -148,6 +148,12 @@ 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; }, }; From e973515ef06616039336b4d2fec1bdad5a23847a Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Wed, 18 Oct 2023 18:11:56 +0800 Subject: [PATCH 4/4] Add tests for registerCollapsibleNavHeader warning console output Signed-off-by: Yulong Ruan --- src/core/public/chrome/chrome_service.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index eb73405118b6..be879bb4b5e9 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -109,6 +109,10 @@ afterAll(() => { }); describe('setup', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + it('register custom Nav Header render', async () => { const customHeaderMock = React.createElement('TestCustomNavHeader'); const renderMock = jest.fn().mockReturnValue(customHeaderMock); @@ -122,6 +126,24 @@ describe('setup', () => { 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', () => {