diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 3775989c5126b..a67b9caae40be 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -56,6 +56,8 @@ const createStartContractMock = () => { getIsVisible$: jest.fn(), setIsCollapsed: jest.fn(), getIsCollapsed$: jest.fn(), + setIsNavLocked: jest.fn(), + getIsNavLocked$: jest.fn(), addApplicationClass: jest.fn(), removeApplicationClass: jest.fn(), getApplicationClasses$: jest.fn(), @@ -70,6 +72,7 @@ const createStartContractMock = () => { startContract.getBrand$.mockReturnValue(new BehaviorSubject({} as ChromeBrand)); startContract.getIsVisible$.mockReturnValue(new BehaviorSubject(false)); startContract.getIsCollapsed$.mockReturnValue(new BehaviorSubject(false)); + startContract.getIsNavLocked$.mockReturnValue(new BehaviorSubject(false)); startContract.getApplicationClasses$.mockReturnValue(new BehaviorSubject(['class-name'])); startContract.getBadge$.mockReturnValue(new BehaviorSubject({} as ChromeBadge)); startContract.getBreadcrumbs$.mockReturnValue(new BehaviorSubject([{} as ChromeBreadcrumb])); diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 45e94040eeb4a..30ffe02b2cd3d 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -211,6 +211,42 @@ Array [ }); }); + describe('is nav locked', () => { + it('updates/emits isNavLocked', async () => { + const service = new ChromeService({ browserSupportsCsp: true }); + const start = await service.start(defaultStartDeps()); + const promise = start + .getIsNavLocked$() + .pipe(toArray()) + .toPromise(); + + start.setIsNavLocked(true); + start.setIsNavLocked(false); + start.setIsNavLocked(true); + service.stop(); + + await expect(promise).resolves.toMatchInlineSnapshot(` +Array [ + false, + true, + false, + true, +] +`); + }); + + it('only stores true in localStorage', async () => { + const service = new ChromeService({ browserSupportsCsp: true }); + const start = await service.start(defaultStartDeps()); + + start.setIsNavLocked(true); + expect(store.size).toBe(1); + + start.setIsNavLocked(false); + expect(store.size).toBe(0); + }); + }); + describe('application classes', () => { it('updates/emits the application classes', async () => { const service = new ChromeService({ browserSupportsCsp: true }); @@ -361,13 +397,14 @@ Array [ }); describe('stop', () => { - it('completes applicationClass$, isCollapsed$, breadcrumbs$, isVisible$, and brand$ observables', async () => { + it('completes applicationClass$, isCollapsed$, isNavLocked$, breadcrumbs$, isVisible$, and brand$ observables', async () => { const service = new ChromeService({ browserSupportsCsp: true }); const start = await service.start(defaultStartDeps()); const promise = Rx.combineLatest( start.getBrand$(), start.getApplicationClasses$(), start.getIsCollapsed$(), + start.getIsNavLocked$(), start.getBreadcrumbs$(), start.getIsVisible$(), start.getHelpExtension$() @@ -387,6 +424,7 @@ describe('stop', () => { start.getBrand$(), start.getApplicationClasses$(), start.getIsCollapsed$(), + start.getIsNavLocked$(), start.getBreadcrumbs$(), start.getIsVisible$(), start.getHelpExtension$() diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 02195c794d280..9aa2ded9026cc 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -33,12 +33,13 @@ import { HttpStart } from '../http'; import { ChromeNavLinks, NavLinksService } from './nav_links'; import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed'; import { NavControlsService, ChromeNavControls } from './nav_controls'; -import { LoadingIndicator, HeaderWrapper as Header } from './ui'; +import { LoadingIndicator, Header } from './ui'; import { DocLinksStart } from '../doc_links'; export { ChromeNavControls, ChromeRecentlyAccessed }; const IS_COLLAPSED_KEY = 'core.chrome.isCollapsed'; +export const IS_NAV_LOCKED_KEY = 'core.chrome.isNavLocked'; function isEmbedParamInHash() { const { query } = Url.parse(String(window.location.hash).slice(1), true); @@ -103,6 +104,7 @@ export class ChromeService { const brand$ = new BehaviorSubject({}); const isVisible$ = new BehaviorSubject(true); const isCollapsed$ = new BehaviorSubject(!!localStorage.getItem(IS_COLLAPSED_KEY)); + const isNavLocked$ = new BehaviorSubject(!!localStorage.getItem(IS_NAV_LOCKED_KEY)); const applicationClasses$ = new BehaviorSubject>(new Set()); const helpExtension$ = new BehaviorSubject(undefined); const breadcrumbs$ = new BehaviorSubject([]); @@ -120,6 +122,15 @@ export class ChromeService { ); } + const setIsNavLocked = (isNavLocked: boolean) => { + isNavLocked$.next(isNavLocked); + if (isNavLocked) { + localStorage.setItem(IS_NAV_LOCKED_KEY, 'true'); + } else { + localStorage.removeItem(IS_NAV_LOCKED_KEY); + } + }; + return { navControls, navLinks, @@ -143,6 +154,8 @@ export class ChromeService { map(visibility => (FORCE_HIDDEN ? false : visibility)), takeUntil(this.stop$) )} + isNavLocked$={isNavLocked$.pipe(takeUntil(this.stop$))} + onIsNavLockedUpdate={setIsNavLocked} kibanaVersion={injectedMetadata.getKibanaVersion()} legacyMode={injectedMetadata.getLegacyMode()} navLinks$={navLinks.getNavLinks$()} @@ -175,7 +188,6 @@ export class ChromeService { setIsVisible: (visibility: boolean) => { isVisible$.next(visibility); }, - getIsCollapsed$: () => isCollapsed$.pipe(takeUntil(this.stop$)), setIsCollapsed: (isCollapsed: boolean) => { @@ -187,6 +199,10 @@ export class ChromeService { } }, + getIsNavLocked$: () => isNavLocked$.pipe(takeUntil(this.stop$)), + + setIsNavLocked, + getApplicationClasses$: () => applicationClasses$.pipe( map(set => [...set]), @@ -321,6 +337,16 @@ export interface ChromeStart { */ setIsCollapsed(isCollapsed: boolean): void; + /** + * Get an observable of the current locked open state of the chrome. + */ + getIsNavLocked$(): Observable; + + /** + * Set the locked open state of the chrome navigation. + */ + setIsNavLocked(isCollapsed: boolean): void; + /** * Get the current set of classNames that will be set on the application container. */ diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index afd9f8e4a3820..33855a5addb86 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -21,6 +21,7 @@ import Url from 'url'; import React, { Component, createRef, Fragment } from 'react'; import * as Rx from 'rxjs'; +import classnames from 'classnames'; import { // TODO: add type annotations @@ -64,7 +65,7 @@ import { ChromeNavControl, } from '../..'; import { HttpStart } from '../../../http'; -import { ChromeHelpExtension } from '../../chrome_service'; +import { ChromeHelpExtension, IS_NAV_LOCKED_KEY } from '../../chrome_service'; import { ApplicationStart, InternalApplicationStart } from '../../../application/types'; // Providing a buffer between the limit and the cut off index @@ -177,8 +178,8 @@ interface Props { navControlsRight$: Rx.Observable; intl: InjectedIntl; basePath: HttpStart['basePath']; - isLocked?: boolean; - onIsLockedUpdate?: (isLocked: boolean) => void; + isNavLocked$: Rx.Observable; + onIsNavLockedUpdate?: (isNavLocked: boolean) => void; } interface State { @@ -190,6 +191,7 @@ interface State { forceNavigation: boolean; navControlsLeft: readonly ChromeNavControl[]; navControlsRight: readonly ChromeNavControl[]; + isNavLocked: boolean; } class HeaderUI extends Component { @@ -207,6 +209,7 @@ class HeaderUI extends Component { forceNavigation: false, navControlsLeft: [], navControlsRight: [], + isNavLocked: !!localStorage.getItem(IS_NAV_LOCKED_KEY), }; } @@ -221,7 +224,8 @@ class HeaderUI extends Component { Rx.combineLatest( this.props.navControlsLeft$, this.props.navControlsRight$, - this.props.application.currentAppId$ + this.props.application.currentAppId$, + this.props.isNavLocked$ ) ).subscribe({ next: ([ @@ -230,7 +234,7 @@ class HeaderUI extends Component { forceNavigation, navLinks, recentlyAccessed, - [navControlsLeft, navControlsRight, currentAppId], + [navControlsLeft, navControlsRight, currentAppId, isNavLocked], ]) => { this.setState({ appTitle, @@ -245,6 +249,7 @@ class HeaderUI extends Component { navControlsLeft, navControlsRight, currentAppId, + isNavLocked, }); }, }); @@ -291,10 +296,9 @@ class HeaderUI extends Component { breadcrumbs$, helpExtension$, intl, - isLocked, kibanaDocLink, kibanaVersion, - onIsLockedUpdate, + onIsNavLockedUpdate, legacyMode, } = this.props; const { @@ -305,6 +309,7 @@ class HeaderUI extends Component { navControlsRight, navLinks, recentlyAccessed, + isNavLocked, } = this.state; if (!isVisible) { @@ -375,8 +380,16 @@ class HeaderUI extends Component { }, ]; + const className = classnames( + 'chrHeaderWrapper', + { + 'chrHeaderWrapper--navIsLocked': isNavLocked, + }, + 'hide-for-sharing' + ); + return ( - +
@@ -404,14 +417,14 @@ class HeaderUI extends Component { - +
); } diff --git a/src/core/public/chrome/ui/header/header_wrapper.tsx b/src/core/public/chrome/ui/header/header_wrapper.tsx deleted file mode 100644 index 917fbfc909c50..0000000000000 --- a/src/core/public/chrome/ui/header/header_wrapper.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { FunctionComponent, useState } from 'react'; -import classnames from 'classnames'; -import { Header, HeaderProps } from './'; - -export const IS_NAV_LOCKED_KEY = 'core.chrome.isLocked'; - -export const HeaderWrapper: FunctionComponent = props => { - const initialIsLocked = localStorage.getItem(IS_NAV_LOCKED_KEY); - const [isLocked, setIsLocked] = useState(initialIsLocked === 'true'); - const setIsLockedStored = (locked: boolean) => { - localStorage.setItem(IS_NAV_LOCKED_KEY, `${locked}`); - setIsLocked(locked); - }; - const className = classnames( - 'chrHeaderWrapper', - { - 'chrHeaderWrapper--navIsLocked': isLocked, - }, - 'hide-for-sharing' - ); - return ( -
-
-
- ); -}; diff --git a/src/core/public/chrome/ui/header/index.ts b/src/core/public/chrome/ui/header/index.ts index f9c122b864dce..f4c7127b93bfb 100644 --- a/src/core/public/chrome/ui/header/index.ts +++ b/src/core/public/chrome/ui/header/index.ts @@ -18,4 +18,3 @@ */ export { Header, HeaderProps } from './header'; -export { HeaderWrapper } from './header_wrapper'; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index b2d730d7fa467..ebebe2f4de4c5 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -186,35 +186,37 @@ export interface ChromeRecentlyAccessedHistoryItem { label: string; // (undocumented) link: string; -} - -// @public -export interface ChromeStart { - addApplicationClass(className: string): void; - getApplicationClasses$(): Observable; - getBadge$(): Observable; - getBrand$(): Observable; - getBreadcrumbs$(): Observable; - getHelpExtension$(): Observable; - getIsCollapsed$(): Observable; - getIsVisible$(): Observable; - navControls: ChromeNavControls; - navLinks: ChromeNavLinks; - recentlyAccessed: ChromeRecentlyAccessed; - removeApplicationClass(className: string): void; - setAppTitle(appTitle: string): void; - setBadge(badge?: ChromeBadge): void; - setBrand(brand: ChromeBrand): void; - setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void; - setHelpExtension(helpExtension?: ChromeHelpExtension): void; - setIsCollapsed(isCollapsed: boolean): void; - setIsVisible(isVisible: boolean): void; -} - -// @public -export interface ContextSetup { - createContextContainer(): IContextContainer; -} +} + +// @public +export interface ChromeStart { + addApplicationClass(className: string): void; + getApplicationClasses$(): Observable; + getBadge$(): Observable; + getBrand$(): Observable; + getBreadcrumbs$(): Observable; + getHelpExtension$(): Observable; + getIsCollapsed$(): Observable; + getIsVisible$(): Observable; + getIsNavLocked$(): Observable; + navControls: ChromeNavControls; + navLinks: ChromeNavLinks; + recentlyAccessed: ChromeRecentlyAccessed; + removeApplicationClass(className: string): void; + setAppTitle(appTitle: string): void; + setBadge(badge?: ChromeBadge): void; + setBrand(brand: ChromeBrand): void; + setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void; + setHelpExtension(helpExtension?: ChromeHelpExtension): void; + setIsCollapsed(isCollapsed: boolean): void; + setIsVisible(isVisible: boolean): void; + setIsNavLocked(isNavLocked: boolean): void; +} + +// @public +export interface ContextSetup { + createContextContainer(): IContextContainer; +} // @internal (undocumented) export interface CoreContext { diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index bb9676388015c..92093fe378bb2 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -46,6 +46,7 @@ interface State { to: string; }; }; + chromeNavIsLockedOpen?: boolean; } function isLocalStateDirty( @@ -75,7 +76,6 @@ export function App({ docStorage: SavedObjectStore; redirectTo: (id?: string) => void; }) { - const chromeNavIsLockedOpen = localStorage.getItem('core.chrome.isLocked'); const timeDefaults = core.uiSettings.get('timepicker:timeDefaults'); const language = store.get('kibana.userQueryLanguage') || core.uiSettings.get('search:queryLanguage'); @@ -98,10 +98,18 @@ export function App({ to: timeDefaults.to, }, }, + chromeNavIsLockedOpen: false, }); const lastKnownDocRef = useRef(undefined); + useEffect(() => { + const subscription = core.chrome + .getIsNavLocked$() + .subscribe(isNavLocked => setState({ ...state, chromeNavIsLockedOpen: isNavLocked })); + return subscription.unsubscribe; + }, []); + // Sync Kibana breadcrumbs any time the saved document's title changes useEffect(() => { core.chrome.setBreadcrumbs([ @@ -171,7 +179,7 @@ export function App({ }); const bottomBarClasses = classNames('lnsApp__bottomBar', { - 'lnsApp__bottomBar-navIsLockedOpen': chromeNavIsLockedOpen === 'true', + 'lnsApp__bottomBar-navIsLockedOpen': state.chromeNavIsLockedOpen, }); return (