Improve ML serverless breadcrumbs for oblt and security (#169513)

## Summary

close https://github.com/elastic/kibana/issues/167337

It introduces a new way to automatically set the deeper context
breadcrumbs in serverless navigation. Instead of using the
`serverless.setBreadcrumbs` for setting deeper context breadcrumbs in
serverless, the project navigation service merges navigational
breadcrumbs with regular chrome's breadcrumbs by deepLinkId
This commit is contained in:
Anton Dosov 2023-10-25 11:42:45 +02:00 committed by GitHub
parent 443cf43442
commit ce9a765d8e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 266 additions and 56 deletions

View file

@ -223,7 +223,12 @@ export class ChromeService {
const navControls = this.navControls.start();
const navLinks = this.navLinks.start({ application, http });
const projectNavigation = this.projectNavigation.start({ application, navLinks, http });
const projectNavigation = this.projectNavigation.start({
application,
navLinks,
http,
chromeBreadcrumbs$: breadcrumbs$,
});
const recentlyAccessed = await this.recentlyAccessed.start({ http });
const docTitle = this.docTitle.start();
const { customBranding$ } = customBranding;

View file

@ -0,0 +1,81 @@
/*
* 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 {
AppDeepLinkId,
ChromeProjectBreadcrumb,
ChromeProjectNavigationNode,
ChromeSetProjectBreadcrumbsParams,
ChromeBreadcrumb,
} from '@kbn/core-chrome-browser';
import { createHomeBreadcrumb } from './home_breadcrumbs';
export function buildBreadcrumbs({
homeHref,
projectBreadcrumbs,
activeNodes,
chromeBreadcrumbs,
}: {
homeHref: string;
projectBreadcrumbs: {
breadcrumbs: ChromeProjectBreadcrumb[];
params: ChromeSetProjectBreadcrumbsParams;
};
chromeBreadcrumbs: ChromeBreadcrumb[];
activeNodes: ChromeProjectNavigationNode[][];
}): ChromeProjectBreadcrumb[] {
const homeBreadcrumb = createHomeBreadcrumb({
homeHref,
});
if (projectBreadcrumbs.params.absolute) {
return [homeBreadcrumb, ...projectBreadcrumbs.breadcrumbs];
}
// breadcrumbs take the first active path
const activePath: ChromeProjectNavigationNode[] = activeNodes[0] ?? [];
const navBreadcrumbPath = activePath.filter(
(n) => Boolean(n.title) && n.breadcrumbStatus !== 'hidden'
);
const navBreadcrumbs = navBreadcrumbPath.map(
(node): ChromeProjectBreadcrumb => ({
href: node.deepLink?.url ?? node.href,
deepLinkId: node.deepLink?.id as AppDeepLinkId,
text: node.title,
})
);
// if there are project breadcrumbs set, use them
if (projectBreadcrumbs.breadcrumbs.length !== 0) {
return [homeBreadcrumb, ...navBreadcrumbs, ...projectBreadcrumbs.breadcrumbs];
}
// otherwise try to merge legacy breadcrumbs with navigational project breadcrumbs using deeplinkid
let chromeBreadcrumbStartIndex = -1;
let navBreadcrumbEndIndex = -1;
navBreadcrumbsLoop: for (let i = navBreadcrumbs.length - 1; i >= 0; i--) {
if (!navBreadcrumbs[i].deepLinkId) continue;
for (let j = 0; j < chromeBreadcrumbs.length; j++) {
if (chromeBreadcrumbs[j].deepLinkId === navBreadcrumbs[i].deepLinkId) {
chromeBreadcrumbStartIndex = j;
navBreadcrumbEndIndex = i;
break navBreadcrumbsLoop;
}
}
}
if (chromeBreadcrumbStartIndex === -1) {
return [homeBreadcrumb, ...navBreadcrumbs];
} else {
return [
homeBreadcrumb,
...navBreadcrumbs.slice(0, navBreadcrumbEndIndex),
...chromeBreadcrumbs.slice(chromeBreadcrumbStartIndex),
];
}
}

View file

@ -7,15 +7,17 @@
*/
import { createMemoryHistory } from 'history';
import { firstValueFrom, lastValueFrom, take } from 'rxjs';
import { firstValueFrom, lastValueFrom, take, BehaviorSubject } from 'rxjs';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
import { applicationServiceMock } from '@kbn/core-application-browser-mocks';
import type { ChromeNavLinks } from '@kbn/core-chrome-browser';
import type { ChromeNavLinks, ChromeBreadcrumb, AppDeepLinkId } from '@kbn/core-chrome-browser';
import { ProjectNavigationService } from './project_navigation_service';
const setup = ({ locationPathName = '/' }: { locationPathName?: string } = {}) => {
const projectNavigationService = new ProjectNavigationService();
const history = createMemoryHistory();
const chromeBreadcrumbs$ = new BehaviorSubject<ChromeBreadcrumb[]>([]);
history.replace(locationPathName);
const projectNavigation = projectNavigationService.start({
application: {
@ -24,15 +26,18 @@ const setup = ({ locationPathName = '/' }: { locationPathName?: string } = {}) =
},
navLinks: {} as unknown as ChromeNavLinks,
http: httpServiceMock.createStartContract(),
chromeBreadcrumbs$,
});
return { projectNavigation, history };
return { projectNavigation, history, chromeBreadcrumbs$ };
};
describe('breadcrumbs', () => {
const setupWithNavTree = () => {
const currentLocationPathName = '/foo/item1';
const { projectNavigation, history } = setup({ locationPathName: currentLocationPathName });
const { projectNavigation, chromeBreadcrumbs$, history } = setup({
locationPathName: currentLocationPathName,
});
const mockNavigation = {
navigationTree: [
@ -66,7 +71,7 @@ describe('breadcrumbs', () => {
],
};
projectNavigation.setProjectNavigation(mockNavigation);
return { projectNavigation, history, mockNavigation };
return { projectNavigation, history, mockNavigation, chromeBreadcrumbs$ };
};
test('should set breadcrumbs home / nav / custom', async () => {
@ -89,7 +94,7 @@ describe('breadcrumbs', () => {
"title": "Home",
},
Object {
"data-test-subj": "breadcrumb-deepLinkId-navItem1",
"deepLinkId": "navItem1",
"href": "/foo/item1",
"text": "Nav Item 1",
},
@ -139,6 +144,38 @@ describe('breadcrumbs', () => {
`);
});
test('should merge nav breadcrumbs and chrome breadcrumbs', async () => {
const { projectNavigation, chromeBreadcrumbs$ } = setupWithNavTree();
projectNavigation.setProjectBreadcrumbs([]);
chromeBreadcrumbs$.next([
{ text: 'Kibana' },
{ deepLinkId: 'navItem1' as AppDeepLinkId, text: 'Nav Item 1 from Chrome' },
{ text: 'Deep context from Chrome' },
]);
const breadcrumbs = await firstValueFrom(projectNavigation.getProjectBreadcrumbs$());
expect(breadcrumbs).toMatchInlineSnapshot(`
Array [
Object {
"data-test-subj": "breadcrumb-home",
"href": "/",
"text": <EuiIcon
type="home"
/>,
"title": "Home",
},
Object {
"deepLinkId": "navItem1",
"text": "Nav Item 1 from Chrome",
},
Object {
"text": "Deep context from Chrome",
},
]
`);
});
test('should reset custom breadcrumbs when active path changes', async () => {
const { projectNavigation, history } = setupWithNavTree();
projectNavigation.setProjectBreadcrumbs([

View file

@ -12,6 +12,7 @@ import {
ChromeProjectNavigation,
SideNavComponent,
ChromeProjectBreadcrumb,
ChromeBreadcrumb,
ChromeSetProjectBreadcrumbsParams,
ChromeProjectNavigationNode,
} from '@kbn/core-chrome-browser';
@ -29,15 +30,15 @@ import {
} from 'rxjs';
import type { Location } from 'history';
import deepEqual from 'react-fast-compare';
import classnames from 'classnames';
import { createHomeBreadcrumb } from './home_breadcrumbs';
import { findActiveNodes, flattenNav, stripQueryParams } from './utils';
import { buildBreadcrumbs } from './breadcrumbs';
interface StartDeps {
application: InternalApplicationStart;
navLinks: ChromeNavLinks;
http: HttpStart;
chromeBreadcrumbs$: Observable<ChromeBreadcrumb[]>;
}
export class ProjectNavigationService {
@ -60,7 +61,7 @@ export class ProjectNavigationService {
private http?: HttpStart;
private unlistenHistory?: () => void;
public start({ application, navLinks, http }: StartDeps) {
public start({ application, navLinks, http, chromeBreadcrumbs$ }: StartDeps) {
this.application = application;
this.http = http;
this.onHistoryLocationChange(application.history.location);
@ -136,33 +137,15 @@ export class ProjectNavigationService {
this.projectBreadcrumbs$,
this.activeNodes$,
this.projectHome$.pipe(map((homeHref) => homeHref ?? '/')),
chromeBreadcrumbs$,
]).pipe(
map(([breadcrumbs, activeNodes, homeHref]) => {
const homeBreadcrumb = createHomeBreadcrumb({
map(([projectBreadcrumbs, activeNodes, homeHref, chromeBreadcrumbs]) => {
return buildBreadcrumbs({
homeHref: this.http?.basePath.prepend?.(homeHref) ?? homeHref,
projectBreadcrumbs,
activeNodes,
chromeBreadcrumbs,
});
if (breadcrumbs.params.absolute) {
return [homeBreadcrumb, ...breadcrumbs.breadcrumbs];
} else {
// breadcrumbs take the first active path
const activePath: ChromeProjectNavigationNode[] = activeNodes[0] ?? [];
const navBreadcrumbs = activePath
.filter((n) => Boolean(n.title) && n.breadcrumbStatus !== 'hidden')
.map(
(node): ChromeProjectBreadcrumb => ({
href: node.deepLink?.url ?? node.href,
text: node.title,
'data-test-subj': classnames({
[`breadcrumb-deepLinkId-${node.deepLink?.id}`]: !!node.deepLink,
}),
})
);
const result = [homeBreadcrumb, ...navBreadcrumbs, ...breadcrumbs.breadcrumbs];
return result;
}
})
);
},

View file

@ -27,13 +27,15 @@ export function HeaderBreadcrumbs({ breadcrumbs$ }: Props) {
crumbs = crumbs.map((breadcrumb, i) => {
const isLast = i === breadcrumbs.length - 1;
const { deepLinkId, ...rest } = breadcrumb;
return {
...breadcrumb,
...rest,
href: isLast ? undefined : breadcrumb.href,
onClick: isLast ? undefined : breadcrumb.onClick,
'data-test-subj': classNames(
'breadcrumb',
deepLinkId && `breadcrumb-deepLinkId-${deepLinkId}`,
breadcrumb['data-test-subj'],
i === 0 && 'first',
isLast && 'last'

View file

@ -8,9 +8,16 @@
import type { EuiBreadcrumb } from '@elastic/eui';
import type { MountPoint } from '@kbn/core-mount-utils-browser';
import type { AppDeepLinkId } from './project_navigation';
/** @public */
export type ChromeBreadcrumb = EuiBreadcrumb;
export interface ChromeBreadcrumb extends EuiBreadcrumb {
/**
* The deepLinkId can be used to merge the navigational breadcrumbs set via project navigation
* with the deeper context breadcrumbs set via the `chrome.setBreadcrumbs` API.
*/
deepLinkId?: AppDeepLinkId;
}
/** @public */
export interface ChromeBreadcrumbsAppendExtension {

View file

@ -33,11 +33,14 @@ export const defaultNavigation: MlNodeDefinition = {
{
link: 'ml:notifications',
},
{
link: 'ml:memoryUsage',
},
{
title: i18n.translate('defaultNavigation.ml.anomalyDetection', {
defaultMessage: 'Anomaly Detection',
}),
id: 'anomaly_detection',
link: 'ml:anomalyDetection',
renderAs: 'accordion',
children: [
{
@ -45,6 +48,7 @@ export const defaultNavigation: MlNodeDefinition = {
defaultMessage: 'Jobs',
}),
link: 'ml:anomalyDetection',
breadcrumbStatus: 'hidden',
},
{
link: 'ml:anomalyExplorer',
@ -58,7 +62,7 @@ export const defaultNavigation: MlNodeDefinition = {
],
},
{
id: 'data_frame_analytics',
link: 'ml:dataFrameAnalytics',
title: i18n.translate('defaultNavigation.ml.dataFrameAnalytics', {
defaultMessage: 'Data Frame Analytics',
}),
@ -67,6 +71,7 @@ export const defaultNavigation: MlNodeDefinition = {
{
title: 'Jobs',
link: 'ml:dataFrameAnalytics',
breadcrumbStatus: 'hidden',
},
{
link: 'ml:resultExplorer',
@ -109,12 +114,21 @@ export const defaultNavigation: MlNodeDefinition = {
defaultMessage: 'Data view',
}),
link: 'ml:indexDataVisualizer',
getIsActive: ({ pathNameSerialized, prepend }) => {
return (
pathNameSerialized.includes(prepend('/app/ml/datavisualizer')) ||
pathNameSerialized.includes(prepend('/app/ml/jobs/new_job/datavisualizer'))
);
},
},
{
title: i18n.translate('defaultNavigation.ml.dataComparison', {
defaultMessage: 'Data drift',
}),
link: 'ml:dataDrift',
getIsActive: ({ pathNameSerialized, prepend }) => {
return pathNameSerialized.includes(prepend('/app/ml/data_drift'));
},
},
],
},
@ -127,12 +141,21 @@ export const defaultNavigation: MlNodeDefinition = {
children: [
{
link: 'ml:logRateAnalysis',
getIsActive: ({ pathNameSerialized, prepend }) => {
return pathNameSerialized.includes(prepend('/app/ml/aiops/log_rate_analysis'));
},
},
{
link: 'ml:logPatternAnalysis',
getIsActive: ({ pathNameSerialized, prepend }) => {
return pathNameSerialized.includes(prepend('/app/ml/aiops/log_categorization'));
},
},
{
link: 'ml:changePointDetections',
getIsActive: ({ pathNameSerialized, prepend }) => {
return pathNameSerialized.includes(prepend('/app/ml/aiops/change_point_detection'));
},
},
],
},

View file

@ -190,9 +190,30 @@ Array [
"sideNavStatus": "visible",
"title": "Deeplink ml:notifications",
},
Object {
"children": undefined,
"deepLink": Object {
"baseUrl": "/mocked",
"href": "http://mocked/ml:memoryUsage",
"id": "ml:memoryUsage",
"title": "Deeplink ml:memoryUsage",
"url": "/mocked/ml:memoryUsage",
},
"href": undefined,
"id": "ml:memoryUsage",
"isActive": false,
"isGroup": false,
"path": Array [
"rootNav:ml",
"ml:memoryUsage",
],
"sideNavStatus": "visible",
"title": "Deeplink ml:memoryUsage",
},
Object {
"children": Array [
Object {
"breadcrumbStatus": "hidden",
"children": undefined,
"deepLink": Object {
"baseUrl": "/mocked",
@ -207,7 +228,7 @@ Array [
"isGroup": false,
"path": Array [
"rootNav:ml",
"anomaly_detection",
"ml:anomalyDetection",
"ml:anomalyDetection",
],
"sideNavStatus": "visible",
@ -228,7 +249,7 @@ Array [
"isGroup": false,
"path": Array [
"rootNav:ml",
"anomaly_detection",
"ml:anomalyDetection",
"ml:anomalyExplorer",
],
"sideNavStatus": "visible",
@ -249,7 +270,7 @@ Array [
"isGroup": false,
"path": Array [
"rootNav:ml",
"anomaly_detection",
"ml:anomalyDetection",
"ml:singleMetricViewer",
],
"sideNavStatus": "visible",
@ -270,21 +291,27 @@ Array [
"isGroup": false,
"path": Array [
"rootNav:ml",
"anomaly_detection",
"ml:anomalyDetection",
"ml:settings",
],
"sideNavStatus": "visible",
"title": "Deeplink ml:settings",
},
],
"deepLink": undefined,
"deepLink": Object {
"baseUrl": "/mocked",
"href": "http://mocked/ml:anomalyDetection",
"id": "ml:anomalyDetection",
"title": "Deeplink ml:anomalyDetection",
"url": "/mocked/ml:anomalyDetection",
},
"href": undefined,
"id": "anomaly_detection",
"id": "ml:anomalyDetection",
"isActive": false,
"isGroup": true,
"path": Array [
"rootNav:ml",
"anomaly_detection",
"ml:anomalyDetection",
],
"renderAs": "accordion",
"sideNavStatus": "visible",
@ -293,6 +320,7 @@ Array [
Object {
"children": Array [
Object {
"breadcrumbStatus": "hidden",
"children": undefined,
"deepLink": Object {
"baseUrl": "/mocked",
@ -307,7 +335,7 @@ Array [
"isGroup": false,
"path": Array [
"rootNav:ml",
"data_frame_analytics",
"ml:dataFrameAnalytics",
"ml:dataFrameAnalytics",
],
"sideNavStatus": "visible",
@ -328,7 +356,7 @@ Array [
"isGroup": false,
"path": Array [
"rootNav:ml",
"data_frame_analytics",
"ml:dataFrameAnalytics",
"ml:resultExplorer",
],
"sideNavStatus": "visible",
@ -349,21 +377,27 @@ Array [
"isGroup": false,
"path": Array [
"rootNav:ml",
"data_frame_analytics",
"ml:dataFrameAnalytics",
"ml:analyticsMap",
],
"sideNavStatus": "visible",
"title": "Deeplink ml:analyticsMap",
},
],
"deepLink": undefined,
"deepLink": Object {
"baseUrl": "/mocked",
"href": "http://mocked/ml:dataFrameAnalytics",
"id": "ml:dataFrameAnalytics",
"title": "Deeplink ml:dataFrameAnalytics",
"url": "/mocked/ml:dataFrameAnalytics",
},
"href": undefined,
"id": "data_frame_analytics",
"id": "ml:dataFrameAnalytics",
"isActive": false,
"isGroup": true,
"path": Array [
"rootNav:ml",
"data_frame_analytics",
"ml:dataFrameAnalytics",
],
"renderAs": "accordion",
"sideNavStatus": "visible",
@ -459,6 +493,7 @@ Array [
"title": "Deeplink ml:indexDataVisualizer",
"url": "/mocked/ml:indexDataVisualizer",
},
"getIsActive": [Function],
"href": undefined,
"id": "ml:indexDataVisualizer",
"isActive": false,
@ -480,6 +515,7 @@ Array [
"title": "Deeplink ml:dataDrift",
"url": "/mocked/ml:dataDrift",
},
"getIsActive": [Function],
"href": undefined,
"id": "ml:dataDrift",
"isActive": false,
@ -517,6 +553,7 @@ Array [
"title": "Deeplink ml:logRateAnalysis",
"url": "/mocked/ml:logRateAnalysis",
},
"getIsActive": [Function],
"href": undefined,
"id": "ml:logRateAnalysis",
"isActive": false,
@ -538,6 +575,7 @@ Array [
"title": "Deeplink ml:logPatternAnalysis",
"url": "/mocked/ml:logPatternAnalysis",
},
"getIsActive": [Function],
"href": undefined,
"id": "ml:logPatternAnalysis",
"isActive": false,
@ -559,6 +597,7 @@ Array [
"title": "Deeplink ml:changePointDetections",
"url": "/mocked/ml:changePointDetections",
},
"getIsActive": [Function],
"href": undefined,
"id": "ml:changePointDetections",
"isActive": false,

View file

@ -172,7 +172,7 @@ const nodeToEuiCollapsibleNavProps = (
spaceBefore: _spaceBefore,
} = navNode;
const isExternal = Boolean(href) && isAbsoluteLink(href!);
const isSelected = hasChildren ? false : isActive;
const isSelected = hasChildren && !isItem ? false : isActive;
const dataTestSubj = classnames(`nav-item`, `nav-item-${id}`, {
[`nav-item-deepLinkId-${deepLink?.id}`]: !!deepLink,
[`nav-item-id-${id}`]: id,

View file

@ -5,8 +5,6 @@
* 2.0.
*/
import { EuiBreadcrumb } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ChromeBreadcrumb } from '@kbn/core/public';
@ -25,6 +23,7 @@ export const SETTINGS_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
defaultMessage: 'Settings',
}),
href: '/settings',
deepLinkId: 'ml:settings',
});
export const ANOMALY_DETECTION_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
@ -32,6 +31,7 @@ export const ANOMALY_DETECTION_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
defaultMessage: 'Anomaly Detection',
}),
href: '/jobs',
deepLinkId: 'ml:anomalyDetection',
});
export const DATA_FRAME_ANALYTICS_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
@ -39,6 +39,7 @@ export const DATA_FRAME_ANALYTICS_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
defaultMessage: 'Data Frame Analytics',
}),
href: '/data_frame_analytics',
deepLinkId: 'ml:dataFrameAnalytics',
});
export const TRAINED_MODELS: ChromeBreadcrumb = Object.freeze({
@ -46,6 +47,7 @@ export const TRAINED_MODELS: ChromeBreadcrumb = Object.freeze({
defaultMessage: 'Model Management',
}),
href: '/trained_models',
deepLinkId: 'ml:modelManagement',
});
export const DATA_VISUALIZER_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
@ -53,6 +55,7 @@ export const DATA_VISUALIZER_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
defaultMessage: 'Data Visualizer',
}),
href: '/datavisualizer',
deepLinkId: 'ml:dataVisualizer',
});
// we need multiple AIOPS_BREADCRUMB breadcrumb items as they each need to link
@ -83,6 +86,7 @@ export const LOG_RATE_ANALYSIS: ChromeBreadcrumb = Object.freeze({
defaultMessage: 'Log Rate Analysis',
}),
href: '/aiops/log_rate_analysis_index_select',
deepLinkId: 'ml:logRateAnalysis',
});
export const LOG_PATTERN_ANALYSIS: ChromeBreadcrumb = Object.freeze({
@ -90,6 +94,7 @@ export const LOG_PATTERN_ANALYSIS: ChromeBreadcrumb = Object.freeze({
defaultMessage: 'Log Pattern Analysis',
}),
href: '/aiops/log_categorization_index_select',
deepLinkId: 'ml:logPatternAnalysis',
});
export const CHANGE_POINT_DETECTION: ChromeBreadcrumb = Object.freeze({
@ -97,6 +102,7 @@ export const CHANGE_POINT_DETECTION: ChromeBreadcrumb = Object.freeze({
defaultMessage: 'Change Point Detection',
}),
href: '/aiops/change_point_detection_index_select',
deepLinkId: 'ml:changePointDetections',
});
export const CREATE_JOB_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
@ -111,6 +117,7 @@ export const CALENDAR_MANAGEMENT_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
defaultMessage: 'Calendar management',
}),
href: '/settings/calendars_list',
deepLinkId: 'ml:calendarSettings',
});
export const FILTER_LISTS_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
@ -118,6 +125,7 @@ export const FILTER_LISTS_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
defaultMessage: 'Filter lists',
}),
href: '/settings/filter_lists',
deepLinkId: 'ml:filterListsSettings',
});
export const DATA_DRIFT_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
@ -125,6 +133,7 @@ export const DATA_DRIFT_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
defaultMessage: 'Data drift',
}),
href: '/data_drift_index_select',
deepLinkId: 'ml:dataDrift',
});
const breadcrumbs = {
@ -150,7 +159,7 @@ type Breadcrumb = keyof typeof breadcrumbs;
export const breadcrumbOnClickFactory = (
path: string | undefined,
navigateToPath: NavigateToPath
): EuiBreadcrumb['onClick'] => {
): ChromeBreadcrumb['onClick'] => {
return (e) => {
e.preventDefault();
navigateToPath(path);
@ -161,12 +170,13 @@ export const getBreadcrumbWithUrlForApp = (
breadcrumbName: Breadcrumb,
navigateToPath?: NavigateToPath,
basePath?: string
): EuiBreadcrumb => {
): ChromeBreadcrumb => {
return {
text: breadcrumbs[breadcrumbName].text,
...(navigateToPath
? {
href: `${basePath}/app/ml${breadcrumbs[breadcrumbName].href}`,
deepLinkId: breadcrumbs[breadcrumbName].deepLinkId,
onClick: breadcrumbOnClickFactory(breadcrumbs[breadcrumbName].href, navigateToPath),
}
: {}),

View file

@ -81,6 +81,8 @@ export const getFormatChromeProjectNavNodes = (services: Services) => {
id,
title: node.title || '',
path: [...path, id],
breadcrumbStatus: node.breadcrumbStatus,
getIsActive: node.getIsActive,
};
if (chrome.navLinks.has(id)) {
const deepLink = chrome.navLinks.get(id);

View file

@ -90,6 +90,18 @@ const navigationTree: NavigationTreeDefinition = {
defaultMessage: 'Anomaly detection',
}),
link: 'ml:anomalyDetection',
renderAs: 'item',
children: [
{
link: 'ml:singleMetricViewer',
},
{
link: 'ml:anomalyExplorer',
},
{
link: 'ml:settings',
},
],
},
{
title: i18n.translate('xpack.serverlessObservability.ml.logRateAnalysis', {

View file

@ -59,6 +59,15 @@ export default function ({ getPageObject, getService }: FtrProviderContext) {
await svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({
deepLinkId: 'ml:anomalyDetection',
});
await svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({
text: 'Jobs',
});
await testSubjects.click('mlCreateNewJobButton');
await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts([
'AIOps',
'Anomaly Detection',
'Create job',
]);
// navigate to a different section
await svlCommonNavigation.sidenav.openSection('project_settings_project_nav');