[Stateful sidenav] Custom branding logo (#184035)

This commit is contained in:
Sébastien Loix 2024-05-24 10:02:09 +01:00 committed by GitHub
parent 9bafd068e0
commit e75440335f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 89 additions and 30 deletions

View file

@ -398,6 +398,7 @@ export class ChromeService {
globalHelpExtensionMenuLinks$={globalHelpExtensionMenuLinks$}
actionMenu$={application.currentActionMenu$}
breadcrumbs$={currentProjectBreadcrumbs$}
customBranding$={customBranding$}
helpExtension$={helpExtension$.pipe(takeUntil(this.stop$))}
helpSupportUrl$={helpSupportUrl$.pipe(takeUntil(this.stop$))}
helpMenuLinks$={helpMenuLinks$}

View file

@ -114,9 +114,7 @@ export class ProjectNavigationService {
);
return {
setProjectHome: (homeHref: string) => {
this.projectHome$.next(homeHref);
},
setProjectHome: this.setProjectHome.bind(this),
getProjectHome$: () => {
return this.projectHome$.asObservable();
},
@ -408,14 +406,25 @@ export class ProjectNavigationService {
return;
}
const { sideNavComponent } = definition;
const { sideNavComponent, homePage = '' } = definition;
const homePageLink = this.navLinksService?.get(homePage);
if (sideNavComponent) {
this.setSideNavComponent(sideNavComponent);
}
if (homePageLink) {
this.setProjectHome(homePageLink.href);
}
this.initNavigation(nextId, definition.navigationTree$);
});
}
private setProjectHome(homeHref: string) {
this.projectHome$.next(homeHref);
}
private goToSolutionHome(id: string) {
const definitions = this.solutionNavDefinitions$.getValue();
const definition = definitions[id];

View file

@ -33,6 +33,7 @@ describe('Header', () => {
navControlsLeft$: Rx.of([]),
navControlsCenter$: Rx.of([]),
navControlsRight$: Rx.of([]),
customBranding$: Rx.of({}),
prependBasePath: (str) => `hello/world/${str}`,
toggleSideNav: jest.fn(),
};
@ -46,5 +47,16 @@ describe('Header', () => {
expect(await screen.findByTestId('euiCollapsibleNavButton')).toBeVisible();
expect(await screen.findByText('Hello, world!')).toBeVisible();
expect(screen.queryByTestId(/customLogo/)).toBeNull();
});
it('renders custom branding logo', async () => {
const { queryByTestId } = render(
<ProjectHeader {...mockProps} customBranding$={Rx.of({ logo: 'foo.jpg' })}>
<EuiHeader>Hello, world!</EuiHeader>
</ProjectHeader>
);
expect(queryByTestId(/customLogo/)).not.toBeNull();
});
});

View file

@ -14,6 +14,7 @@ import {
EuiLoadingSpinner,
useEuiTheme,
EuiThemeComputed,
EuiImage,
} from '@elastic/eui';
import { css } from '@emotion/react';
import type { InternalApplicationStart } from '@kbn/core-application-browser-internal';
@ -33,7 +34,9 @@ import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import { Router } from '@kbn/shared-ux-router';
import React, { useCallback } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { debounceTime, Observable, of } from 'rxjs';
import { debounceTime, Observable } from 'rxjs';
import type { CustomBranding } from '@kbn/core-custom-branding-common';
import { useHeaderActionMenuMounter } from '../header/header_action_menu';
import { Breadcrumbs } from './breadcrumbs';
import { HeaderHelpMenu } from '../header/header_help_menu';
@ -46,11 +49,11 @@ import { ProjectNavigation } from './navigation';
const getHeaderCss = ({ size, colors }: EuiThemeComputed) => ({
logo: {
container: css`
display: inline-block;
display: flex;
align-items: center;
justify-content: center;
min-width: 56px; /* 56 = 40 + 8 + 8 */
padding: 0 ${size.s};
cursor: pointer;
margin-left: -${size.s}; // to get equal spacing between .euiCollapsibleNavButtonWrapper, logo and breadcrumbs
`,
logo: css`
min-width: 0; /* overrides min-width: 40px */
@ -113,6 +116,7 @@ export interface Props {
actionMenu$: Observable<MountPoint | undefined>;
docLinks: DocLinksStart;
children: React.ReactNode;
customBranding$: Observable<CustomBranding>;
globalHelpExtensionMenuLinks$: Observable<ChromeGlobalHelpExtensionMenuLink[]>;
helpExtension$: Observable<ChromeHelpExtension | undefined>;
helpSupportUrl$: Observable<string>;
@ -130,46 +134,77 @@ export interface Props {
const LOADING_DEBOUNCE_TIME = 80;
type LogoProps = Pick<Props, 'application' | 'homeHref$' | 'loadingCount$' | 'prependBasePath'> & {
type LogoProps = Pick<
Props,
'application' | 'homeHref$' | 'loadingCount$' | 'prependBasePath' | 'customBranding$'
> & {
logoCss: HeaderCss['logo'];
};
const Logo = (props: LogoProps) => {
const loadingCount = useObservable(
props.loadingCount$.pipe(debounceTime(LOADING_DEBOUNCE_TIME)),
0
);
const homeHref = useObservable(props.homeHref$, '/app/home');
const Logo = ({
loadingCount$,
homeHref$,
prependBasePath,
application,
logoCss,
customBranding$,
}: LogoProps) => {
const loadingCount = useObservable(loadingCount$.pipe(debounceTime(LOADING_DEBOUNCE_TIME)), 0);
const homeHref = useObservable(homeHref$, '/app/home');
const customBranding = useObservable(customBranding$, {});
const { logo } = customBranding;
let fullHref: string | undefined;
if (homeHref) {
fullHref = props.prependBasePath(homeHref);
fullHref = prependBasePath(homeHref);
}
const navigateHome = useCallback(
(event: React.MouseEvent) => {
if (fullHref) {
props.application.navigateToUrl(fullHref);
application.navigateToUrl(fullHref);
}
event.preventDefault();
},
[fullHref, props.application]
[fullHref, application]
);
const renderLogo = () => {
if (logo) {
return (
<a href={fullHref} onClick={navigateHome} data-test-subj="globalLoadingIndicator-hidden">
<EuiImage
src={logo}
css={logoCss}
data-test-subj="globalLoadingIndicator-hidden customLogo"
size={24}
alt="logo"
aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.customLogoAriaLabel', {
defaultMessage: 'User logo',
})}
/>
</a>
);
}
return (
<EuiHeaderLogo
iconType="logoElastic"
onClick={navigateHome}
href={fullHref}
css={logoCss}
data-test-subj="globalLoadingIndicator-hidden"
aria-label={headerStrings.logo.ariaLabel}
/>
);
};
return (
<span css={props.logoCss.container} data-test-subj="nav-header-logo">
<span css={logoCss.container} data-test-subj="nav-header-logo">
{loadingCount === 0 ? (
<EuiHeaderLogo
iconType="logoElastic"
onClick={navigateHome}
href={fullHref}
css={props.logoCss}
data-test-subj="globalLoadingIndicator-hidden"
aria-label={headerStrings.logo.ariaLabel}
/>
renderLogo()
) : (
<a onClick={navigateHome} href={fullHref} css={props.logoCss.spinner}>
<a onClick={navigateHome} href={fullHref} css={logoCss.spinner}>
<EuiLoadingSpinner
size="l"
aria-hidden={false}
@ -189,6 +224,7 @@ export const ProjectHeader = ({
prependBasePath,
docLinks,
toggleSideNav,
customBranding$,
...observables
}: Props) => {
const headerActionMenuMounter = useHeaderActionMenuMounter(observables.actionMenu$);
@ -200,7 +236,7 @@ export const ProjectHeader = ({
<>
<ScreenReaderRouteAnnouncements
breadcrumbs$={observables.breadcrumbs$}
customBranding$={of()}
customBranding$={customBranding$}
appId$={application.currentAppId$}
/>
<SkipToMainContent />
@ -220,6 +256,7 @@ export const ProjectHeader = ({
application={application}
homeHref$={observables.homeHref$}
loadingCount$={observables.loadingCount$}
customBranding$={customBranding$}
logoCss={logoCss}
/>
</EuiHeaderSectionItem>