diff --git a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.tsx b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.tsx index 9ef4f740167fe3..dc1ad36f01c5e9 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.tsx @@ -11,7 +11,7 @@ import { registerAnalyticsContextProviderMock } from './chrome_service.test.mock import { shallow, mount } from 'enzyme'; import React from 'react'; import * as Rx from 'rxjs'; -import { toArray } from 'rxjs'; +import { toArray, firstValueFrom } from 'rxjs'; import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks'; import { docLinksServiceMock } from '@kbn/core-doc-links-browser-mocks'; import { httpServiceMock } from '@kbn/core-http-browser-mocks'; @@ -556,6 +556,39 @@ describe('start', () => { `); }); }); + + describe('side nav', () => { + describe('isCollapsed$', () => { + it('should return false by default', async () => { + const { chrome, service } = await start(); + const isCollapsed = await firstValueFrom(chrome.sideNav.getIsCollapsed$()); + service.stop(); + expect(isCollapsed).toBe(false); + }); + + it('should read the localStorage value', async () => { + store.set('core.chrome.isSideNavCollapsed', 'true'); + const { chrome, service } = await start(); + const isCollapsed = await firstValueFrom(chrome.sideNav.getIsCollapsed$()); + service.stop(); + expect(isCollapsed).toBe(true); + }); + }); + + describe('setIsCollapsed', () => { + it('should update the isCollapsed$ observable', async () => { + const { chrome, service } = await start(); + const isCollapsed$ = chrome.sideNav.getIsCollapsed$(); + const isCollapsed = await firstValueFrom(isCollapsed$); + + chrome.sideNav.setIsCollapsed(!isCollapsed); + + const updatedIsCollapsed = await firstValueFrom(isCollapsed$); + service.stop(); + expect(updatedIsCollapsed).toBe(!isCollapsed); + }); + }); + }); }); describe('stop', () => { diff --git a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx index 3eb846cc15dc87..4605dd02fd2292 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx @@ -54,6 +54,7 @@ import type { InternalChromeStart } from './types'; import { HeaderTopBanner } from './ui/header/header_top_banner'; const IS_LOCKED_KEY = 'core.chrome.isLocked'; +const IS_SIDENAV_COLLAPSED_KEY = 'core.chrome.isSideNavCollapsed'; const SNAPSHOT_REGEX = /-snapshot/i; interface ConstructorParams { @@ -86,7 +87,9 @@ export class ChromeService { private readonly docTitle = new DocTitleService(); private readonly projectNavigation: ProjectNavigationService; private mutationObserver: MutationObserver | undefined; - private readonly isSideNavCollapsed$ = new BehaviorSubject(true); + private readonly isSideNavCollapsed$ = new BehaviorSubject( + localStorage.getItem(IS_SIDENAV_COLLAPSED_KEY) === 'true' + ); private logger: Logger; private isServerless = false; @@ -360,6 +363,11 @@ export class ChromeService { projectNavigation.setProjectName(projectName); }; + const setIsSideNavCollapsed = (isCollapsed: boolean) => { + localStorage.setItem(IS_SIDENAV_COLLAPSED_KEY, JSON.stringify(isCollapsed)); + this.isSideNavCollapsed$.next(isCollapsed); + }; + if (!this.params.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) { notifications.toasts.addWarning({ title: mountReactNode( @@ -431,9 +439,8 @@ export class ChromeService { docLinks={docLinks} kibanaVersion={injectedMetadata.getKibanaVersion()} prependBasePath={http.basePath.prepend} - toggleSideNav={(isCollapsed) => { - this.isSideNavCollapsed$.next(isCollapsed); - }} + isSideNavCollapsed$={this.isSideNavCollapsed$} + toggleSideNav={setIsSideNavCollapsed} > @@ -556,7 +563,10 @@ export class ChromeService { getBodyClasses$: () => bodyClasses$.pipe(takeUntil(this.stop$)), setChromeStyle, getChromeStyle$: () => chromeStyle$, - getIsSideNavCollapsed$: () => this.isSideNavCollapsed$.asObservable(), + sideNav: { + getIsCollapsed$: () => this.isSideNavCollapsed$.asObservable(), + setIsCollapsed: setIsSideNavCollapsed, + }, getActiveSolutionNavId$: () => projectNavigation.getActiveSolutionNavId$(), project: { setHome: setProjectHome, diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.test.tsx b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.test.tsx index 3e4bc7a8a1fe58..743cd1726e03e8 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.test.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.test.tsx @@ -35,6 +35,7 @@ describe('Header', () => { navControlsCenter$: Rx.of([]), navControlsRight$: Rx.of([]), customBranding$: Rx.of({}), + isSideNavCollapsed$: Rx.of(false), prependBasePath: (str) => `hello/world/${str}`, toggleSideNav: jest.fn(), }; diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.tsx b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.tsx index f3d92ff31638de..bf8b103709260b 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.tsx @@ -130,6 +130,7 @@ export interface Props { navControlsCenter$: Observable; navControlsRight$: Observable; prependBasePath: (url: string) => string; + isSideNavCollapsed$: Observable; toggleSideNav: (isCollapsed: boolean) => void; } @@ -248,7 +249,12 @@ export const ProjectHeader = ({ - {children} + + {children} + diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx index 9daf99bbbfc23a..a607d69fb06335 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx @@ -7,34 +7,28 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useEffect, useRef, FC, PropsWithChildren } from 'react'; +import React, { FC, PropsWithChildren } from 'react'; import { EuiCollapsibleNavBeta } from '@elastic/eui'; -import useLocalStorage from 'react-use/lib/useLocalStorage'; +import useObservable from 'react-use/lib/useObservable'; +import type { Observable } from 'rxjs'; -const LOCAL_STORAGE_IS_COLLAPSED_KEY = 'PROJECT_NAVIGATION_COLLAPSED' as const; +interface Props { + toggleSideNav: (isVisible: boolean) => void; + isSideNavCollapsed$: Observable; +} -export const ProjectNavigation: FC< - PropsWithChildren<{ toggleSideNav: (isVisible: boolean) => void }> -> = ({ children, toggleSideNav }) => { - const isMounted = useRef(false); - const [isCollapsed, setIsCollapsed] = useLocalStorage(LOCAL_STORAGE_IS_COLLAPSED_KEY, false); - const onCollapseToggle = (nextIsCollapsed: boolean) => { - setIsCollapsed(nextIsCollapsed); - toggleSideNav(nextIsCollapsed); - }; - - useEffect(() => { - if (!isMounted.current && isCollapsed !== undefined) { - toggleSideNav(isCollapsed); - } - isMounted.current = true; - }, [isCollapsed, toggleSideNav]); +export const ProjectNavigation: FC> = ({ + children, + isSideNavCollapsed$, + toggleSideNav, +}) => { + const isCollapsed = useObservable(isSideNavCollapsed$, false); return ( { setBadge: jest.fn(), getBreadcrumbs$: jest.fn(), setBreadcrumbs: jest.fn(), - getIsSideNavCollapsed$: jest.fn(), + sideNav: { + getIsCollapsed$: jest.fn(), + setIsCollapsed: jest.fn(), + }, getBreadcrumbsAppendExtension$: jest.fn(), setBreadcrumbsAppendExtension: jest.fn(), getGlobalHelpExtensionMenuLinks$: jest.fn(), @@ -94,7 +97,7 @@ const createStartContractMock = () => { startContract.getIsNavDrawerLocked$.mockReturnValue(new BehaviorSubject(false)); startContract.getBodyClasses$.mockReturnValue(new BehaviorSubject([])); startContract.hasHeaderBanner$.mockReturnValue(new BehaviorSubject(false)); - startContract.getIsSideNavCollapsed$.mockReturnValue(new BehaviorSubject(false)); + startContract.sideNav.getIsCollapsed$.mockReturnValue(new BehaviorSubject(false)); return startContract; }; diff --git a/packages/core/chrome/core-chrome-browser/src/contracts.ts b/packages/core/chrome/core-chrome-browser/src/contracts.ts index c326e7107aa2af..1e9ea66bc0920e 100644 --- a/packages/core/chrome/core-chrome-browser/src/contracts.ts +++ b/packages/core/chrome/core-chrome-browser/src/contracts.ts @@ -173,10 +173,18 @@ export interface ChromeStart { */ getChromeStyle$(): Observable; - /** - * Get an observable of the current collapsed state of the side nav. - */ - getIsSideNavCollapsed$(): Observable; + sideNav: { + /** + * Get an observable of the current collapsed state of the side nav. + */ + getIsCollapsed$(): Observable; + + /** + * Set the collapsed state of the side nav. + * @param isCollapsed The collapsed state of the side nav. + */ + setIsCollapsed(isCollapsed: boolean): void; + }; /** * Get the id of the currently active project navigation or `null` otherwise. diff --git a/packages/shared-ux/chrome/navigation/src/services.tsx b/packages/shared-ux/chrome/navigation/src/services.tsx index fec11110f8b6ad..1b0102533e53e0 100644 --- a/packages/shared-ux/chrome/navigation/src/services.tsx +++ b/packages/shared-ux/chrome/navigation/src/services.tsx @@ -36,7 +36,7 @@ export const NavigationKibanaProvider: FC ({ diff --git a/packages/shared-ux/chrome/navigation/src/types.ts b/packages/shared-ux/chrome/navigation/src/types.ts index c7f882c3580a6d..9db808e37799a6 100644 --- a/packages/shared-ux/chrome/navigation/src/types.ts +++ b/packages/shared-ux/chrome/navigation/src/types.ts @@ -53,7 +53,9 @@ export interface NavigationKibanaDependencies { navLinks: { getNavLinks$: () => Observable>; }; - getIsSideNavCollapsed$: () => Observable; + sideNav: { + getIsCollapsed$: () => Observable; + }; }; http: { basePath: BasePathService;