From 22bbaf9769bace97940f03619c2156745c9d13d0 Mon Sep 17 00:00:00 2001 From: Josh Romero Date: Wed, 15 Jun 2022 00:48:54 +0000 Subject: [PATCH] [UX] Consolidate menu bars - Add expanded header option - Move navControlsRight to left of help - Rename HeaderLogo to HomeLoader - Serves as unified home button and global loading indicator - Add title for tooltip, because purpose may not be obvious - Rename Mark to HomeIcon - Uses home icon when header expanded by default - Restore HeaderLogo for expanded header - Duplicate default logo, rename to match mark naming (default, dark) - Add new nav control areas for expanded menu only - navControlsExpandedRight - navControlsExpandedCenter - Add new body class for expanded header - used by styles to correctly set app height - remove unecessary duplicate header mixin inclusion Signed-off-by: Josh Romero --- src/core/public/chrome/chrome_service.tsx | 2 + .../nav_controls/nav_controls_service.ts | 24 +++ .../header/__snapshots__/header.test.tsx.snap | 178 ++++++++++-------- .../home_icon.test.tsx.snap} | 64 +++---- src/core/public/chrome/ui/header/_index.scss | 8 +- .../public/chrome/ui/header/header.test.tsx | 2 + src/core/public/chrome/ui/header/header.tsx | 57 +++++- .../public/chrome/ui/header/header_logo.scss | 19 +- .../public/chrome/ui/header/header_logo.tsx | 163 ++++------------ .../mark.test.tsx => home_icon.test.tsx} | 34 ++-- .../{branding/mark.tsx => home_icon.tsx} | 39 ++-- .../public/chrome/ui/header/home_loader.scss | 14 ++ .../public/chrome/ui/header/home_loader.tsx | 143 ++++++++++++++ src/core/public/core_system.ts | 9 +- src/core/public/rendering/_base.scss | 6 +- ...logo.svg => opensearch_logo_dark_mode.svg} | 0 .../opensearch_logo_default_mode.svg | 95 ++++++++++ .../public/application/components/_home.scss | 4 + .../public/components/_overview.scss | 4 + 19 files changed, 571 insertions(+), 294 deletions(-) rename src/core/public/chrome/ui/header/{branding/__snapshots__/mark.test.tsx.snap => __snapshots__/home_icon.test.tsx.snap} (97%) rename src/core/public/chrome/ui/header/{branding/mark.test.tsx => home_icon.test.tsx} (85%) rename src/core/public/chrome/ui/header/{branding/mark.tsx => home_icon.tsx} (52%) create mode 100644 src/core/public/chrome/ui/header/home_loader.scss create mode 100644 src/core/public/chrome/ui/header/home_loader.tsx rename src/core/server/core_app/assets/default_branding/{opensearch_logo.svg => opensearch_logo_dark_mode.svg} (100%) create mode 100644 src/core/server/core_app/assets/default_branding/opensearch_logo_default_mode.svg diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index ebfa010b9431..da5cbda1c92a 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -257,6 +257,8 @@ export class ChromeService { navControlsLeft$={navControls.getLeft$()} navControlsCenter$={navControls.getCenter$()} navControlsRight$={navControls.getRight$()} + navControlsExpandedCenter$={navControls.getExpandedCenter$()} + navControlsExpandedRight$={navControls.getExpandedRight$()} onIsLockedUpdate={setIsNavDrawerLocked} isLocked$={getIsNavDrawerLocked$} branding={injectedMetadata.getBranding()} diff --git a/src/core/public/chrome/nav_controls/nav_controls_service.ts b/src/core/public/chrome/nav_controls/nav_controls_service.ts index 27ac136c18d5..57298dac39ff 100644 --- a/src/core/public/chrome/nav_controls/nav_controls_service.ts +++ b/src/core/public/chrome/nav_controls/nav_controls_service.ts @@ -78,6 +78,10 @@ export class NavControlsService { const navControlsLeft$ = new BehaviorSubject>(new Set()); const navControlsRight$ = new BehaviorSubject>(new Set()); const navControlsCenter$ = new BehaviorSubject>(new Set()); + const navControlsExpandedRight$ = new BehaviorSubject>(new Set()); + const navControlsExpandedCenter$ = new BehaviorSubject>( + new Set() + ); return { // In the future, registration should be moved to the setup phase. This @@ -91,6 +95,16 @@ export class NavControlsService { registerCenter: (navControl: ChromeNavControl) => navControlsCenter$.next(new Set([...navControlsCenter$.value.values(), navControl])), + registerExpandedRight: (navControl: ChromeNavControl) => + navControlsExpandedRight$.next( + new Set([...navControlsExpandedRight$.value.values(), navControl]) + ), + + registerExpandedCenter: (navControl: ChromeNavControl) => + navControlsExpandedCenter$.next( + new Set([...navControlsExpandedCenter$.value.values(), navControl]) + ), + getLeft$: () => navControlsLeft$.pipe( map((controls) => sortBy([...controls.values()], 'order')), @@ -106,6 +120,16 @@ export class NavControlsService { map((controls) => sortBy([...controls.values()], 'order')), takeUntil(this.stop$) ), + getExpandedRight$: () => + navControlsExpandedRight$.pipe( + map((controls) => sortBy([...controls.values()], 'order')), + takeUntil(this.stop$) + ), + getExpandedCenter$: () => + navControlsExpandedCenter$.pipe( + map((controls) => sortBy([...controls.values()], 'order')), + takeUntil(this.stop$) + ), }; } diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index fb36748a96f1..5b7fbe1671a7 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -1449,6 +1449,28 @@ exports[`Header renders 1`] = ` "thrownError": null, } } + navControlsExpandedCenter$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + } + } + navControlsExpandedRight$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + } + } navControlsLeft$={ BehaviorSubject { "_isScalar": false, @@ -1886,7 +1908,7 @@ exports[`Header renders 1`] = `
-
, } } - className="euiHeaderSectionItemButton header__logoNavButton" + className="euiHeaderSectionItemButton header__homeLoaderNavButton" color="text" - data-test-subj="logo" + data-test-subj="homeLoader" href="/" onClick={[Function]} + title="Go to home page" >
- - +
- +
@@ -2708,6 +2734,65 @@ exports[`Header renders 1`] = ` />
+ +
+ +
+
@@ -4347,65 +4432,6 @@ exports[`Header renders 1`] = ` - -
- -
-
diff --git a/src/core/public/chrome/ui/header/branding/__snapshots__/mark.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/home_icon.test.tsx.snap similarity index 97% rename from src/core/public/chrome/ui/header/branding/__snapshots__/mark.test.tsx.snap rename to src/core/public/chrome/ui/header/__snapshots__/home_icon.test.tsx.snap index a375b510f143..02ad6d21cb8f 100644 --- a/src/core/public/chrome/ui/header/branding/__snapshots__/mark.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/home_icon.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Header logo in dark mode uses custom mark dark mode URL 1`] = ` - - + `; exports[`Header logo in dark mode uses custom mark default mode URL if no dark mode mark 1`] = ` - - + `; exports[`Header logo in dark mode uses opensearch logo if custom logo provided without mark 1`] = ` - - + `; exports[`Header logo in dark mode uses opensearch logo if no mark provided 1`] = ` - - + `; exports[`Header logo in default mode uses custom mark default mode URL 1`] = ` - - + `; exports[`Header logo in default mode uses opensearch logo if custom logo provided without mark 1`] = ` - - + `; exports[`Header logo in default mode uses opensearch logo if no branding 1`] = ` - - + `; exports[`Header logo in default mode uses opensearch logo if no mark provided 1`] = ` - - + `; diff --git a/src/core/public/chrome/ui/header/_index.scss b/src/core/public/chrome/ui/header/_index.scss index 44cd86427832..003f41d39fa1 100644 --- a/src/core/public/chrome/ui/header/_index.scss +++ b/src/core/public/chrome/ui/header/_index.scss @@ -1,4 +1,10 @@ -@include euiHeaderAffordForFixed; +:not(.headerIsExpanded) { + @include euiHeaderAffordForFixed($osdHeaderOffset); +} + +.headerIsExpanded { + @include euiHeaderAffordForFixed($osdHeaderOffset * 2); +} .chrHeaderHelpMenu__version { text-transform: none; diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx index 41b9282848b4..b42924b0fa99 100644 --- a/src/core/public/chrome/ui/header/header.test.tsx +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -63,6 +63,8 @@ function mockProps() { navControlsLeft$: new BehaviorSubject([]), navControlsCenter$: new BehaviorSubject([]), navControlsRight$: new BehaviorSubject([]), + navControlsExpandedCenter$: new BehaviorSubject([]), + navControlsExpandedRight$: new BehaviorSubject([]), basePath: http.basePath, isLocked$: new BehaviorSubject(false), loadingCount$: new BehaviorSubject(0), diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 56c0ff0c3489..893b79e197d5 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -34,7 +34,9 @@ import { EuiHeaderSection, EuiHeaderSectionItem, EuiHeaderSectionItemButton, + EuiHideFor, EuiIcon, + EuiShowFor, htmlIdGenerator, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; @@ -58,9 +60,10 @@ import { CollapsibleNav } from './collapsible_nav'; import { HeaderBadge } from './header_badge'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; import { HeaderHelpMenu } from './header_help_menu'; -import { HeaderLogo } from './header_logo'; +import { HomeLoader } from './home_loader'; import { HeaderNavControls } from './header_nav_controls'; import { HeaderActionMenu } from './header_action_menu'; +import { HeaderLogo } from './header_logo'; export interface HeaderProps { opensearchDashboardsVersion: string; @@ -80,6 +83,8 @@ export interface HeaderProps { navControlsLeft$: Observable; navControlsCenter$: Observable; navControlsRight$: Observable; + navControlsExpandedCenter$: Observable; + navControlsExpandedRight$: Observable; basePath: HttpStart['basePath']; isLocked$: Observable; loadingCount$: ReturnType; @@ -108,11 +113,47 @@ export function Header({ const toggleCollapsibleNavRef = createRef void }>(); const navId = htmlIdGenerator()(); const className = classnames('hide-for-sharing', 'headerGlobalNav'); + const { useExpandedMenu } = branding; return ( <>
+ {useExpandedMenu && ( + ], + borders: 'none', + }, + { + ...(observables.navControlsExpandedCenter$ && { + items: [ + + + , + ], + }), + borders: 'none', + }, + { + ...((observables.navControlsExpandedCenter$ || + observables.navControlsExpandedRight$) && { + items: [ + + + , + , + ], + }), + borders: 'none', + }, + ]} + /> + )} + @@ -138,7 +179,7 @@ export function Header({ )} - )} + {observables.navControlsRight$ && ( + + + + )} + - - {observables.navControlsRight$ && ( - - - - )}
diff --git a/src/core/public/chrome/ui/header/header_logo.scss b/src/core/public/chrome/ui/header/header_logo.scss index 5b814ffd3351..7835863e60c0 100644 --- a/src/core/public/chrome/ui/header/header_logo.scss +++ b/src/core/public/chrome/ui/header/header_logo.scss @@ -1,14 +1,9 @@ -.header__logoNavButton .euiHeaderSectionItemButton__content { - // avoid layout shift on smallest breakpoint if logo and loading indicator are different sizes - min-width: 24px; - display: grid; - grid-template-columns: 1fr; - grid-template-rows: 1fr; +.logoContainer { + height: 30px; + padding: 3px 3px 3px 10px; +} - & .loaderContainer, - & .logoContainer { - grid-area: 1 / 1; - align-self: center; - justify-self: center; - } +.logoImage { + height: 100%; + max-width: 100%; } diff --git a/src/core/public/chrome/ui/header/header_logo.tsx b/src/core/public/chrome/ui/header/header_logo.tsx index dbb349454f72..13c4ac81f2d5 100644 --- a/src/core/public/chrome/ui/header/header_logo.tsx +++ b/src/core/public/chrome/ui/header/header_logo.tsx @@ -1,140 +1,45 @@ /* + * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * 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 './header_logo.scss'; - -import { i18n } from '@osd/i18n'; import React from 'react'; -import useObservable from 'react-use/lib/useObservable'; -import { Observable } from 'rxjs'; -import Url from 'url'; -import { EuiHeaderSectionItemButton } from '@elastic/eui'; -import { ChromeNavLink } from '../..'; import { ChromeBranding } from '../../chrome_service'; -import { LoadingIndicator } from '../loading_indicator'; -import { Mark } from './branding/mark'; - -function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void { - let current = element; - while (current) { - if (current.tagName === 'A') { - return current as HTMLAnchorElement; - } - - if (!current.parentElement || current.parentElement === document.body) { - return undefined; - } - current = current.parentElement; - } -} - -function onClick( - event: React.MouseEvent, - forceNavigation: boolean, - navLinks: ChromeNavLink[], - navigateToApp: (appId: string) => void -) { - const anchor = findClosestAnchor((event as any).nativeEvent.target); - if (!anchor) { - return; - } - - const navLink = navLinks.find((item) => item.href === anchor.href); - if (navLink && navLink.disabled) { - event.preventDefault(); - return; - } - - if (event.isDefaultPrevented() || event.altKey || event.metaKey || event.ctrlKey) { - return; - } - - if (forceNavigation) { - const toParsed = Url.parse(anchor.href); - const fromParsed = Url.parse(document.location.href); - const sameProto = toParsed.protocol === fromParsed.protocol; - const sameHost = toParsed.host === fromParsed.host; - const samePath = toParsed.path === fromParsed.path; - - if (sameProto && sameHost && samePath) { - if (toParsed.hash) { - document.location.reload(); - } - - // event.preventDefault() keeps the browser from seeing the new url as an update - // and even setting window.location does not mimic that behavior, so instead - // we use stopPropagation() to prevent angular from seeing the click and - // starting a digest cycle/attempting to handle it in the router. - event.stopPropagation(); - } - } else { - navigateToApp('home'); - event.preventDefault(); - } -} - -interface Props { - href: string; - navLinks$: Observable; - forceNavigation$: Observable; - loadingCount$: Observable; - navigateToApp: (appId: string) => void; - branding: ChromeBranding; -} - -export function HeaderLogo({ href, navigateToApp, branding, ...observables }: Props) { - const forceNavigation = useObservable(observables.forceNavigation$, false); - const navLinks = useObservable(observables.navLinks$, []); - const loadingCount = useObservable(observables.loadingCount$, 0); +/** + * Use branding configurations to render the logo on the nav bar. + * + * @param {ChromeBranding} - branding object consist of logo, darkmode selection, asset path and title + * @returns Logo component + */ +export const HeaderLogo = ({ + darkMode, + assetFolderUrl = '', + logo, + applicationTitle = 'opensearch dashboards', +}: ChromeBranding) => { + const { defaultUrl: logoUrl, darkModeUrl: darkLogoUrl } = logo ?? {}; + + const customLogo = darkMode ? darkLogoUrl ?? logoUrl : logoUrl; + const defaultLogo = darkMode + ? 'opensearch_logo_dark_mode.svg' + : 'opensearch_logo_default_mode.svg'; + + const logoSrc = customLogo ? customLogo : `${assetFolderUrl}/${defaultLogo}`; + const testSubj = customLogo ? 'customLogo' : 'defaultLogo'; + const alt = `${applicationTitle} logo`; return ( - ) => - onClick(e, forceNavigation, navLinks, navigateToApp) - } - href={href} - aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel', { - defaultMessage: 'Go to home page', - })} - > - {!(loadingCount > 0) && ( -
- -
- )} -
- -
-
+
+ {alt} +
); -} +}; diff --git a/src/core/public/chrome/ui/header/branding/mark.test.tsx b/src/core/public/chrome/ui/header/home_icon.test.tsx similarity index 85% rename from src/core/public/chrome/ui/header/branding/mark.test.tsx rename to src/core/public/chrome/ui/header/home_icon.test.tsx index 2f36ae680803..80d979eaea53 100644 --- a/src/core/public/chrome/ui/header/branding/mark.test.tsx +++ b/src/core/public/chrome/ui/header/home_icon.test.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { Mark } from './mark'; +import { HomeIcon } from './home_icon'; const defaultOpensearchMarkUrl = '/opensearch_mark_default_mode.svg'; const darkOpensearchMarkUrl = '/opensearch_mark_dark_mode.svg'; @@ -14,10 +14,10 @@ describe('Header logo ', () => { describe('in default mode ', () => { it('uses opensearch logo if no branding', () => { const branding = {}; - const component = mountWithIntl(); + const component = mountWithIntl(); const icon = component.find('EuiIcon'); expect(icon.prop('type')).toEqual(defaultOpensearchMarkUrl); - expect(icon.prop('title')).toEqual(`opensearch dashboards logo`); + expect(icon.prop('title')).toEqual(`opensearch dashboards home`); expect(component).toMatchSnapshot(); }); @@ -29,10 +29,10 @@ describe('Header logo ', () => { applicationTitle: 'custom title', assetFolderUrl: 'ui/assets', }; - const component = mountWithIntl(); + const component = mountWithIntl(); const icon = component.find('EuiIcon'); expect(icon.prop('type')).toEqual(`${branding.assetFolderUrl}${defaultOpensearchMarkUrl}`); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} logo`); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); expect(component).toMatchSnapshot(); }); @@ -43,10 +43,10 @@ describe('Header logo ', () => { mark: {}, applicationTitle: 'custom title', }; - const component = mountWithIntl(); + const component = mountWithIntl(); const icon = component.find('EuiIcon'); expect(icon.prop('type')).toEqual(defaultOpensearchMarkUrl); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} logo`); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); expect(component).toMatchSnapshot(); }); @@ -57,10 +57,10 @@ describe('Header logo ', () => { mark: { defaultUrl: '/defaultModeMark' }, applicationTitle: 'custom title', }; - const component = mountWithIntl(); + const component = mountWithIntl(); const icon = component.find('EuiIcon'); expect(icon.prop('type')).toEqual(branding.mark.defaultUrl); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} logo`); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); expect(component).toMatchSnapshot(); }); }); @@ -74,10 +74,10 @@ describe('Header logo ', () => { assetFolderUrl: 'ui/assets', applicationTitle: 'custom title', }; - const component = mountWithIntl(); + const component = mountWithIntl(); const icon = component.find('EuiIcon'); expect(icon.prop('type')).toEqual(`${branding.assetFolderUrl}${darkOpensearchMarkUrl}`); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} logo`); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); expect(component).toMatchSnapshot(); }); @@ -88,10 +88,10 @@ describe('Header logo ', () => { mark: {}, applicationTitle: 'custom title', }; - const component = mountWithIntl(); + const component = mountWithIntl(); const icon = component.find('EuiIcon'); expect(icon.prop('type')).toEqual(darkOpensearchMarkUrl); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} logo`); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); expect(component).toMatchSnapshot(); }); @@ -102,10 +102,10 @@ describe('Header logo ', () => { mark: { defaultUrl: '/defaultModeMark' }, applicationTitle: 'custom title', }; - const component = mountWithIntl(); + const component = mountWithIntl(); const icon = component.find('EuiIcon'); expect(icon.prop('type')).toEqual(branding.mark.defaultUrl); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} logo`); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); expect(component).toMatchSnapshot(); }); @@ -116,10 +116,10 @@ describe('Header logo ', () => { mark: { defaultUrl: '/defaultModeMark', darkModeUrl: '/darkModeMark' }, applicationTitle: 'custom title', }; - const component = mountWithIntl(); + const component = mountWithIntl(); const icon = component.find('EuiIcon'); expect(icon.prop('type')).toEqual(branding.mark.darkModeUrl); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} logo`); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); expect(component).toMatchSnapshot(); }); }); diff --git a/src/core/public/chrome/ui/header/branding/mark.tsx b/src/core/public/chrome/ui/header/home_icon.tsx similarity index 52% rename from src/core/public/chrome/ui/header/branding/mark.tsx rename to src/core/public/chrome/ui/header/home_icon.tsx index 68f26e91f1a5..75db11cf3778 100644 --- a/src/core/public/chrome/ui/header/branding/mark.tsx +++ b/src/core/public/chrome/ui/header/home_icon.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { EuiIcon } from '@elastic/eui'; -import { ChromeBranding } from '../../../chrome_service'; +import { ChromeBranding } from '../../chrome_service'; /** * Use branding configurations to render the header mark on the nav bar. @@ -13,11 +13,12 @@ import { ChromeBranding } from '../../../chrome_service'; * @param {ChromeBranding} - branding object consist of mark, darkmode selection, asset path and title * @returns Mark component which is going to be rendered on the main page header bar. */ -export const Mark = ({ +export const HomeIcon = ({ darkMode, assetFolderUrl = '', mark, applicationTitle = 'opensearch dashboards', + useExpandedMenu = false, }: ChromeBranding) => { const { defaultUrl: markUrl, darkModeUrl: darkMarkUrl } = mark ?? {}; @@ -25,19 +26,27 @@ export const Mark = ({ const defaultMark = darkMode ? 'opensearch_mark_dark_mode.svg' : 'opensearch_mark_default_mode.svg'; - const altText = `${applicationTitle} logo`; - const iconType = customMark ? customMark : `${assetFolderUrl}/${defaultMark}`; - const testSubj = customMark ? 'customLogo' : 'defaultLogo'; + const getIconProps = () => { + const iconType = customMark + ? customMark + : useExpandedMenu + ? 'home' + : `${assetFolderUrl}/${defaultMark}`; + const testSubj = customMark ? 'customLogo' : useExpandedMenu ? 'homeLogo' : 'defaultLogo'; + const title = `${applicationTitle} home`; + const size = useExpandedMenu ? ('m' as const) : ('l' as const); - return ( - - ); + return { + 'data-test-subj': testSubj, + 'data-test-image-url': iconType, + type: iconType, + title, + size, + }; + }; + + const props = getIconProps(); + + return ; }; diff --git a/src/core/public/chrome/ui/header/home_loader.scss b/src/core/public/chrome/ui/header/home_loader.scss new file mode 100644 index 000000000000..78d5345a88f2 --- /dev/null +++ b/src/core/public/chrome/ui/header/home_loader.scss @@ -0,0 +1,14 @@ +.header__homeLoaderNavButton .euiHeaderSectionItemButton__content { + // avoid layout shift on smallest breakpoint if logo and loading indicator are different sizes + min-width: 24px; + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr; + + & .loaderContainer, + & .homeIconContainer { + grid-area: 1 / 1; + align-self: center; + justify-self: center; + } +} diff --git a/src/core/public/chrome/ui/header/home_loader.tsx b/src/core/public/chrome/ui/header/home_loader.tsx new file mode 100644 index 000000000000..15b719a259ec --- /dev/null +++ b/src/core/public/chrome/ui/header/home_loader.tsx @@ -0,0 +1,143 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * 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 './home_loader.scss'; + +import { i18n } from '@osd/i18n'; +import React from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { Observable } from 'rxjs'; +import Url from 'url'; +import { EuiHeaderSectionItemButton } from '@elastic/eui'; +import { ChromeNavLink } from '../..'; +import { ChromeBranding } from '../../chrome_service'; +import { LoadingIndicator } from '../loading_indicator'; +import { HomeIcon } from './home_icon'; + +function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void { + let current = element; + while (current) { + if (current.tagName === 'A') { + return current as HTMLAnchorElement; + } + + if (!current.parentElement || current.parentElement === document.body) { + return undefined; + } + + current = current.parentElement; + } +} + +function onClick( + event: React.MouseEvent, + forceNavigation: boolean, + navLinks: ChromeNavLink[], + navigateToApp: (appId: string) => void +) { + const anchor = findClosestAnchor((event as any).nativeEvent.target); + if (!anchor) { + return; + } + + const navLink = navLinks.find((item) => item.href === anchor.href); + if (navLink && navLink.disabled) { + event.preventDefault(); + return; + } + + if (event.isDefaultPrevented() || event.altKey || event.metaKey || event.ctrlKey) { + return; + } + + if (forceNavigation) { + const toParsed = Url.parse(anchor.href); + const fromParsed = Url.parse(document.location.href); + const sameProto = toParsed.protocol === fromParsed.protocol; + const sameHost = toParsed.host === fromParsed.host; + const samePath = toParsed.path === fromParsed.path; + + if (sameProto && sameHost && samePath) { + if (toParsed.hash) { + document.location.reload(); + } + + // event.preventDefault() keeps the browser from seeing the new url as an update + // and even setting window.location does not mimic that behavior, so instead + // we use stopPropagation() to prevent angular from seeing the click and + // starting a digest cycle/attempting to handle it in the router. + event.stopPropagation(); + } + } else { + navigateToApp('home'); + event.preventDefault(); + } +} + +interface Props { + href: string; + navLinks$: Observable; + forceNavigation$: Observable; + loadingCount$: Observable; + navigateToApp: (appId: string) => void; + branding: ChromeBranding; +} + +export function HomeLoader({ href, navigateToApp, branding, ...observables }: Props) { + const forceNavigation = useObservable(observables.forceNavigation$, false); + const navLinks = useObservable(observables.navLinks$, []); + const loadingCount = useObservable(observables.loadingCount$, 0); + const label = i18n.translate('core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel', { + defaultMessage: 'Go to home page', + }); + + return ( + ) => + onClick(e, forceNavigation, navLinks, navigateToApp) + } + href={href} + // TODO: title seems preferable for tooltip, but need figure out best ally approach to avoid duplication for screen readers: https://www.deque.com/blog/text-links-practices-screen-readers/ + aria-label={label} + title={label} + > + {!(loadingCount > 0) && ( +
+ +
+ )} +
+ +
+
+ ); +} diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index fe6162f30e52..b770ec257607 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -183,7 +183,7 @@ export class CoreSystem { return { fatalErrors: this.fatalErrorsSetup }; } catch (error) { - if (this.fatalErrorsSetup) { + if (this.fatalErrorsSetup && (typeof error === 'string' || error instanceof Error)) { this.fatalErrorsSetup.add(error); } else { // If the FatalErrorsService has not yet been setup, log error to console @@ -260,9 +260,14 @@ export class CoreSystem { await this.plugins.start(core); + const { useExpandedMenu } = injectedMetadata.getBranding() ?? {}; + // ensure the rootDomElement is empty this.rootDomElement.textContent = ''; this.rootDomElement.classList.add('coreSystemRootDomElement'); + if (useExpandedMenu) { + this.rootDomElement.classList.add('headerIsExpanded'); + } this.rootDomElement.appendChild(coreUiTargetDomElement); this.rootDomElement.appendChild(notificationsTargetDomElement); this.rootDomElement.appendChild(overlayTargetDomElement); @@ -278,7 +283,7 @@ export class CoreSystem { application, }; } catch (error) { - if (this.fatalErrorsSetup) { + if (this.fatalErrorsSetup && (typeof error === 'string' || error instanceof Error)) { this.fatalErrorsSetup.add(error); } else { // If the FatalErrorsService has not yet been setup, log error to console diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss index 1ac489c8f129..e59008f08259 100644 --- a/src/core/public/rendering/_base.scss +++ b/src/core/public/rendering/_base.scss @@ -1,5 +1,3 @@ -@include euiHeaderAffordForFixed($osdHeaderOffset); - /** * stretch the root element of the OpenSearch Dashboards application to set the base-size that * flexed children should keep. Only works when paired with root styles applied @@ -17,6 +15,10 @@ margin: 0 auto; min-height: calc(100vh - #{$osdHeaderOffset}); + .headerIsExpanded & { + min-height: calc(100vh - #{$osdHeaderOffset * 2}); + } + &.hidden-chrome { min-height: 100vh; } diff --git a/src/core/server/core_app/assets/default_branding/opensearch_logo.svg b/src/core/server/core_app/assets/default_branding/opensearch_logo_dark_mode.svg similarity index 100% rename from src/core/server/core_app/assets/default_branding/opensearch_logo.svg rename to src/core/server/core_app/assets/default_branding/opensearch_logo_dark_mode.svg diff --git a/src/core/server/core_app/assets/default_branding/opensearch_logo_default_mode.svg b/src/core/server/core_app/assets/default_branding/opensearch_logo_default_mode.svg new file mode 100644 index 000000000000..9ee816341523 --- /dev/null +++ b/src/core/server/core_app/assets/default_branding/opensearch_logo_default_mode.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/home/public/application/components/_home.scss b/src/plugins/home/public/application/components/_home.scss index 3195fcbb0367..374470427d8e 100644 --- a/src/plugins/home/public/application/components/_home.scss +++ b/src/plugins/home/public/application/components/_home.scss @@ -33,6 +33,10 @@ display: flex; flex-direction: column; min-height: calc(100vh - #{$euiHeaderHeightCompensation}); + + .headerIsExpanded & { + min-height: calc(100vh - #{$euiHeaderHeightCompensation * 2}); + } } .homContent { diff --git a/src/plugins/opensearch_dashboards_overview/public/components/_overview.scss b/src/plugins/opensearch_dashboards_overview/public/components/_overview.scss index 45d5575d33cd..45dc59f59edb 100644 --- a/src/plugins/opensearch_dashboards_overview/public/components/_overview.scss +++ b/src/plugins/opensearch_dashboards_overview/public/components/_overview.scss @@ -33,6 +33,10 @@ display: flex; flex-direction: column; min-height: calc(100vh - #{$euiHeaderHeightCompensation}); + + .headerIsExpanded & { + min-height: calc(100vh - #{$euiHeaderHeightCompensation * 2}); + } } .osdOverviewContent {