[8.x] [Stateful sidenav] Fix breadcrumbs (#196169) (#196332)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Stateful sidenav] Fix breadcrumbs
(#196169)](https://github.com/elastic/kibana/pull/196169)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Sébastien
Loix","email":"sebastien.loix@elastic.co"},"sourceCommit":{"committedDate":"2024-10-15T14:37:19Z","message":"[Stateful
sidenav] Fix breadcrumbs
(#196169)","sha":"204f9d3a2f2fef174e24f3a79eb6d7b2f2ef03f2","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:SharedUX","backport:prev-minor","Feature:Chrome"],"title":"[Stateful
sidenav] Fix
breadcrumbs","number":196169,"url":"https://github.com/elastic/kibana/pull/196169","mergeCommit":{"message":"[Stateful
sidenav] Fix breadcrumbs
(#196169)","sha":"204f9d3a2f2fef174e24f3a79eb6d7b2f2ef03f2"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/196169","number":196169,"mergeCommit":{"message":"[Stateful
sidenav] Fix breadcrumbs
(#196169)","sha":"204f9d3a2f2fef174e24f3a79eb6d7b2f2ef03f2"}}]}]
BACKPORT-->

Co-authored-by: Sébastien Loix <sebastien.loix@elastic.co>
This commit is contained in:
Kibana Machine 2024-10-16 03:26:00 +11:00 committed by GitHub
parent 7583e1d596
commit 760021bb27
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 104 additions and 31 deletions

View file

@ -392,7 +392,7 @@ describe('start', () => {
describe('breadcrumbs', () => {
it('updates/emits the current set of breadcrumbs', async () => {
const { chrome, service } = await start();
const promise = chrome.getBreadcrumbs$().pipe(toArray()).toPromise();
const promise = firstValueFrom(chrome.getBreadcrumbs$().pipe(toArray()));
chrome.setBreadcrumbs([{ text: 'foo' }, { text: 'bar' }]);
chrome.setBreadcrumbs([{ text: 'foo' }]);
@ -425,6 +425,35 @@ describe('start', () => {
]
`);
});
it('allows the project breadcrumb to also be set', async () => {
const { chrome } = await start();
chrome.setBreadcrumbs([{ text: 'foo' }, { text: 'bar' }]); // only setting the classic breadcrumbs
{
const breadcrumbs = await firstValueFrom(chrome.project.getBreadcrumbs$());
expect(breadcrumbs.length).toBe(1);
expect(breadcrumbs[0]).toMatchObject({
'data-test-subj': 'deploymentCrumb',
});
}
chrome.setBreadcrumbs([{ text: 'foo' }, { text: 'bar' }], {
project: { value: [{ text: 'baz' }] }, // also setting the project breadcrumb
});
{
const breadcrumbs = await firstValueFrom(chrome.project.getBreadcrumbs$());
expect(breadcrumbs.length).toBe(2);
expect(breadcrumbs[0]).toMatchObject({
'data-test-subj': 'deploymentCrumb',
});
expect(breadcrumbs[1]).toEqual({
text: 'baz', // the project breadcrumb
});
}
});
});
describe('breadcrumbsAppendExtension$', () => {

View file

@ -27,6 +27,7 @@ import type {
ChromeNavLink,
ChromeBadge,
ChromeBreadcrumb,
ChromeSetBreadcrumbsParams,
ChromeBreadcrumbsAppendExtension,
ChromeGlobalHelpExtensionMenuLink,
ChromeHelpExtension,
@ -354,6 +355,17 @@ export class ChromeService {
projectNavigation.setProjectBreadcrumbs(breadcrumbs, params);
};
const setClassicBreadcrumbs = (
newBreadcrumbs: ChromeBreadcrumb[],
{ project }: ChromeSetBreadcrumbsParams = {}
) => {
breadcrumbs$.next(newBreadcrumbs);
if (project) {
const { value: projectValue, absolute = false } = project;
setProjectBreadcrumbs(projectValue ?? [], { absolute });
}
};
const setProjectHome = (homeHref: string) => {
validateChromeStyle();
projectNavigation.setProjectHome(homeHref);
@ -507,9 +519,7 @@ export class ChromeService {
getBreadcrumbs$: () => breadcrumbs$.pipe(takeUntil(this.stop$)),
setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => {
breadcrumbs$.next(newBreadcrumbs);
},
setBreadcrumbs: setClassicBreadcrumbs,
getBreadcrumbsAppendExtension$: () => breadcrumbsAppendExtension$.pipe(takeUntil(this.stop$)),
@ -586,6 +596,7 @@ export class ChromeService {
getNavigationTreeUi$: () => projectNavigation.getNavigationTreeUi$(),
setSideNavComponent: setProjectSideNavComponent,
setBreadcrumbs: setProjectBreadcrumbs,
getBreadcrumbs$: projectNavigation.getProjectBreadcrumbs$.bind(projectNavigation),
getActiveNavigationNodes$: () => projectNavigation.getActiveNodes$(),
updateSolutionNavigations: projectNavigation.updateSolutionNavigations,
changeActiveSolutionNavigation: projectNavigation.changeActiveSolutionNavigation,

View file

@ -11,7 +11,6 @@ import React from 'react';
import { EuiContextMenuPanel, EuiContextMenuItem, EuiButtonEmpty } from '@elastic/eui';
import type {
AppDeepLinkId,
ChromeProjectBreadcrumb,
ChromeProjectNavigationNode,
ChromeSetProjectBreadcrumbsParams,
ChromeBreadcrumb,
@ -30,14 +29,14 @@ export function buildBreadcrumbs({
}: {
projectName?: string;
projectBreadcrumbs: {
breadcrumbs: ChromeProjectBreadcrumb[];
breadcrumbs: ChromeBreadcrumb[];
params: ChromeSetProjectBreadcrumbsParams;
};
chromeBreadcrumbs: ChromeBreadcrumb[];
cloudLinks: CloudLinks;
activeNodes: ChromeProjectNavigationNode[][];
isServerless: boolean;
}): ChromeProjectBreadcrumb[] {
}): ChromeBreadcrumb[] {
const rootCrumb = buildRootCrumb({
projectName,
cloudLinks,
@ -54,7 +53,7 @@ export function buildBreadcrumbs({
(n) => Boolean(n.title) && n.breadcrumbStatus !== 'hidden'
);
const navBreadcrumbs = navBreadcrumbPath.map(
(node): ChromeProjectBreadcrumb => ({
(node): ChromeBreadcrumb => ({
href: node.deepLink?.url ?? node.href,
deepLinkId: node.deepLink?.id as AppDeepLinkId,
text: node.title,
@ -99,7 +98,7 @@ function buildRootCrumb({
projectName?: string;
cloudLinks: CloudLinks;
isServerless: boolean;
}): ChromeProjectBreadcrumb {
}): ChromeBreadcrumb {
if (isServerless) {
return {
text:

View file

@ -11,7 +11,6 @@ import { InternalApplicationStart } from '@kbn/core-application-browser-internal
import type {
ChromeNavLinks,
SideNavComponent,
ChromeProjectBreadcrumb,
ChromeBreadcrumb,
ChromeSetProjectBreadcrumbsParams,
ChromeProjectNavigationNode,
@ -80,7 +79,7 @@ export class ProjectNavigationService {
);
private projectBreadcrumbs$ = new BehaviorSubject<{
breadcrumbs: ChromeProjectBreadcrumb[];
breadcrumbs: ChromeBreadcrumb[];
params: ChromeSetProjectBreadcrumbsParams;
}>({ breadcrumbs: [], params: { absolute: false } });
private readonly stop$ = new ReplaySubject<void>(1);
@ -153,7 +152,7 @@ export class ProjectNavigationService {
return this.customProjectSideNavComponent$.asObservable();
},
setProjectBreadcrumbs: (
breadcrumbs: ChromeProjectBreadcrumb | ChromeProjectBreadcrumb[],
breadcrumbs: ChromeBreadcrumb | ChromeBreadcrumb[],
params?: Partial<ChromeSetProjectBreadcrumbsParams>
) => {
this.projectBreadcrumbs$.next({
@ -161,7 +160,7 @@ export class ProjectNavigationService {
params: { absolute: false, ...params },
});
},
getProjectBreadcrumbs$: (): Observable<ChromeProjectBreadcrumb[]> => {
getProjectBreadcrumbs$: (): Observable<ChromeBreadcrumb[]> => {
return combineLatest([
this.projectBreadcrumbs$,
this.activeNodes$,

View file

@ -9,8 +9,8 @@
import type {
ChromeStart,
ChromeBreadcrumb,
SideNavComponent,
ChromeProjectBreadcrumb,
ChromeSetProjectBreadcrumbsParams,
ChromeProjectNavigationNode,
AppDeepLinkId,
@ -87,6 +87,9 @@ export interface InternalChromeStart extends ChromeStart {
*/
setSideNavComponent(component: SideNavComponent | null): void;
/** Get an Observable of the current project breadcrumbs */
getBreadcrumbs$(): Observable<ChromeBreadcrumb[]>;
/**
* Set project breadcrumbs
* @param breadcrumbs
@ -95,7 +98,7 @@ export interface InternalChromeStart extends ChromeStart {
* Use {@link ServerlessPluginStart.setBreadcrumbs} to set project breadcrumbs.
*/
setBreadcrumbs(
breadcrumbs: ChromeProjectBreadcrumb[] | ChromeProjectBreadcrumb,
breadcrumbs: ChromeBreadcrumb[] | ChromeBreadcrumb,
params?: Partial<ChromeSetProjectBreadcrumbsParams>
): void;

View file

@ -84,6 +84,7 @@ const createStartContractMock = () => {
initNavigation: jest.fn(),
setSideNavComponent: jest.fn(),
setBreadcrumbs: jest.fn(),
getBreadcrumbs$: jest.fn(),
getActiveNavigationNodes$: jest.fn(),
getNavigationTreeUi$: jest.fn(),
changeActiveSolutionNavigation: jest.fn(),

View file

@ -12,6 +12,7 @@ export type {
AppId,
ChromeBadge,
ChromeBreadcrumb,
ChromeSetBreadcrumbsParams,
ChromeBreadcrumbsAppendExtension,
ChromeDocTitle,
ChromeGlobalHelpExtensionMenuLink,
@ -41,7 +42,6 @@ export type {
SideNavCompProps,
SideNavComponent,
SideNavNodeStatus,
ChromeProjectBreadcrumb,
ChromeSetProjectBreadcrumbsParams,
NodeDefinition,
NodeDefinitionWithChildren,

View file

@ -24,3 +24,22 @@ export interface ChromeBreadcrumb extends EuiBreadcrumb {
export interface ChromeBreadcrumbsAppendExtension {
content: MountPoint<HTMLDivElement>;
}
/** @public */
export interface ChromeSetBreadcrumbsParams {
/**
* Declare the breadcrumbs for the project/solution type navigation in stateful.
* Those breadcrumbs correspond to the serverless breadcrumbs declaration.
*/
project?: {
/**
* The breadcrumb value to set. Can be a single breadcrumb or an array of breadcrumbs.
*/
value: ChromeBreadcrumb | ChromeBreadcrumb[];
/**
* Indicates whether the breadcrumb should be absolute (replaces the full path) or relative.
* @default false
*/
absolute?: boolean;
};
}

View file

@ -13,7 +13,11 @@ import type { ChromeRecentlyAccessed } from './recently_accessed';
import type { ChromeDocTitle } from './doc_title';
import type { ChromeHelpMenuLink, ChromeNavControls } from './nav_controls';
import type { ChromeHelpExtension } from './help_extension';
import type { ChromeBreadcrumb, ChromeBreadcrumbsAppendExtension } from './breadcrumb';
import type {
ChromeBreadcrumb,
ChromeBreadcrumbsAppendExtension,
ChromeSetBreadcrumbsParams,
} from './breadcrumb';
import type { ChromeBadge, ChromeStyle, ChromeUserBanner } from './types';
import type { ChromeGlobalHelpExtensionMenuLink } from './help_extension';
import type { PanelSelectedNode } from './project_navigation';
@ -84,7 +88,7 @@ export interface ChromeStart {
/**
* Override the current set of breadcrumbs
*/
setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void;
setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[], params?: ChromeSetBreadcrumbsParams): void;
/**
* Get an observable of the current extension appended to breadcrumbs

View file

@ -7,7 +7,11 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export type { ChromeBreadcrumbsAppendExtension, ChromeBreadcrumb } from './breadcrumb';
export type {
ChromeBreadcrumbsAppendExtension,
ChromeBreadcrumb,
ChromeSetBreadcrumbsParams,
} from './breadcrumb';
export type { ChromeStart } from './contracts';
export type { ChromeDocTitle } from './doc_title';
export type {
@ -42,7 +46,6 @@ export type {
SideNavComponent,
SideNavNodeStatus,
ChromeSetProjectBreadcrumbsParams,
ChromeProjectBreadcrumb,
NodeDefinition,
NodeDefinitionWithChildren,
RenderAs as NodeRenderAs,

View file

@ -39,7 +39,6 @@ import type { AppId as SecurityApp, DeepLinkId as SecurityLink } from '@kbn/deep
import type { AppId as FleetApp, DeepLinkId as FleetLink } from '@kbn/deeplinks-fleet';
import type { AppId as SharedApp, DeepLinkId as SharedLink } from '@kbn/deeplinks-shared';
import type { ChromeBreadcrumb } from './breadcrumb';
import type { ChromeNavLink } from './nav_links';
import type { ChromeRecentlyAccessedHistoryItem } from './recently_accessed';
@ -262,9 +261,6 @@ export interface SideNavCompProps {
/** @public */
export type SideNavComponent = ComponentType<SideNavCompProps>;
/** @public */
export type ChromeProjectBreadcrumb = ChromeBreadcrumb;
/** @public */
export interface ChromeSetProjectBreadcrumbsParams {
absolute: boolean;

View file

@ -189,7 +189,10 @@ export function InternalDashboardTopNav({
},
},
...dashboardTitleBreadcrumbs,
])
]),
{
project: { value: dashboardTitleBreadcrumbs },
}
);
}
}, [redirectTo, dashboardTitle, dashboardApi, viewMode, customLeadingBreadCrumbs]);

View file

@ -131,7 +131,9 @@ export class ManagementPlugin
const [, ...trailingBreadcrumbs] = newBreadcrumbs;
deps.serverless.setBreadcrumbs(trailingBreadcrumbs);
} else {
coreStart.chrome.setBreadcrumbs(newBreadcrumbs);
coreStart.chrome.setBreadcrumbs(newBreadcrumbs, {
project: { value: newBreadcrumbs, absolute: true },
});
}
},
isSidebarEnabled$: managementPlugin.isSidebarEnabled$,

View file

@ -10,6 +10,10 @@ import type { Services } from '../common/services';
export const subscribeBreadcrumbs = (services: Services) => {
const { securitySolution, chrome } = services;
securitySolution.getBreadcrumbsNav$().subscribe((breadcrumbsNav) => {
chrome.setBreadcrumbs([...breadcrumbsNav.leading, ...breadcrumbsNav.trailing]);
chrome.setBreadcrumbs([...breadcrumbsNav.leading, ...breadcrumbsNav.trailing], {
project: {
value: breadcrumbsNav.trailing,
},
});
});
};

View file

@ -6,7 +6,7 @@
*/
import type {
ChromeProjectBreadcrumb,
ChromeBreadcrumb,
ChromeSetProjectBreadcrumbsParams,
SideNavComponent,
NavigationTreeDefinition,
@ -21,7 +21,7 @@ export interface ServerlessPluginSetup {}
export interface ServerlessPluginStart {
setBreadcrumbs: (
breadcrumbs: ChromeProjectBreadcrumb | ChromeProjectBreadcrumb[],
breadcrumbs: ChromeBreadcrumb | ChromeBreadcrumb[],
params?: Partial<ChromeSetProjectBreadcrumbsParams>
) => void;
setProjectHome(homeHref: string): void;

View file

@ -82,7 +82,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await solutionNavigation.sidenav.openSection('project_settings_project_nav');
await solutionNavigation.sidenav.clickLink({ deepLinkId: 'management' });
await solutionNavigation.sidenav.expectLinkActive({ deepLinkId: 'management' });
await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ deepLinkId: 'management' });
await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Stack Management' });
// navigate back to the home page using header logo
await solutionNavigation.clickLogo();

View file

@ -64,7 +64,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await solutionNavigation.sidenav.openSection('project_settings_project_nav');
await solutionNavigation.sidenav.clickLink({ deepLinkId: 'management' });
await solutionNavigation.sidenav.expectLinkActive({ deepLinkId: 'management' });
await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ deepLinkId: 'management' });
await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Stack Management' });
// navigate back to the home page using header logo
await solutionNavigation.clickLogo();