Skip to content

Commit

Permalink
[chrome] Improve breadcrumb extension (elastic#209765)
Browse files Browse the repository at this point in the history
## Summary

fix elastic#208728

This PR improves breadcrumb extension point for adding starring next to
a dashboard breadcrumb elastic#200315:

- Fix breadcrumb extension didn't render in solution nav
- Support multiple extensions (search sessions are deprecated and need
to be enabled with kibana.yml flag, but we still need to support both UI
elements)
- Improve DX to unmount the extension 

To test: 

- Add `data.search.sessions.enabled: true` and see that search session
UI appears in solution nav.
- To test multiple, add more extensions by using
`chrome.setBreadcrumbsAppendExtension`, e.g. in
`src/platform/plugins/shared/data/public/search/search_service.ts` .
This actually gonna be used in
elastic#200315

![Screenshot 2025-02-05 at 14 41
21](https://github.com/user-attachments/assets/f4bece3e-6b09-4afb-94b5-291a7387118c)
  • Loading branch information
Dosant authored Feb 7, 2025
1 parent e21e748 commit 02a88d1
Show file tree
Hide file tree
Showing 14 changed files with 189 additions and 75 deletions.
5 changes: 3 additions & 2 deletions src/core/packages/application/common/src/global_app_style.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ export const chromeStyles = (euiTheme: UseEuiTheme['euiTheme']) => css`
.header__breadcrumbsWithExtensionContainer {
overflow: hidden; // enables text-ellipsis in the last breadcrumb
.euiHeaderBreadcrumbs {
.euiHeaderBreadcrumbs,
.euiBreadcrumbs {
// stop breadcrumbs from growing.
// this makes the extension appear right next to the last breadcrumb
flex-grow: 0;
Expand All @@ -147,7 +148,7 @@ export const chromeStyles = (euiTheme: UseEuiTheme['euiTheme']) => css`
overflow: hidden; // enables text-ellipsis in the last breadcrumb
}
}
.header__breadcrumbsAppendExtension {
.header__breadcrumbsAppendExtension--last {
flex-grow: 1;
}
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,21 +492,70 @@ describe('start', () => {
describe('breadcrumbsAppendExtension$', () => {
it('updates the breadcrumbsAppendExtension$', async () => {
const { chrome, service } = await start();
const promise = chrome.getBreadcrumbsAppendExtension$().pipe(toArray()).toPromise();
const promise = chrome.getBreadcrumbsAppendExtensions$().pipe(toArray()).toPromise();

const ext1 = chrome.setBreadcrumbsAppendExtension({
content: () => () => {},
});
chrome.setBreadcrumbsAppendExtension({
order: 0,
content: () => () => {},
});
const ext3 = chrome.setBreadcrumbsAppendExtension({
order: 100,
content: () => () => {},
});
ext3();
ext1();
service.stop();

await expect(promise).resolves.toMatchInlineSnapshot(`
Array [
undefined,
Object {
"content": [Function],
},
]
`);
Array [
Array [],
Array [
Object {
"content": [Function],
},
],
Array [
Object {
"content": [Function],
"order": 0,
},
Object {
"content": [Function],
},
],
Array [
Object {
"content": [Function],
"order": 0,
},
Object {
"content": [Function],
},
Object {
"content": [Function],
"order": 100,
},
],
Array [
Object {
"content": [Function],
"order": 0,
},
Object {
"content": [Function],
},
],
Array [
Object {
"content": [Function],
"order": 0,
},
],
]
`);
});
});

Expand Down
29 changes: 22 additions & 7 deletions src/core/packages/chrome/browser-internal/src/chrome_service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,9 @@ export class ChromeService {
);
const helpExtension$ = new BehaviorSubject<ChromeHelpExtension | undefined>(undefined);
const breadcrumbs$ = new BehaviorSubject<ChromeBreadcrumb[]>([]);
const breadcrumbsAppendExtension$ = new BehaviorSubject<
ChromeBreadcrumbsAppendExtension | undefined
>(undefined);
const breadcrumbsAppendExtensions$ = new BehaviorSubject<ChromeBreadcrumbsAppendExtension[]>(
[]
);
const badge$ = new BehaviorSubject<ChromeBadge | undefined>(undefined);
const customNavLink$ = new BehaviorSubject<ChromeNavLink | undefined>(undefined);
const helpSupportUrl$ = new BehaviorSubject<string>(docLinks.links.kibana.askElastic);
Expand Down Expand Up @@ -467,6 +467,9 @@ export class ChromeService {
globalHelpExtensionMenuLinks$={globalHelpExtensionMenuLinks$}
actionMenu$={application.currentActionMenu$}
breadcrumbs$={currentProjectBreadcrumbs$}
breadcrumbsAppendExtensions$={breadcrumbsAppendExtensions$.pipe(
takeUntil(this.stop$)
)}
customBranding$={customBranding$}
helpExtension$={helpExtension$.pipe(takeUntil(this.stop$))}
helpSupportUrl$={helpSupportUrl$.pipe(takeUntil(this.stop$))}
Expand Down Expand Up @@ -500,7 +503,7 @@ export class ChromeService {
badge$={badge$.pipe(takeUntil(this.stop$))}
basePath={http.basePath}
breadcrumbs$={breadcrumbs$.pipe(takeUntil(this.stop$))}
breadcrumbsAppendExtension$={breadcrumbsAppendExtension$.pipe(takeUntil(this.stop$))}
breadcrumbsAppendExtensions$={breadcrumbsAppendExtensions$.pipe(takeUntil(this.stop$))}
customNavLink$={customNavLink$.pipe(takeUntil(this.stop$))}
kibanaDocLink={docLinks.links.kibana.guide}
docLinks={docLinks}
Expand Down Expand Up @@ -548,12 +551,24 @@ export class ChromeService {

setBreadcrumbs: setClassicBreadcrumbs,

getBreadcrumbsAppendExtension$: () => breadcrumbsAppendExtension$.pipe(takeUntil(this.stop$)),
getBreadcrumbsAppendExtensions$: () =>
breadcrumbsAppendExtensions$.pipe(takeUntil(this.stop$)),

setBreadcrumbsAppendExtension: (
breadcrumbsAppendExtension?: ChromeBreadcrumbsAppendExtension
breadcrumbsAppendExtension: ChromeBreadcrumbsAppendExtension
) => {
breadcrumbsAppendExtension$.next(breadcrumbsAppendExtension);
breadcrumbsAppendExtensions$.next(
[...breadcrumbsAppendExtensions$.getValue(), breadcrumbsAppendExtension].sort(
({ order: orderA = 50 }, { order: orderB = 50 }) => orderA - orderB
)
);
return () => {
breadcrumbsAppendExtensions$.next(
breadcrumbsAppendExtensions$
.getValue()
.filter((ext) => ext !== breadcrumbsAppendExtension)
);
};
},

getGlobalHelpExtensionMenuLinks$: () => globalHelpExtensionMenuLinks$.asObservable(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import React, { PropsWithChildren } from 'react';
import { Observable } from 'rxjs';
import type { ChromeBreadcrumbsAppendExtension } from '@kbn/core-chrome-browser';
import useObservable from 'react-use/lib/useObservable';
import { EuiFlexGroup } from '@elastic/eui';
import classnames from 'classnames';
import { HeaderExtension } from './header_extension';

export interface Props {
breadcrumbsAppendExtensions$: Observable<ChromeBreadcrumbsAppendExtension[]>;
}

export const BreadcrumbsWithExtensionsWrapper = ({
breadcrumbsAppendExtensions$,
children,
}: PropsWithChildren<Props>) => {
const breadcrumbsAppendExtensions = useObservable(breadcrumbsAppendExtensions$, []);

return breadcrumbsAppendExtensions.length === 0 ? (
<>{children}</>
) : (
<EuiFlexGroup
responsive={false}
wrap={false}
alignItems={'center'}
className={'header__breadcrumbsWithExtensionContainer'}
gutterSize={'none'}
>
{children}
{breadcrumbsAppendExtensions.map((breadcrumbsAppendExtension, index) => {
const isLast = breadcrumbsAppendExtensions.length - 1 === index;
return (
<HeaderExtension
key={index}
extension={breadcrumbsAppendExtension.content}
containerClassName={classnames({
'header__breadcrumbsAppendExtension--last': isLast,
})}
/>
);
})}
</EuiFlexGroup>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ describe('Header', () => {
const recentlyAccessed$ = new BehaviorSubject([
{ link: '', label: 'dashboard', id: 'dashboard' },
]);
const breadcrumbsAppendExtension$ = new BehaviorSubject<
undefined | ChromeBreadcrumbsAppendExtension
>(undefined);
const breadcrumbsAppendExtensions$ = new BehaviorSubject<ChromeBreadcrumbsAppendExtension[]>(
[]
);
const component = mountWithIntl(
<Header
{...mockProps()}
Expand All @@ -93,7 +93,7 @@ describe('Header', () => {
recentlyAccessed$={recentlyAccessed$}
isLocked$={isLocked$}
customNavLink$={customNavLink$}
breadcrumbsAppendExtension$={breadcrumbsAppendExtension$}
breadcrumbsAppendExtensions$={breadcrumbsAppendExtensions$}
headerBanner$={headerBanner$}
helpMenuLinks$={of([])}
isServerless={false}
Expand All @@ -108,17 +108,28 @@ describe('Header', () => {
expect(component.render()).toMatchSnapshot();

act(() =>
breadcrumbsAppendExtension$.next({
content: (root: HTMLDivElement) => {
root.innerHTML = '<div class="my-extension">__render__</div>';
return () => (root.innerHTML = '');
breadcrumbsAppendExtensions$.next([
{
content: (root: HTMLDivElement) => {
root.innerHTML = '<div class="my-extension1">__render__</div>';
return () => (root.innerHTML = '');
},
},
{
content: (root: HTMLDivElement) => {
root.innerHTML = '<div class="my-extension2">__render__</div>';
return () => (root.innerHTML = '');
},
},
})
])
);
component.update();
expect(component.find('HeaderExtension').exists()).toBeTruthy();
expect(component.find('HeaderExtension').length).toBe(2);
expect(
component.find('HeaderExtension').at(0).getDOMNode().querySelector('.my-extension1')
).toBeTruthy();
expect(
component.find('HeaderExtension').getDOMNode().querySelector('.my-extension')
component.find('HeaderExtension').at(1).getDOMNode().querySelector('.my-extension2')
).toBeTruthy();
});
});
32 changes: 8 additions & 24 deletions src/core/packages/chrome/browser-internal/src/ui/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/

import {
EuiFlexGroup,
EuiHeader,
EuiHeaderSection,
EuiHeaderSectionItem,
Expand All @@ -19,7 +18,6 @@ import {
import { i18n } from '@kbn/i18n';
import classnames from 'classnames';
import React, { createRef, useState } from 'react';
import useObservable from 'react-use/lib/useObservable';
import type { Observable } from 'rxjs';
import type { HttpStart } from '@kbn/core-http-browser';
import type { InternalApplicationStart } from '@kbn/core-application-browser-internal';
Expand All @@ -45,7 +43,7 @@ import { HeaderHelpMenu } from './header_help_menu';
import { HeaderLogo } from './header_logo';
import { HeaderNavControls } from './header_nav_controls';
import { HeaderActionMenu, useHeaderActionMenuMounter } from './header_action_menu';
import { HeaderExtension } from './header_extension';
import { BreadcrumbsWithExtensionsWrapper } from './breadcrumbs_with_extensions';
import { HeaderTopBanner } from './header_top_banner';
import { HeaderMenuButton } from './header_menu_button';
import { ScreenReaderRouteAnnouncements, SkipToMainContent } from './screen_reader_a11y';
Expand All @@ -56,7 +54,7 @@ export interface HeaderProps {
headerBanner$: Observable<ChromeUserBanner | undefined>;
badge$: Observable<ChromeBadge | undefined>;
breadcrumbs$: Observable<ChromeBreadcrumb[]>;
breadcrumbsAppendExtension$: Observable<ChromeBreadcrumbsAppendExtension | undefined>;
breadcrumbsAppendExtensions$: Observable<ChromeBreadcrumbsAppendExtension[]>;
customNavLink$: Observable<ChromeNavLink | undefined>;
homeHref: string;
kibanaDocLink: string;
Expand Down Expand Up @@ -88,15 +86,14 @@ export function Header({
basePath,
onIsLockedUpdate,
homeHref,
breadcrumbsAppendExtension$,
breadcrumbsAppendExtensions$,
globalHelpExtensionMenuLinks$,
customBranding$,
isServerless,
...observables
}: HeaderProps) {
const [isNavOpen, setIsNavOpen] = useState(false);
const [navId] = useState(htmlIdGenerator()());
const breadcrumbsAppendExtension = useObservable(breadcrumbsAppendExtension$);
const headerActionMenuMounter = useHeaderActionMenuMounter(application.currentActionMenu$);

const toggleCollapsibleNavRef = createRef<HTMLButtonElement & { euiAnimate: () => void }>();
Expand Down Expand Up @@ -206,24 +203,11 @@ export function Header({

<HeaderNavControls side="left" navControls$={observables.navControlsLeft$} />
</EuiHeaderSection>

{!breadcrumbsAppendExtension ? (
Breadcrumbs
) : (
<EuiFlexGroup
responsive={false}
wrap={false}
alignItems={'center'}
className={'header__breadcrumbsWithExtensionContainer'}
gutterSize={'none'}
>
{Breadcrumbs}
<HeaderExtension
extension={breadcrumbsAppendExtension.content}
containerClassName={'header__breadcrumbsAppendExtension'}
/>
</EuiFlexGroup>
)}
<BreadcrumbsWithExtensionsWrapper
breadcrumbsAppendExtensions$={breadcrumbsAppendExtensions$}
>
{Breadcrumbs}
</BreadcrumbsWithExtensionsWrapper>

<HeaderBadge badge$={observables.badge$} />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('Header', () => {
const mockProps: Omit<ProjectHeaderProps, 'children'> = {
application: mockApplication,
breadcrumbs$: Rx.of([]),
breadcrumbsAppendExtensions$: Rx.of([]),
actionMenu$: Rx.of(undefined),
docLinks: docLinksServiceMock.createStartContract(),
globalHelpExtensionMenuLinks$: Rx.of([]),
Expand Down
Loading

0 comments on commit 02a88d1

Please sign in to comment.