[Serverless/breadcrumbs] Bootstrap and API (#156855)

## Summary

Partially address https://github.com/elastic/kibana/issues/156517

Based on: 
- [Serverless chrome breadcrumbs
requirements](https://docs.google.com/document/d/1e5SbDPpySiPeBrjgLJD6Qw6fJyiy34uO2dmGLHlu38E/edit)
- [Serverless chrome breadcrumbs API tech
doc](https://docs.google.com/document/d/1_qL14NMGYdI0913eclJd3DXG0lQ2jkE0V3578iaDASs/edit#heading=h.ndqge1i76y6p)

Adds an api and bootstrap code for project (serverless) breadcrumbs
which allows to either set a "deeper context" breadcrumbs or override
nav controlled breadcrumbs:

```
plugins.serverless.setBreadcrumbs(myDeeperContextBreadcrumbs);

plugins.serverless.setBreadcrumbs(myCustomBreadcrumbs, {absolute: true});
``` 

This PR adds an API and sets everything around the breadcrumb building
logic. Actual breadcrumbs building is not implemented and depends on
https://github.com/elastic/kibana/issues/157702 as we need the
navigation tree to be available in chrome service.


This PR doesn't have any visible changes
This commit is contained in:
Anton Dosov 2023-06-06 15:10:35 +02:00 committed by GitHub
parent addb7c0859
commit d4f4a25e60
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 211 additions and 3 deletions

View file

@ -29,6 +29,7 @@ import type {
ChromeUserBanner,
ChromeStyle,
ChromeProjectNavigation,
ChromeSetProjectBreadcrumbsParams,
} from '@kbn/core-chrome-browser';
import type { CustomBrandingStart } from '@kbn/core-custom-branding-browser';
import type { SideNavComponent as ISideNavComponent } from '@kbn/core-chrome-browser';
@ -198,6 +199,13 @@ export class ChromeService {
projectNavigation.setProjectNavigation(config);
};
const setProjectBreadcrumbs = (
breadcrumbs: ChromeBreadcrumb[] | ChromeBreadcrumb,
params?: ChromeSetProjectBreadcrumbsParams
) => {
projectNavigation.setProjectBreadcrumbs(breadcrumbs, params);
};
const isIE = () => {
const ua = window.navigator.userAgent;
const msie = ua.indexOf('MSIE '); // IE 10 or older
@ -247,9 +255,11 @@ export class ChromeService {
// }
const projectNavigationComponent$ = projectNavigation.getProjectSideNavComponent$();
// const projectNavigation$ = projectNavigation.getProjectNavigation$();
const ProjectHeaderWithNavigation = () => {
const CustomSideNavComponent = useObservable(projectNavigationComponent$, undefined);
// const projectNavigationConfig = useObservable(projectNavigation$, undefined);
let SideNavComponent: ISideNavComponent = () => null;
@ -261,6 +271,13 @@ export class ChromeService {
: ProjectSideNavigation;
}
// if projectNavigation wasn't set fallback to the default breadcrumbs
// TODO: Uncommented when we support the project navigation config
// const projectBreadcrumbs$ = projectNavigationConfig
// ? projectNavigation.getProjectBreadcrumbs$()
// : breadcrumbs$;
const projectBreadcrumbs$ = breadcrumbs$;
return (
<ProjectHeader
{...{
@ -268,7 +285,7 @@ export class ChromeService {
globalHelpExtensionMenuLinks$,
}}
actionMenu$={application.currentActionMenu$}
breadcrumbs$={breadcrumbs$.pipe(takeUntil(this.stop$))}
breadcrumbs$={projectBreadcrumbs$.pipe(takeUntil(this.stop$))}
helpExtension$={helpExtension$.pipe(takeUntil(this.stop$))}
helpSupportUrl$={helpSupportUrl$.pipe(takeUntil(this.stop$))}
navControlsRight$={navControls.getRight$()}
@ -390,6 +407,7 @@ export class ChromeService {
project: {
setNavigation: setProjectNavigation,
setSideNavComponent: setProjectSideNavComponent,
setBreadcrumbs: setProjectBreadcrumbs,
},
};
}

View file

@ -0,0 +1,23 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { ChromeProjectBreadcrumb } from '@kbn/core-chrome-browser';
import { EuiIcon } from '@elastic/eui';
import React from 'react';
export const createHomeBreadcrumb = ({
homeHref,
}: {
homeHref: string;
}): ChromeProjectBreadcrumb => {
return {
text: <EuiIcon type="home" />,
title: 'Home',
href: homeHref,
};
};

View file

@ -0,0 +1,91 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { firstValueFrom } from 'rxjs';
import { ProjectNavigationService } from './project_navigation_service';
import { applicationServiceMock } from '@kbn/core-application-browser-mocks';
import type { ChromeNavLinks } from '@kbn/core-chrome-browser';
const setup = () => {
const projectNavigationService = new ProjectNavigationService();
const projectNavigation = projectNavigationService.start({
application: applicationServiceMock.createInternalStartContract(),
navLinks: {} as unknown as ChromeNavLinks,
});
return { projectNavigation };
};
describe('breadcrumbs', () => {
test('should return list of breadcrumbs home / nav / custom', async () => {
const { projectNavigation } = setup();
projectNavigation.setProjectBreadcrumbs([
{ text: 'custom1', href: '/custom1' },
{ text: 'custom2', href: '/custom1/custom2' },
]);
// TODO: add projectNavigation.setProjectNavigation() to test the part of breadcrumbs extracted from the nav tree
const breadcrumbs = await firstValueFrom(projectNavigation.getProjectBreadcrumbs$());
expect(breadcrumbs).toMatchInlineSnapshot(`
Array [
Object {
"href": "/",
"text": <EuiIcon
type="home"
/>,
"title": "Home",
},
Object {
"href": "/custom1",
"text": "custom1",
},
Object {
"href": "/custom1/custom2",
"text": "custom2",
},
]
`);
});
test('should skip the default navigation from project navigation when absolute: true is used', async () => {
const { projectNavigation } = setup();
projectNavigation.setProjectBreadcrumbs(
[
{ text: 'custom1', href: '/custom1' },
{ text: 'custom2', href: '/custom1/custom2' },
],
{ absolute: true }
);
// TODO: add projectNavigation.setProjectNavigation() to test the part of breadcrumbs extracted from the nav tree
const breadcrumbs = await firstValueFrom(projectNavigation.getProjectBreadcrumbs$());
expect(breadcrumbs).toMatchInlineSnapshot(`
Array [
Object {
"href": "/",
"text": <EuiIcon
type="home"
/>,
"title": "Home",
},
Object {
"href": "/custom1",
"text": "custom1",
},
Object {
"href": "/custom1/custom2",
"text": "custom2",
},
]
`);
});
});

View file

@ -11,8 +11,11 @@ import {
ChromeNavLinks,
ChromeProjectNavigation,
SideNavComponent,
ChromeProjectBreadcrumb,
ChromeSetProjectBreadcrumbsParams,
} from '@kbn/core-chrome-browser';
import { BehaviorSubject } from 'rxjs';
import { BehaviorSubject, Observable, combineLatest, map } from 'rxjs';
import { createHomeBreadcrumb } from './home_breadcrumbs';
interface StartDeps {
application: InternalApplicationStart;
@ -25,6 +28,11 @@ export class ProjectNavigationService {
}>({ current: null });
private projectNavigation$ = new BehaviorSubject<ChromeProjectNavigation | undefined>(undefined);
private projectBreadcrumbs$ = new BehaviorSubject<{
breadcrumbs: ChromeProjectBreadcrumb[];
params: ChromeSetProjectBreadcrumbsParams;
}>({ breadcrumbs: [], params: { absolute: false } });
public start({ application, navLinks }: StartDeps) {
// TODO: use application, navLink and projectNavigation$ to:
// 1. validate projectNavigation$ against navLinks,
@ -44,6 +52,33 @@ export class ProjectNavigationService {
getProjectSideNavComponent$: () => {
return this.customProjectSideNavComponent$.asObservable();
},
setProjectBreadcrumbs: (
breadcrumbs: ChromeProjectBreadcrumb | ChromeProjectBreadcrumb[],
params?: Partial<ChromeSetProjectBreadcrumbsParams>
) => {
this.projectBreadcrumbs$.next({
breadcrumbs: Array.isArray(breadcrumbs) ? breadcrumbs : [breadcrumbs],
params: { absolute: false, ...params },
});
},
getProjectBreadcrumbs$: (): Observable<ChromeProjectBreadcrumb[]> => {
return combineLatest([this.projectBreadcrumbs$, this.projectNavigation$]).pipe(
map(([breadcrumbs, projectNavigation]) => {
/* TODO: point home breadcrumb to the correct place */
const homeBreadcrumb = createHomeBreadcrumb({ homeHref: '/' });
if (breadcrumbs.params.absolute) {
return [homeBreadcrumb, ...breadcrumbs.breadcrumbs];
} else {
return [
homeBreadcrumb,
/* TODO: insert nav breadcrumbs based on projectNavigation and application path */
...breadcrumbs.breadcrumbs,
];
}
})
);
},
};
}
}

View file

@ -10,6 +10,8 @@ import type {
ChromeProjectNavigation,
ChromeStart,
SideNavComponent,
ChromeProjectBreadcrumb,
ChromeSetProjectBreadcrumbsParams,
} from '@kbn/core-chrome-browser';
import type { Observable } from 'rxjs';
@ -50,5 +52,18 @@ export interface InternalChromeStart extends ChromeStart {
* @remarks Has no effect if the chrome style is not `project`.
*/
setSideNavComponent(component: SideNavComponent | null): void;
/**
* Set project breadcrumbs
*
* @param breadcrumbs
* @param params.absolute If true, If true, the breadcrumbs will replace the defaults, otherwise they will be appended to the default ones. false by default.
*
* @remarks Has no effect if the chrome style is not `project` or if setNavigation was not called
*/
setBreadcrumbs(
breadcrumbs: ChromeProjectBreadcrumb[] | ChromeProjectBreadcrumb,
params?: Partial<ChromeSetProjectBreadcrumbsParams>
): void;
};
}

View file

@ -66,6 +66,7 @@ const createStartContractMock = () => {
project: {
setNavigation: jest.fn(),
setSideNavComponent: jest.fn(),
setBreadcrumbs: jest.fn(),
},
};
startContract.navLinks.getAll.mockReturnValue([]);

View file

@ -34,4 +34,6 @@ export type {
ChromeProjectNavigationNode,
SideNavCompProps,
SideNavComponent,
ChromeProjectBreadcrumb,
ChromeSetProjectBreadcrumbsParams,
} from './src';

View file

@ -34,4 +34,6 @@ export type {
ChromeProjectNavigationLink,
SideNavCompProps,
SideNavComponent,
ChromeSetProjectBreadcrumbsParams,
ChromeProjectBreadcrumb,
} from './project_navigation';

View file

@ -7,6 +7,7 @@
*/
import type { ComponentType } from 'react';
import type { ChromeBreadcrumb } from './breadcrumb';
import type { ChromeNavLink } from './nav_links';
/** @internal */
@ -68,3 +69,11 @@ export interface SideNavCompProps {
/** @public */
export type SideNavComponent = ComponentType<SideNavCompProps>;
/** @public */
export type ChromeProjectBreadcrumb = ChromeBreadcrumb;
/** @public */
export interface ChromeSetProjectBreadcrumbsParams {
absolute: boolean;
}

View file

@ -10,6 +10,7 @@ import { ServerlessPluginStart } from './types';
const startMock = (): ServerlessPluginStart => ({
setSideNavComponent: jest.fn(),
setNavigation: jest.fn(),
setBreadcrumbs: jest.fn(),
});
export const serverlessMock = {

View file

@ -67,6 +67,8 @@ export class ServerlessPlugin
(core.chrome as InternalChromeStart).project.setSideNavComponent(sideNavigationComponent),
setNavigation: (projectNavigation) =>
(core.chrome as InternalChromeStart).project.setNavigation(projectNavigation),
setBreadcrumbs: (breadcrumbs, params) =>
(core.chrome as InternalChromeStart).project.setBreadcrumbs(breadcrumbs, params),
};
}

View file

@ -6,7 +6,12 @@
*/
import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public';
import type { SideNavComponent, ChromeProjectNavigation } from '@kbn/core-chrome-browser';
import type {
SideNavComponent,
ChromeProjectNavigation,
ChromeProjectBreadcrumb,
ChromeSetProjectBreadcrumbsParams,
} from '@kbn/core-chrome-browser';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ServerlessPluginSetup {}
@ -14,6 +19,10 @@ export interface ServerlessPluginSetup {}
export interface ServerlessPluginStart {
setSideNavComponent: (navigation: SideNavComponent) => void;
setNavigation(projectNavigation: ChromeProjectNavigation): void;
setBreadcrumbs: (
breadcrumbs: ChromeProjectBreadcrumb | ChromeProjectBreadcrumb[],
params?: Partial<ChromeSetProjectBreadcrumbsParams>
) => void;
}
export interface ServerlessPluginSetupDependencies {