mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Project Side Navigation: Use EuiCollapsibleNavBeta component (#164910)
## Summary Closes https://github.com/elastic/kibana/issues/162507 Relates to https://github.com/elastic/kibana/issues/166545 ^ additional IA-related tasks - related to the alignment discussions - can be found here ## Work for next steps In this PR, some work items are being saved for a next PR: 1. _Only affects Search solution_: Navigation "group titles" do not create a breadcrumb item, as sub-items in the group are not hierarchically under the title. To address this, group titles may be going away from the design. https://github.com/elastic/kibana/issues/167323 2. _Only affects Observability solution_: Navigation accordions can not be collapsed and do not show arrow icons. To address this, in a later PR we will add internal state management for the open/closed state of each accordion. https://github.com/elastic/kibana/issues/167328 3. _Affects all solutions:_ The "collapsed" state of the side nav should show a docked view with icons-only. To address this, in later PRs we will bring Security solution into the unified nav components. 4. https://github.com/elastic/kibana/issues/167326 5. https://github.com/elastic/kibana/issues/167330 6. https://github.com/elastic/kibana/issues/167332 ### Recordings These videos show a before-and-after with the new UI. | project | old | new | |--|--|--| |observability|663765a3
-4e4b-416e-b7d5-7d87eece83e8 | <img width="298" alt="CleanShot 2023-09-22 at 14 20 48@2x" src="d61f6fe0
-a6a9-4806-bc27-08b0ff2afb49"> | |search|f383773e
-27a8-4485-8289-274d8231b960 | <img width="281" alt="CleanShot 2023-09-22 at 14 18 43@2x" src="901b8fc1
-9945-4cee-b566-5e4539f08043"> | |security|481f4533
-64e5-41db-bc8e-5012f82c188a | *will change to the new style after this PR and the flyout/panel support are completed | ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Sébastien Loix <sebastien.loix@elastic.co>
This commit is contained in:
parent
9fba1e3f24
commit
0c680d7783
28 changed files with 780 additions and 1129 deletions
|
@ -13,7 +13,6 @@ import React from 'react';
|
|||
import { HeaderActionMenu } from '../header/header_action_menu';
|
||||
|
||||
interface AppMenuBarProps {
|
||||
isOpen: boolean;
|
||||
headerActionMenuMounter: { mount: MountPoint<HTMLElement> | undefined };
|
||||
}
|
||||
export const AppMenuBar = ({ headerActionMenuMounter }: AppMenuBarProps) => {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import { EuiHeader } from '@elastic/eui';
|
||||
import { applicationServiceMock } from '@kbn/core-application-browser-mocks';
|
||||
import { docLinksServiceMock } from '@kbn/core-doc-links-browser-mocks';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import * as Rx from 'rxjs';
|
||||
import { ProjectHeader, Props as ProjectHeaderProps } from './header';
|
||||
|
@ -45,35 +45,10 @@ describe('Header', () => {
|
|||
</ProjectHeader>
|
||||
);
|
||||
|
||||
expect(await screen.findByTestId('toggleNavButton')).toBeVisible();
|
||||
expect(await screen.findByTestId('euiCollapsibleNavButton')).toBeVisible();
|
||||
expect(await screen.findByText('Hello, world!')).toBeVisible();
|
||||
});
|
||||
|
||||
it('can collapse and uncollapse', async () => {
|
||||
render(
|
||||
<ProjectHeader {...mockProps}>
|
||||
<EuiHeader>Hello, goodbye!</EuiHeader>
|
||||
</ProjectHeader>
|
||||
);
|
||||
|
||||
expect(await screen.findByTestId('toggleNavButton')).toBeVisible();
|
||||
expect(await screen.findByText('Hello, goodbye!')).toBeVisible(); // title is shown
|
||||
|
||||
const toggleNav = async () => {
|
||||
fireEvent.click(await screen.findByTestId('toggleNavButton')); // click
|
||||
|
||||
expect(await screen.findByText('Hello, goodbye!')).not.toBeVisible();
|
||||
|
||||
fireEvent.click(await screen.findByTestId('toggleNavButton')); // click again
|
||||
|
||||
expect(await screen.findByText('Hello, goodbye!')).toBeVisible(); // title is shown
|
||||
};
|
||||
|
||||
await toggleNav();
|
||||
await toggleNav();
|
||||
await toggleNav();
|
||||
});
|
||||
|
||||
it('displays the link to projects', async () => {
|
||||
render(
|
||||
<ProjectHeader {...mockProps}>
|
||||
|
@ -81,7 +56,7 @@ describe('Header', () => {
|
|||
</ProjectHeader>
|
||||
);
|
||||
|
||||
const projectsLink = await screen.getByTestId('projectsLink');
|
||||
const projectsLink = screen.getByTestId('projectsLink');
|
||||
expect(projectsLink).toHaveAttribute('href', '/projects/');
|
||||
expect(projectsLink).toHaveTextContent('My Project');
|
||||
});
|
||||
|
|
|
@ -12,10 +12,7 @@ import {
|
|||
EuiHeaderLogo,
|
||||
EuiHeaderSection,
|
||||
EuiHeaderSectionItem,
|
||||
EuiHeaderSectionItemButton,
|
||||
EuiIcon,
|
||||
EuiLoadingSpinner,
|
||||
htmlIdGenerator,
|
||||
useEuiTheme,
|
||||
EuiThemeComputed,
|
||||
} from '@elastic/eui';
|
||||
|
@ -35,8 +32,7 @@ import { MountPoint } from '@kbn/core-mount-utils-browser';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
|
||||
import { Router } from '@kbn/shared-ux-router';
|
||||
import React, { createRef, useCallback, useState } from 'react';
|
||||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
import React, { useCallback } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { debounceTime, Observable, of } from 'rxjs';
|
||||
import { useHeaderActionMenuMounter } from '../header/header_action_menu';
|
||||
|
@ -66,13 +62,6 @@ const getHeaderCss = ({ size }: EuiThemeComputed) => ({
|
|||
top: 2px;
|
||||
`,
|
||||
},
|
||||
nav: {
|
||||
toggleNavButton: css`
|
||||
border-right: 1px solid #d3dae6;
|
||||
margin-left: -1px;
|
||||
padding-right: ${size.xs};
|
||||
`,
|
||||
},
|
||||
projectName: {
|
||||
link: css`
|
||||
/* TODO: make header layout more flexible? */
|
||||
|
@ -123,7 +112,6 @@ export interface Props {
|
|||
prependBasePath: (url: string) => string;
|
||||
}
|
||||
|
||||
const LOCAL_STORAGE_IS_OPEN_KEY = 'PROJECT_NAVIGATION_OPEN' as const;
|
||||
const LOADING_DEBOUNCE_TIME = 80;
|
||||
|
||||
type LogoProps = Pick<Props, 'application' | 'homeHref$' | 'loadingCount$' | 'prependBasePath'> & {
|
||||
|
@ -186,9 +174,6 @@ export const ProjectHeader = ({
|
|||
docLinks,
|
||||
...observables
|
||||
}: Props) => {
|
||||
const [navId] = useState(htmlIdGenerator()());
|
||||
const [isOpen, setIsOpen] = useLocalStorage(LOCAL_STORAGE_IS_OPEN_KEY, true);
|
||||
const toggleCollapsibleNavRef = createRef<HTMLButtonElement & { euiAnimate: () => void }>();
|
||||
const headerActionMenuMounter = useHeaderActionMenuMounter(observables.actionMenu$);
|
||||
const projectsUrl = useObservable(observables.projectsUrl$);
|
||||
const projectName = useObservable(observables.projectName$);
|
||||
|
@ -210,34 +195,9 @@ export const ProjectHeader = ({
|
|||
<div id="globalHeaderBars" data-test-subj="headerGlobalNav" className="header__bars">
|
||||
<EuiHeader position="fixed" className="header__firstBar">
|
||||
<EuiHeaderSection grow={false}>
|
||||
<EuiHeaderSectionItem css={headerCss.nav.toggleNavButton}>
|
||||
<Router history={application.history}>
|
||||
<ProjectNavigation
|
||||
isOpen={isOpen!}
|
||||
closeNav={() => {
|
||||
setIsOpen(false);
|
||||
if (toggleCollapsibleNavRef.current) {
|
||||
toggleCollapsibleNavRef.current.focus();
|
||||
}
|
||||
}}
|
||||
button={
|
||||
<EuiHeaderSectionItemButton
|
||||
data-test-subj="toggleNavButton"
|
||||
aria-label={headerStrings.nav.closeNavAriaLabel}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
aria-expanded={isOpen!}
|
||||
aria-pressed={isOpen!}
|
||||
aria-controls={navId}
|
||||
ref={toggleCollapsibleNavRef}
|
||||
>
|
||||
<EuiIcon type={isOpen ? 'menuLeft' : 'menuRight'} size="m" />
|
||||
</EuiHeaderSectionItemButton>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</ProjectNavigation>
|
||||
</Router>
|
||||
</EuiHeaderSectionItem>
|
||||
<Router history={application.history}>
|
||||
<ProjectNavigation>{children}</ProjectNavigation>
|
||||
</Router>
|
||||
|
||||
<EuiHeaderSectionItem>
|
||||
<Logo
|
||||
|
@ -297,7 +257,7 @@ export const ProjectHeader = ({
|
|||
</header>
|
||||
|
||||
{headerActionMenuMounter.mount && (
|
||||
<AppMenuBar isOpen={isOpen ?? false} headerActionMenuMounter={headerActionMenuMounter} />
|
||||
<AppMenuBar headerActionMenuMounter={headerActionMenuMounter} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -6,55 +6,25 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiCollapsibleNavBeta } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { EuiCollapsibleNav, EuiCollapsibleNavProps } from '@elastic/eui';
|
||||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
|
||||
const SIZE_EXPANDED = 248;
|
||||
const SIZE_COLLAPSED = 0;
|
||||
const LOCAL_STORAGE_IS_COLLAPSED_KEY = 'PROJECT_NAVIGATION_COLLAPSED' as const;
|
||||
|
||||
export interface ProjectNavigationProps {
|
||||
isOpen: boolean;
|
||||
closeNav: () => void;
|
||||
button: EuiCollapsibleNavProps['button'];
|
||||
}
|
||||
|
||||
export const ProjectNavigation: React.FC<ProjectNavigationProps> = ({
|
||||
children,
|
||||
isOpen,
|
||||
closeNav,
|
||||
button,
|
||||
}) => {
|
||||
const collabsibleNavCSS = css`
|
||||
border-inline-end-width: 1,
|
||||
display: flex,
|
||||
flex-direction: row,
|
||||
`;
|
||||
|
||||
const DOCKED_BREAKPOINT = 's' as const;
|
||||
const isVisible = isOpen;
|
||||
export const ProjectNavigation: React.FC = ({ children }) => {
|
||||
const [isCollapsed, setIsCollapsed] = useLocalStorage(LOCAL_STORAGE_IS_COLLAPSED_KEY, false);
|
||||
const onCollapseToggle = (nextIsCollapsed: boolean) => {
|
||||
setIsCollapsed(nextIsCollapsed);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
/* must render the tree to initialize the navigation, even if it shouldn't be visible */
|
||||
!isOpen && <div hidden>{children}</div>
|
||||
}
|
||||
<EuiCollapsibleNav
|
||||
className="projectLayoutSideNav"
|
||||
css={collabsibleNavCSS}
|
||||
isOpen={isVisible /* only affects docked state */}
|
||||
showButtonIfDocked={true}
|
||||
onClose={closeNav}
|
||||
isDocked={true}
|
||||
size={isVisible ? SIZE_EXPANDED : SIZE_COLLAPSED}
|
||||
hideCloseButton={false}
|
||||
dockedBreakpoint={DOCKED_BREAKPOINT}
|
||||
ownFocus={false}
|
||||
button={button}
|
||||
>
|
||||
{isOpen && children}
|
||||
</EuiCollapsibleNav>
|
||||
</>
|
||||
<EuiCollapsibleNavBeta
|
||||
initialIsCollapsed={isCollapsed}
|
||||
onCollapseToggle={onCollapseToggle}
|
||||
css={isCollapsed ? { display: 'none;' } : {}}
|
||||
>
|
||||
{children}
|
||||
</EuiCollapsibleNavBeta>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,8 +5,10 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { ComponentType } from 'react';
|
||||
import type { Location } from 'history';
|
||||
import { EuiAccordionProps } from '@elastic/eui';
|
||||
import type { AppId as DevToolsApp, DeepLinkId as DevToolsLink } from '@kbn/deeplinks-devtools';
|
||||
import type {
|
||||
AppId as AnalyticsApp,
|
||||
|
@ -68,6 +70,8 @@ export interface ChromeProjectNavigationNode {
|
|||
deepLink?: ChromeNavLink;
|
||||
/** Optional icon for the navigation node. Note: not all navigation depth will render the icon */
|
||||
icon?: string;
|
||||
/** Optional flag to indicate if the node must be treated as a group title */
|
||||
isGroupTitle?: boolean;
|
||||
/** Optional children of the navigation node */
|
||||
children?: ChromeProjectNavigationNode[];
|
||||
/**
|
||||
|
@ -88,6 +92,8 @@ export interface ChromeProjectNavigationNode {
|
|||
* @default 'visible'
|
||||
*/
|
||||
breadcrumbStatus?: 'hidden' | 'visible';
|
||||
|
||||
accordionProps?: Partial<EuiAccordionProps>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -139,7 +145,12 @@ export interface NodeDefinition<
|
|||
cloudLink?: CloudLinkId;
|
||||
/** Optional icon for the navigation node. Note: not all navigation depth will render the icon */
|
||||
icon?: string;
|
||||
/** Optional children of the navigation node */
|
||||
/**
|
||||
* Optional flag to indicate if the node must be treated as a group title.
|
||||
* Can not be used with `children`
|
||||
*/
|
||||
isGroupTitle?: boolean;
|
||||
/** Optional children of the navigation node. Can not be used with `isGroupTitle` */
|
||||
children?: NonEmptyArray<NodeDefinition<LinkId, Id, ChildrenId>>;
|
||||
/**
|
||||
* Use href for absolute links only. Internal links should use "link".
|
||||
|
@ -155,6 +166,8 @@ export interface NodeDefinition<
|
|||
* @default 'visible'
|
||||
*/
|
||||
breadcrumbStatus?: 'hidden' | 'visible';
|
||||
|
||||
accordionProps?: Partial<EuiAccordionProps>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,18 +21,13 @@ export const defaultNavigation: AnalyticsNodeDefinition = {
|
|||
icon: 'stats',
|
||||
children: [
|
||||
{
|
||||
id: 'root',
|
||||
children: [
|
||||
{
|
||||
link: 'discover',
|
||||
},
|
||||
{
|
||||
link: 'dashboards',
|
||||
},
|
||||
{
|
||||
link: 'visualize',
|
||||
},
|
||||
],
|
||||
link: 'discover',
|
||||
},
|
||||
{
|
||||
link: 'dashboards',
|
||||
},
|
||||
{
|
||||
link: 'visualize',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -21,21 +21,16 @@ export const defaultNavigation: DevToolsNodeDefinition = {
|
|||
icon: 'editorCodeBlock',
|
||||
children: [
|
||||
{
|
||||
id: 'root',
|
||||
children: [
|
||||
{
|
||||
link: 'dev_tools:console',
|
||||
},
|
||||
{
|
||||
link: 'dev_tools:searchprofiler',
|
||||
},
|
||||
{
|
||||
link: 'dev_tools:grokdebugger',
|
||||
},
|
||||
{
|
||||
link: 'dev_tools:painless_lab',
|
||||
},
|
||||
],
|
||||
link: 'dev_tools:console',
|
||||
},
|
||||
{
|
||||
link: 'dev_tools:searchprofiler',
|
||||
},
|
||||
{
|
||||
link: 'dev_tools:grokdebugger',
|
||||
},
|
||||
{
|
||||
link: 'dev_tools:painless_lab',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -29,13 +29,7 @@ export const defaultNavigation: ManagementNodeDefinition = {
|
|||
icon: 'gear',
|
||||
children: [
|
||||
{
|
||||
id: 'root',
|
||||
title: '',
|
||||
children: [
|
||||
{
|
||||
link: 'monitoring',
|
||||
},
|
||||
],
|
||||
link: 'monitoring',
|
||||
},
|
||||
{
|
||||
id: 'integration_management',
|
||||
|
|
|
@ -28,16 +28,10 @@ export const defaultNavigation: MlNodeDefinition = {
|
|||
icon: 'machineLearningApp',
|
||||
children: [
|
||||
{
|
||||
title: '',
|
||||
id: 'root',
|
||||
children: [
|
||||
{
|
||||
link: 'ml:overview',
|
||||
},
|
||||
{
|
||||
link: 'ml:notifications',
|
||||
},
|
||||
],
|
||||
link: 'ml:overview',
|
||||
},
|
||||
{
|
||||
link: 'ml:notifications',
|
||||
},
|
||||
{
|
||||
title: i18n.translate('defaultNavigation.ml.anomalyDetection', {
|
||||
|
|
|
@ -14,7 +14,7 @@ import { NavigationServices } from '../../types';
|
|||
type Arguments = NavigationServices;
|
||||
export type Params = Pick<
|
||||
Arguments,
|
||||
'navIsOpen' | 'recentlyAccessed$' | 'navLinks$' | 'onProjectNavigationChange'
|
||||
'navIsOpen' | 'recentlyAccessed$' | 'activeNodes$' | 'navLinks$' | 'onProjectNavigationChange'
|
||||
>;
|
||||
|
||||
export class StorybookMock extends AbstractStorybookMock<{}, NavigationServices> {
|
||||
|
@ -43,7 +43,7 @@ export class StorybookMock extends AbstractStorybookMock<{}, NavigationServices>
|
|||
recentlyAccessed$: params.recentlyAccessed$ ?? new BehaviorSubject([]),
|
||||
navLinks$: params.navLinks$ ?? new BehaviorSubject([]),
|
||||
onProjectNavigationChange: params.onProjectNavigationChange ?? (() => undefined),
|
||||
activeNodes$: new BehaviorSubject([]),
|
||||
activeNodes$: params.activeNodes$ ?? new BehaviorSubject([]),
|
||||
cloudLinks: {
|
||||
billingAndSub: {
|
||||
title: 'Billing & Subscriptions',
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* 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 { css } from '@emotion/react';
|
||||
|
||||
export const navigationStyles = {
|
||||
euiSideNavItems: css`
|
||||
padding-left: 45px;
|
||||
`,
|
||||
};
|
|
@ -14,7 +14,6 @@ Array [
|
|||
"group1",
|
||||
"item1",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Item 1",
|
||||
},
|
||||
Object {
|
||||
|
@ -33,7 +32,6 @@ Array [
|
|||
"group1",
|
||||
"item2",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Title from deeplink!",
|
||||
},
|
||||
Object {
|
||||
|
@ -52,7 +50,6 @@ Array [
|
|||
"group1",
|
||||
"item3",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink title overriden",
|
||||
},
|
||||
],
|
||||
|
@ -69,77 +66,58 @@ Array [
|
|||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/discover",
|
||||
"id": "discover",
|
||||
"title": "Deeplink discover",
|
||||
"url": "/mocked/discover",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "discover",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:analytics",
|
||||
"root",
|
||||
"discover",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink discover",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/dashboards",
|
||||
"id": "dashboards",
|
||||
"title": "Deeplink dashboards",
|
||||
"url": "/mocked/dashboards",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "dashboards",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:analytics",
|
||||
"root",
|
||||
"dashboards",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink dashboards",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/visualize",
|
||||
"id": "visualize",
|
||||
"title": "Deeplink visualize",
|
||||
"url": "/mocked/visualize",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "visualize",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:analytics",
|
||||
"root",
|
||||
"visualize",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink visualize",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/discover",
|
||||
"id": "discover",
|
||||
"title": "Deeplink discover",
|
||||
"url": "/mocked/discover",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "root",
|
||||
"id": "discover",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:analytics",
|
||||
"root",
|
||||
"discover",
|
||||
],
|
||||
"title": "",
|
||||
"title": "Deeplink discover",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/dashboards",
|
||||
"id": "dashboards",
|
||||
"title": "Deeplink dashboards",
|
||||
"url": "/mocked/dashboards",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "dashboards",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:analytics",
|
||||
"dashboards",
|
||||
],
|
||||
"title": "Deeplink dashboards",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/visualize",
|
||||
"id": "visualize",
|
||||
"title": "Deeplink visualize",
|
||||
"url": "/mocked/visualize",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "visualize",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:analytics",
|
||||
"visualize",
|
||||
],
|
||||
"title": "Deeplink visualize",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
|
@ -156,57 +134,40 @@ Array [
|
|||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:overview",
|
||||
"id": "ml:overview",
|
||||
"title": "Deeplink ml:overview",
|
||||
"url": "/mocked/ml:overview",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "ml:overview",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"root",
|
||||
"ml:overview",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:overview",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:notifications",
|
||||
"id": "ml:notifications",
|
||||
"title": "Deeplink ml:notifications",
|
||||
"url": "/mocked/ml:notifications",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "ml:notifications",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"root",
|
||||
"ml:notifications",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:notifications",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:overview",
|
||||
"id": "ml:overview",
|
||||
"title": "Deeplink ml:overview",
|
||||
"url": "/mocked/ml:overview",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "root",
|
||||
"id": "ml:overview",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"root",
|
||||
"ml:overview",
|
||||
],
|
||||
"title": "",
|
||||
"title": "Deeplink ml:overview",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:notifications",
|
||||
"id": "ml:notifications",
|
||||
"title": "Deeplink ml:notifications",
|
||||
"url": "/mocked/ml:notifications",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "ml:notifications",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"ml:notifications",
|
||||
],
|
||||
"title": "Deeplink ml:notifications",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
|
@ -227,7 +188,6 @@ Array [
|
|||
"anomaly_detection",
|
||||
"ml:anomalyDetection",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Jobs",
|
||||
},
|
||||
Object {
|
||||
|
@ -247,7 +207,6 @@ Array [
|
|||
"anomaly_detection",
|
||||
"ml:anomalyExplorer",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:anomalyExplorer",
|
||||
},
|
||||
Object {
|
||||
|
@ -267,7 +226,6 @@ Array [
|
|||
"anomaly_detection",
|
||||
"ml:singleMetricViewer",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:singleMetricViewer",
|
||||
},
|
||||
Object {
|
||||
|
@ -287,7 +245,6 @@ Array [
|
|||
"anomaly_detection",
|
||||
"ml:settings",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:settings",
|
||||
},
|
||||
],
|
||||
|
@ -320,7 +277,6 @@ Array [
|
|||
"data_frame_analytics",
|
||||
"ml:dataFrameAnalytics",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Jobs",
|
||||
},
|
||||
Object {
|
||||
|
@ -340,7 +296,6 @@ Array [
|
|||
"data_frame_analytics",
|
||||
"ml:resultExplorer",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:resultExplorer",
|
||||
},
|
||||
Object {
|
||||
|
@ -360,7 +315,6 @@ Array [
|
|||
"data_frame_analytics",
|
||||
"ml:analyticsMap",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:analyticsMap",
|
||||
},
|
||||
],
|
||||
|
@ -393,7 +347,6 @@ Array [
|
|||
"model_management",
|
||||
"ml:nodesOverview",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:nodesOverview",
|
||||
},
|
||||
Object {
|
||||
|
@ -413,7 +366,6 @@ Array [
|
|||
"model_management",
|
||||
"ml:nodes",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:nodes",
|
||||
},
|
||||
],
|
||||
|
@ -446,7 +398,6 @@ Array [
|
|||
"data_visualizer",
|
||||
"ml:fileUpload",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "File",
|
||||
},
|
||||
Object {
|
||||
|
@ -466,7 +417,6 @@ Array [
|
|||
"data_visualizer",
|
||||
"ml:indexDataVisualizer",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Data view",
|
||||
},
|
||||
Object {
|
||||
|
@ -486,7 +436,6 @@ Array [
|
|||
"data_visualizer",
|
||||
"ml:dataDrift",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Data drift",
|
||||
},
|
||||
],
|
||||
|
@ -519,7 +468,6 @@ Array [
|
|||
"aiops_labs",
|
||||
"ml:logRateAnalysis",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:logRateAnalysis",
|
||||
},
|
||||
Object {
|
||||
|
@ -539,7 +487,6 @@ Array [
|
|||
"aiops_labs",
|
||||
"ml:logPatternAnalysis",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:logPatternAnalysis",
|
||||
},
|
||||
Object {
|
||||
|
@ -559,7 +506,6 @@ Array [
|
|||
"aiops_labs",
|
||||
"ml:changePointDetections",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:changePointDetections",
|
||||
},
|
||||
],
|
||||
|
@ -608,65 +554,46 @@ Array [
|
|||
"breadcrumbStatus": "hidden",
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/management",
|
||||
"id": "management",
|
||||
"title": "Deeplink management",
|
||||
"url": "/mocked/management",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "management",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"project_settings_project_nav",
|
||||
"settings",
|
||||
"management",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Management",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": undefined,
|
||||
"href": "https://cloud.elastic.co/deployments/123456789/security/users",
|
||||
"id": "cloudLinkUserAndRoles",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"project_settings_project_nav",
|
||||
"settings",
|
||||
"cloudLinkUserAndRoles",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Mock Users & Roles",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": undefined,
|
||||
"href": "https://cloud.elastic.co/account/billing",
|
||||
"id": "cloudLinkBilling",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"project_settings_project_nav",
|
||||
"settings",
|
||||
"cloudLinkBilling",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Mock Billing & Subscriptions",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/management",
|
||||
"id": "management",
|
||||
"title": "Deeplink management",
|
||||
"url": "/mocked/management",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "settings",
|
||||
"id": "management",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"project_settings_project_nav",
|
||||
"settings",
|
||||
"management",
|
||||
],
|
||||
"title": "",
|
||||
"title": "Management",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": undefined,
|
||||
"href": "https://cloud.elastic.co/deployments/123456789/security/users",
|
||||
"id": "cloudLinkUserAndRoles",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"project_settings_project_nav",
|
||||
"cloudLinkUserAndRoles",
|
||||
],
|
||||
"title": "Mock Users & Roles",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": undefined,
|
||||
"href": "https://cloud.elastic.co/account/billing",
|
||||
"id": "cloudLinkBilling",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"project_settings_project_nav",
|
||||
"cloudLinkBilling",
|
||||
],
|
||||
"title": "Mock Billing & Subscriptions",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiTitle,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import type { NavigateToUrlFn } from '../../../types/internal';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
href: string;
|
||||
navigateToUrl: NavigateToUrlFn;
|
||||
iconType?: string;
|
||||
}
|
||||
|
||||
export const GroupAsLink = ({ title, href, navigateToUrl, iconType }: Props) => {
|
||||
const groupID = useGeneratedHtmlId();
|
||||
const titleID = `${groupID}__title`;
|
||||
const TitleElement = 'h3';
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="m" alignItems="center" responsive={false}>
|
||||
{iconType && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type={iconType} size="m" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
<EuiFlexItem>
|
||||
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
|
||||
<EuiLink
|
||||
color="text"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
navigateToUrl(href);
|
||||
}}
|
||||
href={href}
|
||||
>
|
||||
<EuiTitle size="xxs">
|
||||
<TitleElement id={titleID} className="euiCollapsibleNavGroup__title">
|
||||
{title}
|
||||
</TitleElement>
|
||||
</EuiTitle>
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -40,12 +40,12 @@ describe('<Navigation />', () => {
|
|||
const { findByTestId } = render(
|
||||
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
|
||||
<Navigation>
|
||||
<Navigation.Group id="group1">
|
||||
<Navigation.Group id="group1" defaultIsCollapsed={false}>
|
||||
<Navigation.Item id="item1" title="Item 1" href="https://foo" />
|
||||
<Navigation.Item id="item2" title="Item 2" href="https://foo" />
|
||||
<Navigation.Group id="group1A" title="Group1A">
|
||||
<Navigation.Group id="group1A" title="Group1A" defaultIsCollapsed={false}>
|
||||
<Navigation.Item id="item1" title="Group 1A Item 1" href="https://foo" />
|
||||
<Navigation.Group id="group1A_1" title="Group1A_1">
|
||||
<Navigation.Group id="group1A_1" title="Group1A_1" defaultIsCollapsed={false}>
|
||||
<Navigation.Item id="item1" title="Group 1A_1 Item 1" href="https://foo" />
|
||||
</Navigation.Group>
|
||||
</Navigation.Group>
|
||||
|
@ -62,10 +62,10 @@ describe('<Navigation />', () => {
|
|||
expect(await findByTestId(/nav-item-group1.item2/)).toBeVisible();
|
||||
expect(await findByTestId(/nav-item-group1.group1A\s/)).toBeVisible();
|
||||
expect(await findByTestId(/nav-item-group1.group1A.item1/)).toBeVisible();
|
||||
expect(await findByTestId(/nav-item-group1.group1A.group1A_1/)).toBeVisible();
|
||||
expect(await findByTestId(/nav-item-group1.group1A.group1A_1\s/)).toBeVisible();
|
||||
|
||||
// Click the last group to expand and show the last depth
|
||||
(await findByTestId(/nav-item-group1.group1A.group1A_1/)).click();
|
||||
(await findByTestId(/nav-item-group1.group1A.group1A_1\s/)).click();
|
||||
|
||||
expect(await findByTestId(/nav-item-group1.group1A.group1A_1.item1/)).toBeVisible();
|
||||
|
||||
|
@ -76,56 +76,56 @@ describe('<Navigation />', () => {
|
|||
|
||||
expect(navTree.navigationTree).toEqual([
|
||||
{
|
||||
id: 'group1',
|
||||
path: ['group1'],
|
||||
title: '',
|
||||
isActive: false,
|
||||
children: [
|
||||
{
|
||||
id: 'item1',
|
||||
title: 'Item 1',
|
||||
href: 'https://foo',
|
||||
id: 'item1',
|
||||
isActive: false,
|
||||
path: ['group1', 'item1'],
|
||||
title: 'Item 1',
|
||||
},
|
||||
{
|
||||
id: 'item2',
|
||||
title: 'Item 2',
|
||||
href: 'https://foo',
|
||||
id: 'item2',
|
||||
isActive: false,
|
||||
path: ['group1', 'item2'],
|
||||
title: 'Item 2',
|
||||
},
|
||||
{
|
||||
id: 'group1A',
|
||||
title: 'Group1A',
|
||||
isActive: false,
|
||||
path: ['group1', 'group1A'],
|
||||
children: [
|
||||
{
|
||||
id: 'item1',
|
||||
href: 'https://foo',
|
||||
title: 'Group 1A Item 1',
|
||||
id: 'item1',
|
||||
isActive: false,
|
||||
path: ['group1', 'group1A', 'item1'],
|
||||
title: 'Group 1A Item 1',
|
||||
},
|
||||
{
|
||||
id: 'group1A_1',
|
||||
title: 'Group1A_1',
|
||||
isActive: false,
|
||||
path: ['group1', 'group1A', 'group1A_1'],
|
||||
children: [
|
||||
{
|
||||
id: 'item1',
|
||||
title: 'Group 1A_1 Item 1',
|
||||
isActive: false,
|
||||
href: 'https://foo',
|
||||
id: 'item1',
|
||||
isActive: false,
|
||||
path: ['group1', 'group1A', 'group1A_1', 'item1'],
|
||||
title: 'Group 1A_1 Item 1',
|
||||
},
|
||||
],
|
||||
id: 'group1A_1',
|
||||
isActive: true,
|
||||
path: ['group1', 'group1A', 'group1A_1'],
|
||||
title: 'Group1A_1',
|
||||
},
|
||||
],
|
||||
id: 'group1A',
|
||||
isActive: true,
|
||||
path: ['group1', 'group1A'],
|
||||
title: 'Group1A',
|
||||
},
|
||||
],
|
||||
id: 'group1',
|
||||
isActive: true,
|
||||
path: ['group1'],
|
||||
title: '',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -250,8 +250,8 @@ describe('<Navigation />', () => {
|
|||
onProjectNavigationChange={onProjectNavigationChange}
|
||||
>
|
||||
<Navigation>
|
||||
<Navigation.Group id="root">
|
||||
<Navigation.Group id="group1">
|
||||
<Navigation.Group id="root" defaultIsCollapsed={false}>
|
||||
<Navigation.Group id="group1" defaultIsCollapsed={false}>
|
||||
{/* Title from deeplink */}
|
||||
<Navigation.Item<any> id="item1" link="item1" />
|
||||
{/* Should not appear */}
|
||||
|
@ -279,13 +279,13 @@ describe('<Navigation />', () => {
|
|||
id: 'root',
|
||||
path: ['root'],
|
||||
title: '',
|
||||
isActive: false,
|
||||
isActive: true,
|
||||
children: [
|
||||
{
|
||||
id: 'group1',
|
||||
path: ['root', 'group1'],
|
||||
title: '',
|
||||
isActive: false,
|
||||
isActive: true,
|
||||
children: [
|
||||
{
|
||||
id: 'item1',
|
||||
|
@ -326,11 +326,11 @@ describe('<Navigation />', () => {
|
|||
onProjectNavigationChange={onProjectNavigationChange}
|
||||
>
|
||||
<Navigation>
|
||||
<Navigation.Group id="root">
|
||||
<Navigation.Group id="group1">
|
||||
<Navigation.Group id="root" defaultIsCollapsed={false}>
|
||||
<Navigation.Group id="group1" defaultIsCollapsed={false}>
|
||||
<Navigation.Item<any> id="item1" link="notRegistered" />
|
||||
</Navigation.Group>
|
||||
<Navigation.Group id="group2">
|
||||
<Navigation.Group id="group2" defaultIsCollapsed={false}>
|
||||
<Navigation.Item<any> id="item1" link="item1" />
|
||||
</Navigation.Group>
|
||||
</Navigation.Group>
|
||||
|
@ -352,128 +352,39 @@ describe('<Navigation />', () => {
|
|||
|
||||
expect(navTree.navigationTree).toEqual([
|
||||
{
|
||||
id: 'root',
|
||||
path: ['root'],
|
||||
title: '',
|
||||
isActive: false,
|
||||
children: [
|
||||
{
|
||||
id: 'group1',
|
||||
isActive: true,
|
||||
path: ['root', 'group1'],
|
||||
title: '',
|
||||
isActive: false,
|
||||
},
|
||||
{
|
||||
id: 'group2',
|
||||
path: ['root', 'group2'],
|
||||
title: '',
|
||||
isActive: false,
|
||||
children: [
|
||||
{
|
||||
deepLink: {
|
||||
baseUrl: '',
|
||||
href: '',
|
||||
id: 'item1',
|
||||
title: 'Title from deeplink',
|
||||
url: '',
|
||||
},
|
||||
id: 'item1',
|
||||
isActive: false,
|
||||
path: ['root', 'group2', 'item1'],
|
||||
title: 'Title from deeplink',
|
||||
isActive: false,
|
||||
deepLink: {
|
||||
id: 'item1',
|
||||
title: 'Title from deeplink',
|
||||
baseUrl: '',
|
||||
url: '',
|
||||
href: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
id: 'group2',
|
||||
isActive: true,
|
||||
path: ['root', 'group2'],
|
||||
title: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should render custom react element', async () => {
|
||||
const navLinks$: Observable<ChromeNavLink[]> = of([
|
||||
{
|
||||
id: 'item1',
|
||||
title: 'Title from deeplink',
|
||||
baseUrl: '',
|
||||
url: '',
|
||||
href: '',
|
||||
},
|
||||
]);
|
||||
|
||||
const onProjectNavigationChange = jest.fn();
|
||||
|
||||
const { findByTestId } = render(
|
||||
<NavigationProvider
|
||||
{...services}
|
||||
navLinks$={navLinks$}
|
||||
onProjectNavigationChange={onProjectNavigationChange}
|
||||
>
|
||||
<Navigation>
|
||||
<Navigation.Group id="root">
|
||||
<Navigation.Group id="group1">
|
||||
<Navigation.Item<any> link="item1">
|
||||
<div data-test-subj="my-custom-element">Custom element</div>
|
||||
</Navigation.Item>
|
||||
<Navigation.Item id="item2" title="Children prop" href="http://foo">
|
||||
{(navNode) => <div data-test-subj="my-other-custom-element">{navNode.title}</div>}
|
||||
</Navigation.Item>
|
||||
</Navigation.Group>
|
||||
</Navigation.Group>
|
||||
</Navigation>
|
||||
</NavigationProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(SET_NAVIGATION_DELAY);
|
||||
});
|
||||
|
||||
expect(await findByTestId('my-custom-element')).toBeVisible();
|
||||
expect(await findByTestId('my-other-custom-element')).toBeVisible();
|
||||
expect((await findByTestId('my-other-custom-element')).textContent).toBe('Children prop');
|
||||
|
||||
expect(onProjectNavigationChange).toHaveBeenCalled();
|
||||
const lastCall =
|
||||
onProjectNavigationChange.mock.calls[onProjectNavigationChange.mock.calls.length - 1];
|
||||
const [navTree] = lastCall;
|
||||
|
||||
expect(navTree.navigationTree).toEqual([
|
||||
{
|
||||
id: 'root',
|
||||
isActive: true,
|
||||
path: ['root'],
|
||||
title: '',
|
||||
isActive: false,
|
||||
children: [
|
||||
{
|
||||
id: 'group1',
|
||||
path: ['root', 'group1'],
|
||||
title: '',
|
||||
isActive: false,
|
||||
children: [
|
||||
{
|
||||
id: 'item1',
|
||||
path: ['root', 'group1', 'item1'],
|
||||
title: 'Title from deeplink',
|
||||
renderItem: expect.any(Function),
|
||||
isActive: false,
|
||||
deepLink: {
|
||||
id: 'item1',
|
||||
title: 'Title from deeplink',
|
||||
baseUrl: '',
|
||||
url: '',
|
||||
href: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'item2',
|
||||
href: 'http://foo',
|
||||
path: ['root', 'group1', 'item2'],
|
||||
title: 'Children prop',
|
||||
isActive: false,
|
||||
renderItem: expect.any(Function),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -649,11 +560,11 @@ describe('<Navigation />', () => {
|
|||
</NavigationProvider>
|
||||
);
|
||||
|
||||
expect(await findByTestId(/nav-item-group1.item1/)).toHaveClass(
|
||||
'euiSideNavItemButton-isSelected'
|
||||
expect((await findByTestId(/nav-item-group1.item1/)).dataset.testSubj).toMatch(
|
||||
/nav-item-isActive/
|
||||
);
|
||||
expect(await findByTestId(/nav-item-group1.item2/)).not.toHaveClass(
|
||||
'euiSideNavItemButton-isSelected'
|
||||
expect((await findByTestId(/nav-item-group1.item2/)).dataset.testSubj).not.toMatch(
|
||||
/nav-item-isActive/
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
@ -673,11 +584,11 @@ describe('<Navigation />', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
expect(await findByTestId(/nav-item-group1.item1/)).not.toHaveClass(
|
||||
'euiSideNavItemButton-isSelected'
|
||||
expect((await findByTestId(/nav-item-group1.item1/)).dataset.testSubj).not.toMatch(
|
||||
/nav-item-isActive/
|
||||
);
|
||||
expect(await findByTestId(/nav-item-group1.item2/)).toHaveClass(
|
||||
'euiSideNavItemButton-isSelected'
|
||||
expect((await findByTestId(/nav-item-group1.item2/)).dataset.testSubj).toMatch(
|
||||
/nav-item-isActive/
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -730,8 +641,8 @@ describe('<Navigation />', () => {
|
|||
|
||||
jest.advanceTimersByTime(SET_NAVIGATION_DELAY);
|
||||
|
||||
expect(await findByTestId(/nav-item-group1.item1/)).toHaveClass(
|
||||
'euiSideNavItemButton-isSelected'
|
||||
expect((await findByTestId(/nav-item-group1.item1/)).dataset.testSubj).toMatch(
|
||||
/nav-item-isActive/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -743,7 +654,7 @@ describe('<Navigation />', () => {
|
|||
const { findByTestId } = render(
|
||||
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
|
||||
<Navigation>
|
||||
<Navigation.Group id="group1">
|
||||
<Navigation.Group id="group1" defaultIsCollapsed={false}>
|
||||
<Navigation.Item id="cloudLink1" cloudLink="userAndRoles" />
|
||||
<Navigation.Item id="cloudLink2" cloudLink="performance" />
|
||||
<Navigation.Item id="cloudLink3" cloudLink="billingAndSub" />
|
||||
|
@ -756,13 +667,13 @@ describe('<Navigation />', () => {
|
|||
expect(await findByTestId(/nav-item-group1.cloudLink2/)).toBeVisible();
|
||||
expect(await findByTestId(/nav-item-group1.cloudLink3/)).toBeVisible();
|
||||
|
||||
expect(await (await findByTestId(/nav-item-group1.cloudLink1/)).textContent).toBe(
|
||||
expect((await findByTestId(/nav-item-group1.cloudLink1/)).textContent).toBe(
|
||||
'Mock Users & RolesExternal link'
|
||||
);
|
||||
expect(await (await findByTestId(/nav-item-group1.cloudLink2/)).textContent).toBe(
|
||||
expect((await findByTestId(/nav-item-group1.cloudLink2/)).textContent).toBe(
|
||||
'Mock PerformanceExternal link'
|
||||
);
|
||||
expect(await (await findByTestId(/nav-item-group1.cloudLink3/)).textContent).toBe(
|
||||
expect((await findByTestId(/nav-item-group1.cloudLink3/)).textContent).toBe(
|
||||
'Mock Billing & SubscriptionsExternal link'
|
||||
);
|
||||
});
|
||||
|
|
|
@ -85,7 +85,7 @@ function NavigationGroupInternalComp<
|
|||
)}
|
||||
{/* We render the children so they mount and can register themselves but
|
||||
visually they don't appear here in the DOM. They are rendered inside the
|
||||
<EuiSideNav /> "items" prop (see <NavigationSectionUI />) */}
|
||||
<EuiCollapsibleNavItem /> "items" prop (see <NavigationSectionUI />) */}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { Fragment, ReactElement, ReactNode, useEffect, useMemo } from 'react';
|
||||
import React, { Fragment, useEffect, useMemo } from 'react';
|
||||
|
||||
import type { AppDeepLinkId } from '@kbn/core-chrome-browser';
|
||||
import type { AppDeepLinkId, ChromeProjectNavigationNode } from '@kbn/core-chrome-browser';
|
||||
import { useNavigation as useNavigationServices } from '../../services';
|
||||
import type { ChromeProjectNavigationNodeEnhanced, NodeProps } from '../types';
|
||||
import { useInitNavNode } from '../hooks';
|
||||
import type { NodeProps } from '../types';
|
||||
import { useNavigation } from './navigation';
|
||||
|
||||
export interface Props<
|
||||
|
@ -22,10 +22,6 @@ export interface Props<
|
|||
unstyled?: boolean;
|
||||
}
|
||||
|
||||
function isReactElement(element: ReactNode): element is ReactElement {
|
||||
return React.isValidElement(element);
|
||||
}
|
||||
|
||||
function NavigationItemComp<
|
||||
LinkId extends AppDeepLinkId = AppDeepLinkId,
|
||||
Id extends string = string,
|
||||
|
@ -33,7 +29,7 @@ function NavigationItemComp<
|
|||
>(props: Props<LinkId, Id, ChildrenId>) {
|
||||
const { cloudLinks } = useNavigationServices();
|
||||
const navigationContext = useNavigation();
|
||||
const navNodeRef = React.useRef<ChromeProjectNavigationNodeEnhanced | null>(null);
|
||||
const navNodeRef = React.useRef<ChromeProjectNavigationNode | null>(null);
|
||||
|
||||
const { children, node } = useMemo(() => {
|
||||
const { children: _children, ...rest } = props;
|
||||
|
@ -44,14 +40,7 @@ function NavigationItemComp<
|
|||
}, [props]);
|
||||
const unstyled = props.unstyled ?? navigationContext.unstyled;
|
||||
|
||||
let renderItem: (() => ReactElement) | undefined;
|
||||
|
||||
if (!unstyled && children && (typeof children === 'function' || isReactElement(children))) {
|
||||
renderItem =
|
||||
typeof children === 'function' ? () => children(navNodeRef.current) : () => children;
|
||||
}
|
||||
|
||||
const { navNode } = useInitNavNode({ ...node, children, renderItem }, { cloudLinks });
|
||||
const { navNode } = useInitNavNode({ ...node, children }, { cloudLinks });
|
||||
|
||||
useEffect(() => {
|
||||
navNodeRef.current = navNode;
|
||||
|
|
|
@ -7,28 +7,22 @@
|
|||
*/
|
||||
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
EuiCollapsibleNavGroup,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiSideNav,
|
||||
EuiSideNavItemType,
|
||||
EuiText,
|
||||
EuiCollapsibleNavItem,
|
||||
EuiCollapsibleNavItemProps,
|
||||
EuiCollapsibleNavSubItemGroupTitle,
|
||||
} from '@elastic/eui';
|
||||
import { ChromeProjectNavigationNode } from '@kbn/core-chrome-browser';
|
||||
import classnames from 'classnames';
|
||||
import type { BasePathService, NavigateToUrlFn } from '../../../types/internal';
|
||||
import { navigationStyles as styles } from '../../styles';
|
||||
import { useNavigation as useServices } from '../../services';
|
||||
import { ChromeProjectNavigationNodeEnhanced } from '../types';
|
||||
import { isAbsoluteLink } from '../../utils';
|
||||
import { GroupAsLink } from './group_as_link';
|
||||
|
||||
type RenderItem = EuiSideNavItemType<unknown>['renderItem'];
|
||||
|
||||
const navigationNodeToEuiItem = (
|
||||
item: ChromeProjectNavigationNodeEnhanced,
|
||||
item: ChromeProjectNavigationNode,
|
||||
{ navigateToUrl, basePath }: { navigateToUrl: NavigateToUrlFn; basePath: BasePathService }
|
||||
): EuiSideNavItemType<unknown> => {
|
||||
): EuiCollapsibleNavSubItemGroupTitle | EuiCollapsibleNavItemProps => {
|
||||
const href = item.deepLink?.url ?? item.href;
|
||||
const id = item.path ? item.path.join('.') : item.id;
|
||||
const isExternal = Boolean(href) && isAbsoluteLink(href!);
|
||||
|
@ -39,24 +33,16 @@ const navigationNodeToEuiItem = (
|
|||
[`nav-item-isActive`]: isSelected,
|
||||
});
|
||||
|
||||
const getRenderItem = (): RenderItem | undefined => {
|
||||
if (!isExternal || item.renderItem) {
|
||||
return item.renderItem;
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div className="euiSideNavItemButton" data-test-subj={dataTestSubj}>
|
||||
<EuiLink href={href} external color="text">
|
||||
{item.title}
|
||||
</EuiLink>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
id,
|
||||
name: item.title,
|
||||
isGroupTitle: item.isGroupTitle,
|
||||
title: item.title,
|
||||
isSelected,
|
||||
accordionProps: {
|
||||
...item.accordionProps,
|
||||
initialIsOpen: true, // FIXME open state is controlled on component mount
|
||||
},
|
||||
linkProps: { external: isExternal },
|
||||
onClick:
|
||||
href !== undefined
|
||||
? (event: React.MouseEvent) => {
|
||||
|
@ -65,20 +51,18 @@ const navigationNodeToEuiItem = (
|
|||
}
|
||||
: undefined,
|
||||
href,
|
||||
renderItem: getRenderItem(),
|
||||
items: item.children?.map((_item) =>
|
||||
navigationNodeToEuiItem(_item, { navigateToUrl, basePath })
|
||||
),
|
||||
['data-test-subj']: dataTestSubj,
|
||||
...(item.icon && {
|
||||
icon: <EuiIcon type={item.icon} size="s" />,
|
||||
}),
|
||||
icon: item.icon,
|
||||
iconProps: { size: 's' },
|
||||
};
|
||||
};
|
||||
|
||||
interface Props {
|
||||
navNode: ChromeProjectNavigationNodeEnhanced;
|
||||
items?: ChromeProjectNavigationNodeEnhanced[];
|
||||
navNode: ChromeProjectNavigationNode;
|
||||
items?: ChromeProjectNavigationNode[];
|
||||
}
|
||||
|
||||
export const NavigationSectionUI: FC<Props> = ({ navNode, items = [] }) => {
|
||||
|
@ -90,8 +74,12 @@ export const NavigationSectionUI: FC<Props> = ({ navNode, items = [] }) => {
|
|||
const [doCollapseFromActiveState, setDoCollapseFromActiveState] = useState(true);
|
||||
|
||||
// If the item has no link and no cildren, we don't want to render it
|
||||
const itemHasLinkOrChildren = (item: ChromeProjectNavigationNodeEnhanced) => {
|
||||
const itemHasLinkOrChildren = (item: ChromeProjectNavigationNode) => {
|
||||
const isGroupTitle = Boolean(item.isGroupTitle);
|
||||
const hasLink = Boolean(item.deepLink) || Boolean(item.href);
|
||||
if (isGroupTitle) {
|
||||
return true;
|
||||
}
|
||||
if (hasLink) {
|
||||
return true;
|
||||
}
|
||||
|
@ -128,49 +116,39 @@ export const NavigationSectionUI: FC<Props> = ({ navNode, items = [] }) => {
|
|||
return null;
|
||||
}
|
||||
|
||||
const propsForGroupAsLink = groupIsLink
|
||||
const propsForGroupAsLink: Partial<EuiCollapsibleNavItemProps> = groupIsLink
|
||||
? {
|
||||
buttonElement: 'div' as const,
|
||||
// If we don't force the state there is a little UI animation as if the
|
||||
// accordion was openin/closing. We don't want any animation when it is a link.
|
||||
forceState: 'closed' as const,
|
||||
buttonContent: (
|
||||
<GroupAsLink
|
||||
title={title}
|
||||
iconType={icon}
|
||||
href={groupHref}
|
||||
navigateToUrl={navigateToUrl}
|
||||
/>
|
||||
),
|
||||
arrowProps: { style: { display: 'none' } },
|
||||
linkProps: {
|
||||
href: groupHref,
|
||||
onClick: (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
navigateToUrl(groupHref);
|
||||
},
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<EuiCollapsibleNavGroup
|
||||
<EuiCollapsibleNavItem
|
||||
id={id}
|
||||
title={title}
|
||||
iconType={icon}
|
||||
iconSize="m"
|
||||
isCollapsible
|
||||
initialIsOpen={isActive}
|
||||
onToggle={(isOpen) => {
|
||||
setIsCollapsed(!isOpen);
|
||||
setDoCollapseFromActiveState(false);
|
||||
icon={icon}
|
||||
iconProps={{ size: 'm' }}
|
||||
accordionProps={{
|
||||
initialIsOpen: isActive,
|
||||
forceState: isCollapsed ? 'closed' : 'open',
|
||||
onToggle: (isOpen) => {
|
||||
setIsCollapsed(!isOpen);
|
||||
setDoCollapseFromActiveState(false);
|
||||
},
|
||||
...navNode.accordionProps,
|
||||
}}
|
||||
forceState={isCollapsed ? 'closed' : 'open'}
|
||||
data-test-subj={`nav-bucket-${id}`}
|
||||
{...propsForGroupAsLink}
|
||||
>
|
||||
<EuiText color="default">
|
||||
<EuiSideNav
|
||||
mobileBreakpoints={/* turn off responsive behavior */ []}
|
||||
items={filteredItems.map((item) =>
|
||||
navigationNodeToEuiItem(item, { navigateToUrl, basePath })
|
||||
)}
|
||||
css={styles.euiSideNavItems}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiCollapsibleNavGroup>
|
||||
items={filteredItems.map((item) =>
|
||||
navigationNodeToEuiItem(item, { navigateToUrl, basePath })
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiFlyoutBody, EuiFlyoutFooter } from '@elastic/eui';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
interface Props {
|
||||
|
@ -21,17 +21,12 @@ export const NavigationUI: FC<Props> = ({ children, unstyled, footerChildren, da
|
|||
{unstyled ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
style={{ overflowY: 'auto' }}
|
||||
justifyContent="spaceBetween"
|
||||
data-test-subj={dataTestSubj}
|
||||
>
|
||||
<EuiFlexItem grow={false}>{children}</EuiFlexItem>
|
||||
|
||||
{footerChildren && <EuiFlexItem grow={false}>{footerChildren}</EuiFlexItem>}
|
||||
</EuiFlexGroup>
|
||||
<>
|
||||
<EuiFlyoutBody scrollableTabIndex={-1} data-test-subj={dataTestSubj}>
|
||||
{children}
|
||||
</EuiFlyoutBody>
|
||||
{footerChildren && <EuiFlyoutFooter>{footerChildren}</EuiFlyoutFooter>}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -6,14 +6,13 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiCollapsibleNavGroup, EuiSideNav, EuiSideNavItemType } from '@elastic/eui';
|
||||
import { EuiCollapsibleNavItem } from '@elastic/eui';
|
||||
import React, { FC } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import type { Observable } from 'rxjs';
|
||||
|
||||
import { RecentItem } from '../../../types/internal';
|
||||
import { useNavigation as useServices } from '../../services';
|
||||
import { navigationStyles as styles } from '../../styles';
|
||||
|
||||
import { getI18nStrings } from '../i18n_strings';
|
||||
|
||||
|
@ -42,40 +41,31 @@ export const RecentlyAccessed: FC<Props> = ({
|
|||
return null;
|
||||
}
|
||||
|
||||
const navItems: Array<EuiSideNavItemType<unknown>> = [
|
||||
{
|
||||
name: '', // no list header title
|
||||
id: 'recents_root',
|
||||
items: recentlyAccessed.map((recent) => {
|
||||
const { id, label, link } = recent;
|
||||
const href = basePath.prepend(link);
|
||||
const navItems = recentlyAccessed.map((recent) => {
|
||||
const { id, label, link } = recent;
|
||||
const href = basePath.prepend(link);
|
||||
|
||||
return {
|
||||
id,
|
||||
name: label,
|
||||
href,
|
||||
onClick: (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
navigateToUrl(href);
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
];
|
||||
return {
|
||||
id,
|
||||
title: label,
|
||||
href,
|
||||
onClick: (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
navigateToUrl(href);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiCollapsibleNavGroup
|
||||
<EuiCollapsibleNavItem
|
||||
title={strings.recentlyAccessed}
|
||||
iconType="clock"
|
||||
isCollapsible={true}
|
||||
initialIsOpen={!defaultIsCollapsed}
|
||||
icon="clock"
|
||||
iconProps={{ size: 'm' }}
|
||||
accordionProps={{
|
||||
initialIsOpen: !defaultIsCollapsed,
|
||||
}}
|
||||
data-test-subj={`nav-bucket-recentlyAccessed`}
|
||||
>
|
||||
<EuiSideNav
|
||||
items={navItems}
|
||||
css={styles.euiSideNavItems}
|
||||
mobileBreakpoints={/* turn off responsive behavior */ []}
|
||||
/>
|
||||
</EuiCollapsibleNavGroup>
|
||||
items={navItems}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -80,7 +80,7 @@ describe('<DefaultNavigation />', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const { findByTestId } = render(
|
||||
const { findAllByTestId } = render(
|
||||
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
|
||||
<DefaultNavigation navigationTree={{ body: navigationBody }} />
|
||||
</NavigationProvider>
|
||||
|
@ -90,16 +90,8 @@ describe('<DefaultNavigation />', () => {
|
|||
jest.advanceTimersByTime(SET_NAVIGATION_DELAY);
|
||||
});
|
||||
|
||||
expect(await findByTestId(/nav-item-group1.item1/)).toBeVisible();
|
||||
expect(await findByTestId(/nav-item-group1.item2/)).toBeVisible();
|
||||
expect(await findByTestId(/nav-item-group1.group1A\s/)).toBeVisible();
|
||||
expect(await findByTestId(/nav-item-group1.group1A.item1/)).toBeVisible();
|
||||
expect(await findByTestId(/nav-item-group1.group1A.group1A_1/)).toBeVisible();
|
||||
|
||||
// Click the last group to expand and show the last depth
|
||||
(await findByTestId(/nav-item-group1.group1A.group1A_1/)).click();
|
||||
|
||||
expect(await findByTestId(/nav-item-group1.group1A.group1A_1.item1/)).toBeVisible();
|
||||
(await findAllByTestId(/nav-item-group1.group1A.group1A_1/))[0].click();
|
||||
|
||||
expect(onProjectNavigationChange).toHaveBeenCalled();
|
||||
const lastCall =
|
||||
|
@ -120,7 +112,6 @@ describe('<DefaultNavigation />', () => {
|
|||
"group1",
|
||||
"item1",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Item 1",
|
||||
},
|
||||
Object {
|
||||
|
@ -133,7 +124,6 @@ describe('<DefaultNavigation />', () => {
|
|||
"group1",
|
||||
"item2",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Item 2",
|
||||
},
|
||||
Object {
|
||||
|
@ -149,7 +139,6 @@ describe('<DefaultNavigation />', () => {
|
|||
"group1A",
|
||||
"item1",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Group 1A Item 1",
|
||||
},
|
||||
Object {
|
||||
|
@ -166,7 +155,6 @@ describe('<DefaultNavigation />', () => {
|
|||
"group1A_1",
|
||||
"item1",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Group 1A_1 Item 1",
|
||||
},
|
||||
],
|
||||
|
@ -291,7 +279,6 @@ describe('<DefaultNavigation />', () => {
|
|||
"group1",
|
||||
"item1",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Title from deeplink",
|
||||
},
|
||||
Object {
|
||||
|
@ -311,7 +298,6 @@ describe('<DefaultNavigation />', () => {
|
|||
"group1",
|
||||
"item2",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Overwrite deeplink title",
|
||||
},
|
||||
],
|
||||
|
@ -394,7 +380,6 @@ describe('<DefaultNavigation />', () => {
|
|||
"group1",
|
||||
"item1",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Absolute link",
|
||||
},
|
||||
],
|
||||
|
@ -556,11 +541,11 @@ describe('<DefaultNavigation />', () => {
|
|||
jest.advanceTimersByTime(SET_NAVIGATION_DELAY);
|
||||
});
|
||||
|
||||
expect(await findByTestId(/nav-item-group1.item1/)).toHaveClass(
|
||||
'euiSideNavItemButton-isSelected'
|
||||
expect((await findByTestId(/nav-item-group1.item1/)).dataset.testSubj).toMatch(
|
||||
/nav-item-isActive/
|
||||
);
|
||||
expect(await findByTestId(/nav-item-group1.item2/)).not.toHaveClass(
|
||||
'euiSideNavItemButton-isSelected'
|
||||
expect((await findByTestId(/nav-item-group1.item2/)).dataset.testSubj).not.toMatch(
|
||||
/nav-item-isActive/
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -619,8 +604,8 @@ describe('<DefaultNavigation />', () => {
|
|||
jest.advanceTimersByTime(SET_NAVIGATION_DELAY);
|
||||
});
|
||||
|
||||
expect(await findByTestId(/nav-item-group1.item1/)).toHaveClass(
|
||||
'euiSideNavItemButton-isSelected'
|
||||
expect((await findByTestId(/nav-item-group1.item1/)).dataset.testSubj).toMatch(
|
||||
/nav-item-isActive/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -703,17 +688,12 @@ describe('<DefaultNavigation />', () => {
|
|||
);
|
||||
|
||||
expect(
|
||||
await (
|
||||
await findByTestId(
|
||||
/nav-item-project_settings_project_nav.settings.cloudLinkUserAndRoles/
|
||||
)
|
||||
).textContent
|
||||
(await findByTestId(/nav-item-project_settings_project_nav.cloudLinkUserAndRoles/))
|
||||
.textContent
|
||||
).toBe('Mock Users & RolesExternal link');
|
||||
|
||||
expect(
|
||||
await (
|
||||
await findByTestId(/nav-item-project_settings_project_nav.settings.cloudLinkBilling/)
|
||||
).textContent
|
||||
(await findByTestId(/nav-item-project_settings_project_nav.cloudLinkBilling/)).textContent
|
||||
).toBe('Mock Billing & SubscriptionsExternal link');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -72,23 +72,18 @@ const getDefaultNavigationTree = (
|
|||
breadcrumbStatus: 'hidden',
|
||||
children: [
|
||||
{
|
||||
id: 'settings',
|
||||
children: [
|
||||
{
|
||||
link: 'management',
|
||||
title: i18n.translate('sharedUXPackages.chrome.sideNavigation.mngt', {
|
||||
defaultMessage: 'Management',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkUserAndRoles',
|
||||
cloudLink: 'userAndRoles',
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkBilling',
|
||||
cloudLink: 'billingAndSub',
|
||||
},
|
||||
],
|
||||
link: 'management',
|
||||
title: i18n.translate('sharedUXPackages.chrome.sideNavigation.mngt', {
|
||||
defaultMessage: 'Management',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkUserAndRoles',
|
||||
cloudLink: 'userAndRoles',
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkBilling',
|
||||
cloudLink: 'billingAndSub',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -19,13 +19,7 @@ import { CloudLinks } from '../../cloud_links';
|
|||
import { useNavigation as useNavigationServices } from '../../services';
|
||||
import { isAbsoluteLink } from '../../utils';
|
||||
import { useNavigation } from '../components/navigation';
|
||||
import {
|
||||
ChromeProjectNavigationNodeEnhanced,
|
||||
NodeProps,
|
||||
NodePropsEnhanced,
|
||||
RegisterFunction,
|
||||
UnRegisterFunction,
|
||||
} from '../types';
|
||||
import { NodeProps, NodePropsEnhanced, RegisterFunction, UnRegisterFunction } from '../types';
|
||||
import { useRegisterTreeNode } from './use_register_tree_node';
|
||||
|
||||
function getIdFromNavigationNode<
|
||||
|
@ -135,7 +129,7 @@ function createInternalNavNode<
|
|||
path: string[] | null,
|
||||
isActive: boolean,
|
||||
{ cloudLinks }: { cloudLinks: CloudLinks }
|
||||
): ChromeProjectNavigationNodeEnhanced | null {
|
||||
): ChromeProjectNavigationNode | null {
|
||||
validateNodeProps(_navNode);
|
||||
|
||||
const { children, link, cloudLink, ...navNode } = _navNode;
|
||||
|
@ -185,9 +179,9 @@ export const useInitNavNode = <
|
|||
/**
|
||||
* Map of children nodes
|
||||
*/
|
||||
const [childrenNodes, setChildrenNodes] = useState<
|
||||
Record<string, ChromeProjectNavigationNodeEnhanced>
|
||||
>({});
|
||||
const [childrenNodes, setChildrenNodes] = useState<Record<string, ChromeProjectNavigationNode>>(
|
||||
{}
|
||||
);
|
||||
|
||||
const isMounted = useRef(false);
|
||||
|
||||
|
|
|
@ -6,84 +6,61 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { of } from 'rxjs';
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { ChromeNavLink } from '@kbn/core-chrome-browser';
|
||||
import { useState } from '@storybook/addons';
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
import React, { EventHandler, FC, PropsWithChildren, MouseEvent } from 'react';
|
||||
import { BehaviorSubject, of } from 'rxjs';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiCollapsibleNav,
|
||||
EuiCollapsibleNavBeta,
|
||||
EuiCollapsibleNavBetaProps,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHeader,
|
||||
EuiHeaderSection,
|
||||
EuiLink,
|
||||
EuiPageTemplate,
|
||||
EuiText,
|
||||
EuiThemeProvider,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import type { ChromeNavLink, ChromeProjectNavigationNode } from '@kbn/core-chrome-browser';
|
||||
import { NavigationStorybookMock, navLinksMock } from '../../mocks';
|
||||
import mdx from '../../README.mdx';
|
||||
import { NavigationProvider } from '../services';
|
||||
import { DefaultNavigation } from './default_navigation';
|
||||
import type { NavigationServices } from '../../types';
|
||||
import { NavigationProvider } from '../services';
|
||||
import { Navigation } from './components';
|
||||
import type { NonEmptyArray, ProjectNavigationDefinition } from './types';
|
||||
import { DefaultNavigation } from './default_navigation';
|
||||
import { getPresets } from './nav_tree_presets';
|
||||
import type { GroupDefinition, NonEmptyArray, ProjectNavigationDefinition } from './types';
|
||||
|
||||
const storybookMock = new NavigationStorybookMock();
|
||||
|
||||
const SIZE_OPEN = 248;
|
||||
const SIZE_CLOSED = 40;
|
||||
|
||||
const NavigationWrapper: FC = ({ children }) => {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const collabsibleNavCSS = css`
|
||||
border-inline-end-width: 1,
|
||||
display: flex,
|
||||
flex-direction: row,
|
||||
`;
|
||||
|
||||
const CollapseButton = () => {
|
||||
const buttonCSS = css`
|
||||
margin-left: -32px;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
`;
|
||||
return (
|
||||
<span css={buttonCSS}>
|
||||
<EuiButtonIcon
|
||||
iconType={isOpen ? 'menuLeft' : 'menuRight'}
|
||||
color={isOpen ? 'ghost' : 'text'}
|
||||
onClick={toggleOpen}
|
||||
aria-label={isOpen ? 'Collapse navigation' : 'Expand navigation'}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const toggleOpen = useCallback(() => {
|
||||
setIsOpen(!isOpen);
|
||||
}, [isOpen, setIsOpen]);
|
||||
|
||||
const NavigationWrapper: FC<
|
||||
PropsWithChildren<{ clickAction?: EventHandler<MouseEvent>; clickActionText?: string }> &
|
||||
Partial<EuiCollapsibleNavBetaProps>
|
||||
> = (props) => {
|
||||
return (
|
||||
<EuiThemeProvider>
|
||||
<EuiCollapsibleNav
|
||||
css={collabsibleNavCSS}
|
||||
isOpen={true}
|
||||
showButtonIfDocked={true}
|
||||
onClose={toggleOpen}
|
||||
isDocked={true}
|
||||
size={isOpen ? SIZE_OPEN : SIZE_CLOSED}
|
||||
hideCloseButton={false}
|
||||
button={<CollapseButton />}
|
||||
>
|
||||
{isOpen && children}
|
||||
</EuiCollapsibleNav>
|
||||
</EuiThemeProvider>
|
||||
<>
|
||||
<EuiHeader position="fixed">
|
||||
<EuiHeaderSection side={props?.side}>
|
||||
<EuiCollapsibleNavBeta {...props} />
|
||||
</EuiHeaderSection>
|
||||
</EuiHeader>
|
||||
<EuiPageTemplate>
|
||||
<EuiPageTemplate.Section>
|
||||
{props.clickAction ? (
|
||||
<EuiButton color="text" onClick={props.clickAction}>
|
||||
{props.clickActionText ?? 'Click me'}
|
||||
</EuiButton>
|
||||
) : (
|
||||
<p>Hello world</p>
|
||||
)}
|
||||
</EuiPageTemplate.Section>
|
||||
</EuiPageTemplate>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -123,30 +100,25 @@ const simpleNavigationDefinition: ProjectNavigationDefinition = {
|
|||
defaultIsCollapsed: false,
|
||||
children: [
|
||||
{
|
||||
id: 'root',
|
||||
children: [
|
||||
{
|
||||
id: 'item1',
|
||||
title: 'Get started',
|
||||
},
|
||||
{
|
||||
id: 'item2',
|
||||
title: 'Alerts',
|
||||
},
|
||||
{
|
||||
id: 'item3',
|
||||
title: 'Dashboards',
|
||||
},
|
||||
{
|
||||
id: 'item4',
|
||||
title: 'External link',
|
||||
href: 'https://elastic.co',
|
||||
},
|
||||
{
|
||||
id: 'item5',
|
||||
title: 'Another link',
|
||||
},
|
||||
],
|
||||
id: 'item1',
|
||||
title: 'Get started',
|
||||
},
|
||||
{
|
||||
id: 'item2',
|
||||
title: 'Alerts',
|
||||
},
|
||||
{
|
||||
id: 'item3',
|
||||
title: 'Dashboards',
|
||||
},
|
||||
{
|
||||
id: 'item4',
|
||||
title: 'External link',
|
||||
href: 'https://elastic.co',
|
||||
},
|
||||
{
|
||||
id: 'item5',
|
||||
title: 'Another link',
|
||||
},
|
||||
{
|
||||
id: 'group:settings',
|
||||
|
@ -205,21 +177,16 @@ const navigationDefinition: ProjectNavigationDefinition = {
|
|||
defaultIsCollapsed: false,
|
||||
children: [
|
||||
{
|
||||
id: 'root',
|
||||
children: [
|
||||
{
|
||||
id: 'item1',
|
||||
title: 'Get started',
|
||||
},
|
||||
{
|
||||
id: 'item2',
|
||||
title: 'Alerts',
|
||||
},
|
||||
{
|
||||
id: 'item3',
|
||||
title: 'Some other node',
|
||||
},
|
||||
],
|
||||
id: 'item1',
|
||||
title: 'Get started',
|
||||
},
|
||||
{
|
||||
id: 'item2',
|
||||
title: 'Alerts',
|
||||
},
|
||||
{
|
||||
id: 'item3',
|
||||
title: 'Some other node',
|
||||
},
|
||||
{
|
||||
id: 'group:settings',
|
||||
|
@ -333,24 +300,22 @@ export const WithUIComponents = (args: NavigationServices) => {
|
|||
icon="logoObservability"
|
||||
defaultIsCollapsed={false}
|
||||
>
|
||||
<Navigation.Group id="root">
|
||||
<Navigation.Item<any> id="item1" link="item1" />
|
||||
<Navigation.Item id="item2" title="Alerts">
|
||||
{(navNode) => {
|
||||
return (
|
||||
<div className="euiSideNavItemButton">
|
||||
<EuiText size="s">{`Render prop: ${navNode.id} - ${navNode.title}`}</EuiText>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Navigation.Item>
|
||||
<Navigation.Item id="item3" title="Title in ReactNode">
|
||||
<div className="euiSideNavItemButton">
|
||||
<EuiLink>Title in ReactNode</EuiLink>
|
||||
</div>
|
||||
</Navigation.Item>
|
||||
<Navigation.Item id="item4" title="External link" href="https://elastic.co" />
|
||||
</Navigation.Group>
|
||||
<Navigation.Item<any> id="item1" link="item1" />
|
||||
<Navigation.Item id="item2" title="Alerts">
|
||||
{(navNode) => {
|
||||
return (
|
||||
<div className="euiSideNavItemButton">
|
||||
<EuiText size="s">{`Render prop: ${navNode.id} - ${navNode.title}`}</EuiText>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Navigation.Item>
|
||||
<Navigation.Item id="item3" title="Title in ReactNode">
|
||||
<div className="euiSideNavItemButton">
|
||||
<EuiLink>Title in ReactNode</EuiLink>
|
||||
</div>
|
||||
</Navigation.Item>
|
||||
<Navigation.Item id="item4" title="External link" href="https://elastic.co" />
|
||||
|
||||
<Navigation.Group id="group:settings" title="Settings">
|
||||
<Navigation.Item id="logs" title="Logs" />
|
||||
|
@ -370,12 +335,10 @@ export const WithUIComponents = (args: NavigationServices) => {
|
|||
breadcrumbStatus="hidden"
|
||||
icon="gear"
|
||||
>
|
||||
<Navigation.Group id="settings">
|
||||
<Navigation.Item link="management" title="Management" />
|
||||
<Navigation.Item id="cloudLinkUserAndRoles" cloudLink="userAndRoles" />
|
||||
<Navigation.Item id="cloudLinkPerformance" cloudLink="performance" />
|
||||
<Navigation.Item id="cloudLinkBilling" cloudLink="billingAndSub" />
|
||||
</Navigation.Group>
|
||||
<Navigation.Item link="management" title="Management" />
|
||||
<Navigation.Item id="cloudLinkUserAndRoles" cloudLink="userAndRoles" />
|
||||
<Navigation.Item id="cloudLinkPerformance" cloudLink="performance" />
|
||||
<Navigation.Item id="cloudLinkBilling" cloudLink="billingAndSub" />
|
||||
</Navigation.Group>
|
||||
</Navigation.Footer>
|
||||
</Navigation>
|
||||
|
@ -551,3 +514,121 @@ export const CreativeUI = (args: NavigationServices) => {
|
|||
</NavigationWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const UpdatingState = (args: NavigationServices) => {
|
||||
const simpleGroupDef: GroupDefinition = {
|
||||
type: 'navGroup',
|
||||
id: 'observability_project_nav',
|
||||
title: 'Observability',
|
||||
icon: 'logoObservability',
|
||||
children: [
|
||||
{
|
||||
id: 'aiops',
|
||||
title: 'AIOps',
|
||||
icon: 'branch',
|
||||
children: [
|
||||
{
|
||||
title: 'Anomaly detection',
|
||||
id: 'ml:anomalyDetection',
|
||||
link: 'ml:anomalyDetection',
|
||||
},
|
||||
{
|
||||
title: 'Log Rate Analysis',
|
||||
id: 'ml:logRateAnalysis',
|
||||
link: 'ml:logRateAnalysis',
|
||||
},
|
||||
{
|
||||
title: 'Change Point Detections',
|
||||
link: 'ml:changePointDetections',
|
||||
id: 'ml:changePointDetections',
|
||||
},
|
||||
{
|
||||
title: 'Job Notifications',
|
||||
link: 'ml:notifications',
|
||||
id: 'ml:notifications',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'project_settings_project_nav',
|
||||
title: 'Project settings',
|
||||
icon: 'gear',
|
||||
children: [
|
||||
{ id: 'management', link: 'management' },
|
||||
{ id: 'integrations', link: 'integrations' },
|
||||
{ id: 'fleet', link: 'fleet' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const firstSection = simpleGroupDef.children![0];
|
||||
const firstSectionFirstChild = firstSection.children![0];
|
||||
const secondSection = simpleGroupDef.children![1];
|
||||
const secondSectionFirstChild = secondSection.children![0];
|
||||
|
||||
const activeNodeSets: ChromeProjectNavigationNode[][][] = [
|
||||
[
|
||||
[
|
||||
{
|
||||
...simpleGroupDef,
|
||||
path: [simpleGroupDef.id],
|
||||
} as unknown as ChromeProjectNavigationNode,
|
||||
{
|
||||
...firstSection,
|
||||
path: [simpleGroupDef.id, firstSection.id],
|
||||
} as unknown as ChromeProjectNavigationNode,
|
||||
{
|
||||
...firstSectionFirstChild,
|
||||
path: [simpleGroupDef.id, firstSection.id, firstSectionFirstChild.id],
|
||||
} as unknown as ChromeProjectNavigationNode,
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
{
|
||||
...simpleGroupDef,
|
||||
path: [simpleGroupDef.id],
|
||||
} as unknown as ChromeProjectNavigationNode,
|
||||
{
|
||||
...secondSection,
|
||||
path: [simpleGroupDef.id, secondSection.id],
|
||||
} as unknown as ChromeProjectNavigationNode,
|
||||
{
|
||||
...secondSectionFirstChild,
|
||||
path: [simpleGroupDef.id, secondSection.id, secondSectionFirstChild.id],
|
||||
} as unknown as ChromeProjectNavigationNode,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// use state to track which element of activeNodeSets is active
|
||||
const [activeNodeIndex, setActiveNodeIndex] = useState<number>(0);
|
||||
const changeActiveNode = () => {
|
||||
const value = (activeNodeIndex + 1) % 2; // toggle between 0 and 1
|
||||
setActiveNodeIndex(value);
|
||||
};
|
||||
|
||||
const activeNodes$ = new BehaviorSubject<ChromeProjectNavigationNode[][]>([]);
|
||||
activeNodes$.next(activeNodeSets[activeNodeIndex]);
|
||||
|
||||
const services = storybookMock.getServices({
|
||||
...args,
|
||||
activeNodes$,
|
||||
navLinks$: of([...navLinksMock, ...deepLinks]),
|
||||
onProjectNavigationChange: (updated) => {
|
||||
action('Update chrome navigation')(JSON.stringify(updated, null, 2));
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<NavigationWrapper clickAction={changeActiveNode} clickActionText="Change active node">
|
||||
<NavigationProvider {...services}>
|
||||
<DefaultNavigation
|
||||
navigationTree={{
|
||||
body: [simpleGroupDef],
|
||||
}}
|
||||
/>
|
||||
</NavigationProvider>
|
||||
</NavigationWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,13 +6,14 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import type { EuiAccordionProps } from '@elastic/eui';
|
||||
import type {
|
||||
AppDeepLinkId,
|
||||
ChromeProjectNavigationNode,
|
||||
NodeDefinition,
|
||||
} from '@kbn/core-chrome-browser';
|
||||
|
||||
import type { RecentlyAccessedProps } from './components';
|
||||
|
||||
export type NonEmptyArray<T> = [T, ...T[]];
|
||||
|
@ -45,11 +46,6 @@ export interface NodePropsEnhanced<
|
|||
Id extends string = string,
|
||||
ChildrenId extends string = Id
|
||||
> extends NodeProps<LinkId, Id, ChildrenId> {
|
||||
/**
|
||||
* This function correspond to the same "itemRender" function that can be passed to
|
||||
* the EuiSideNavItemType (see navigation_section_ui.tsx)
|
||||
*/
|
||||
renderItem?: () => ReactElement;
|
||||
/**
|
||||
* Forces the node to be active. This is used to force a collapisble nav group to be open
|
||||
* even if the URL does not match any of the nodes in the group.
|
||||
|
@ -57,17 +53,6 @@ export interface NodePropsEnhanced<
|
|||
isActive?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface ChromeProjectNavigationNodeEnhanced extends ChromeProjectNavigationNode {
|
||||
/**
|
||||
* This function correspond to the same "itemRender" function that can be passed to
|
||||
* the EuiSideNavItemType (see navigation_section_ui.tsx)
|
||||
*/
|
||||
renderItem?: () => ReactElement;
|
||||
}
|
||||
|
||||
/** The preset that can be pass to the NavigationBucket component */
|
||||
export type NavigationGroupPreset = 'analytics' | 'devtools' | 'ml' | 'management';
|
||||
|
||||
|
@ -101,6 +86,10 @@ export interface GroupDefinition<
|
|||
* `true`: the group will be collapsed event if any of its children nodes matches the current URL.
|
||||
*/
|
||||
defaultIsCollapsed?: boolean;
|
||||
/*
|
||||
* Pass props to the EUI accordion component used to represent a nav group
|
||||
*/
|
||||
accordionProps?: Partial<EuiAccordionProps>;
|
||||
preset?: NavigationGroupPreset;
|
||||
}
|
||||
|
||||
|
@ -172,7 +161,7 @@ export type UnRegisterFunction = (id: string) => void;
|
|||
*
|
||||
* A function to register a navigation node on its parent.
|
||||
*/
|
||||
export type RegisterFunction = (navNode: ChromeProjectNavigationNodeEnhanced) => {
|
||||
export type RegisterFunction = (navNode: ChromeProjectNavigationNode) => {
|
||||
/** The function to unregister the node. */
|
||||
unregister: UnRegisterFunction;
|
||||
/** The full path of the node in the navigation tree. */
|
||||
|
|
|
@ -25,135 +25,134 @@ const navigationTree: NavigationTreeDefinition = {
|
|||
title: 'Observability',
|
||||
icon: 'logoObservability',
|
||||
defaultIsCollapsed: false,
|
||||
accordionProps: {
|
||||
arrowProps: { css: { display: 'none' } },
|
||||
},
|
||||
breadcrumbStatus: 'hidden',
|
||||
children: [
|
||||
{
|
||||
id: 'discover-dashboard-alerts-slos',
|
||||
title: i18n.translate('xpack.serverlessObservability.nav.logExplorer', {
|
||||
defaultMessage: 'Log Explorer',
|
||||
}),
|
||||
link: 'observability-log-explorer',
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.serverlessObservability.nav.dashboards', {
|
||||
defaultMessage: 'Dashboards',
|
||||
}),
|
||||
link: 'dashboards',
|
||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||
return pathNameSerialized.startsWith(prepend('/app/dashboards'));
|
||||
},
|
||||
},
|
||||
{
|
||||
link: 'observability-overview:alerts',
|
||||
},
|
||||
{
|
||||
link: 'observability-overview:slos',
|
||||
},
|
||||
{
|
||||
id: 'aiops',
|
||||
title: 'AIOps',
|
||||
accordionProps: {
|
||||
arrowProps: { css: { display: 'none' } },
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: i18n.translate('xpack.serverlessObservability.nav.logExplorer', {
|
||||
defaultMessage: 'Log Explorer',
|
||||
title: i18n.translate('xpack.serverlessObservability.nav.ml.jobs', {
|
||||
defaultMessage: 'Anomaly detection',
|
||||
}),
|
||||
link: 'observability-log-explorer',
|
||||
link: 'ml:anomalyDetection',
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.serverlessObservability.nav.dashboards', {
|
||||
defaultMessage: 'Dashboards',
|
||||
title: i18n.translate('xpack.serverlessObservability.ml.logRateAnalysis', {
|
||||
defaultMessage: 'Log rate analysis',
|
||||
}),
|
||||
link: 'dashboards',
|
||||
link: 'ml:logRateAnalysis',
|
||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||
return pathNameSerialized.startsWith(prepend('/app/dashboards'));
|
||||
return pathNameSerialized.includes(prepend('/app/ml/aiops/log_rate_analysis'));
|
||||
},
|
||||
},
|
||||
{
|
||||
link: 'observability-overview:alerts',
|
||||
},
|
||||
{
|
||||
link: 'observability-overview:slos',
|
||||
},
|
||||
{
|
||||
id: 'aiops',
|
||||
title: 'AIOps',
|
||||
children: [
|
||||
{
|
||||
title: i18n.translate('xpack.serverlessObservability.nav.ml.jobs', {
|
||||
defaultMessage: 'Anomaly detection',
|
||||
}),
|
||||
link: 'ml:anomalyDetection',
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.serverlessObservability.ml.logRateAnalysis', {
|
||||
defaultMessage: 'Log rate analysis',
|
||||
}),
|
||||
link: 'ml:logRateAnalysis',
|
||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||
return pathNameSerialized.includes(prepend('/app/ml/aiops/log_rate_analysis'));
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.serverlessObservability.ml.changePointDetection', {
|
||||
defaultMessage: 'Change point detection',
|
||||
}),
|
||||
link: 'ml:changePointDetections',
|
||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||
return pathNameSerialized.includes(
|
||||
prepend('/app/ml/aiops/change_point_detection')
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.serverlessObservability.nav.ml.job.notifications', {
|
||||
defaultMessage: 'Job notifications',
|
||||
}),
|
||||
link: 'ml:notifications',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'applications',
|
||||
children: [
|
||||
{
|
||||
id: 'apm',
|
||||
title: i18n.translate('xpack.serverlessObservability.nav.applications', {
|
||||
defaultMessage: 'Applications',
|
||||
title: i18n.translate('xpack.serverlessObservability.ml.changePointDetection', {
|
||||
defaultMessage: 'Change point detection',
|
||||
}),
|
||||
children: [
|
||||
{
|
||||
link: 'apm:services',
|
||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||
const regex = /app\/apm\/.*service.*/;
|
||||
return regex.test(pathNameSerialized);
|
||||
},
|
||||
},
|
||||
{
|
||||
link: 'apm:traces',
|
||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||
return pathNameSerialized.startsWith(prepend('/app/apm/traces'));
|
||||
},
|
||||
},
|
||||
{
|
||||
link: 'apm:dependencies',
|
||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||
return pathNameSerialized.startsWith(prepend('/app/apm/dependencies'));
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'cases-vis',
|
||||
children: [
|
||||
{
|
||||
link: 'observability-overview:cases',
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.serverlessObservability.nav.visualizations', {
|
||||
defaultMessage: 'Visualizations',
|
||||
}),
|
||||
link: 'visualize',
|
||||
link: 'ml:changePointDetections',
|
||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||
return (
|
||||
pathNameSerialized.startsWith(prepend('/app/visualize')) ||
|
||||
pathNameSerialized.startsWith(prepend('/app/lens')) ||
|
||||
pathNameSerialized.startsWith(prepend('/app/maps'))
|
||||
);
|
||||
return pathNameSerialized.includes(prepend('/app/ml/aiops/change_point_detection'));
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.serverlessObservability.nav.ml.job.notifications', {
|
||||
defaultMessage: 'Job notifications',
|
||||
}),
|
||||
link: 'ml:notifications',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'groups-spacer-1',
|
||||
isGroupTitle: true,
|
||||
},
|
||||
{
|
||||
id: 'apm',
|
||||
title: i18n.translate('xpack.serverlessObservability.nav.applications', {
|
||||
defaultMessage: 'Applications',
|
||||
}),
|
||||
accordionProps: {
|
||||
arrowProps: { css: { display: 'none' } },
|
||||
},
|
||||
children: [
|
||||
{
|
||||
link: 'apm:services',
|
||||
getIsActive: ({ pathNameSerialized }) => {
|
||||
const regex = /app\/apm\/.*service.*/;
|
||||
return regex.test(pathNameSerialized);
|
||||
},
|
||||
},
|
||||
{
|
||||
link: 'apm:traces',
|
||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||
return pathNameSerialized.startsWith(prepend('/app/apm/traces'));
|
||||
},
|
||||
},
|
||||
{
|
||||
link: 'apm:dependencies',
|
||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||
return pathNameSerialized.startsWith(prepend('/app/apm/dependencies'));
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'on-boarding',
|
||||
children: [
|
||||
{
|
||||
title: i18n.translate('xpack.serverlessObservability.nav.getStarted', {
|
||||
defaultMessage: 'Add data',
|
||||
}),
|
||||
link: 'observabilityOnboarding',
|
||||
},
|
||||
],
|
||||
id: 'groups-spacer-2',
|
||||
isGroupTitle: true,
|
||||
},
|
||||
{
|
||||
link: 'observability-overview:cases',
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.serverlessObservability.nav.visualizations', {
|
||||
defaultMessage: 'Visualizations',
|
||||
}),
|
||||
link: 'visualize',
|
||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||
return (
|
||||
pathNameSerialized.startsWith(prepend('/app/visualize')) ||
|
||||
pathNameSerialized.startsWith(prepend('/app/lens')) ||
|
||||
pathNameSerialized.startsWith(prepend('/app/maps'))
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'groups-spacer-3',
|
||||
isGroupTitle: true,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.serverlessObservability.nav.getStarted', {
|
||||
defaultMessage: 'Add data',
|
||||
}),
|
||||
link: 'observabilityOnboarding',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -178,29 +177,24 @@ const navigationTree: NavigationTreeDefinition = {
|
|||
breadcrumbStatus: 'hidden',
|
||||
children: [
|
||||
{
|
||||
id: 'settings',
|
||||
children: [
|
||||
{
|
||||
link: 'management',
|
||||
title: i18n.translate('xpack.serverlessObservability.nav.mngt', {
|
||||
defaultMessage: 'Management',
|
||||
}),
|
||||
},
|
||||
{
|
||||
link: 'integrations',
|
||||
},
|
||||
{
|
||||
link: 'fleet',
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkUserAndRoles',
|
||||
cloudLink: 'userAndRoles',
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkBilling',
|
||||
cloudLink: 'billingAndSub',
|
||||
},
|
||||
],
|
||||
link: 'management',
|
||||
title: i18n.translate('xpack.serverlessObservability.nav.mngt', {
|
||||
defaultMessage: 'Management',
|
||||
}),
|
||||
},
|
||||
{
|
||||
link: 'integrations',
|
||||
},
|
||||
{
|
||||
link: 'fleet',
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkUserAndRoles',
|
||||
cloudLink: 'userAndRoles',
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkBilling',
|
||||
cloudLink: 'billingAndSub',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -25,6 +25,9 @@ const navigationTree: NavigationTreeDefinition = {
|
|||
title: 'Elasticsearch',
|
||||
icon: 'logoElasticsearch',
|
||||
defaultIsCollapsed: false,
|
||||
accordionProps: {
|
||||
arrowProps: { css: { display: 'none' } },
|
||||
},
|
||||
breadcrumbStatus: 'hidden',
|
||||
children: [
|
||||
{
|
||||
|
@ -39,77 +42,75 @@ const navigationTree: NavigationTreeDefinition = {
|
|||
title: i18n.translate('xpack.serverlessSearch.nav.devTools', {
|
||||
defaultMessage: 'Dev Tools',
|
||||
}),
|
||||
children: [{ link: 'dev_tools:console' }, { link: 'dev_tools:searchprofiler' }],
|
||||
isGroupTitle: true,
|
||||
},
|
||||
{ link: 'dev_tools:console' },
|
||||
{ link: 'dev_tools:searchprofiler' },
|
||||
{
|
||||
id: 'explore',
|
||||
title: i18n.translate('xpack.serverlessSearch.nav.explore', {
|
||||
defaultMessage: 'Explore',
|
||||
}),
|
||||
children: [
|
||||
{
|
||||
link: 'discover',
|
||||
},
|
||||
{
|
||||
link: 'dashboards',
|
||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||
return pathNameSerialized.startsWith(prepend('/app/dashboards'));
|
||||
},
|
||||
},
|
||||
{
|
||||
link: 'visualize',
|
||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||
return (
|
||||
pathNameSerialized.startsWith(prepend('/app/visualize')) ||
|
||||
pathNameSerialized.startsWith(prepend('/app/lens')) ||
|
||||
pathNameSerialized.startsWith(prepend('/app/maps'))
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
link: 'management:triggersActions',
|
||||
title: i18n.translate('xpack.serverlessSearch.nav.alerts', {
|
||||
defaultMessage: 'Alerts',
|
||||
}),
|
||||
},
|
||||
],
|
||||
isGroupTitle: true,
|
||||
},
|
||||
{
|
||||
link: 'discover',
|
||||
},
|
||||
{
|
||||
link: 'dashboards',
|
||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||
return pathNameSerialized.startsWith(prepend('/app/dashboards'));
|
||||
},
|
||||
},
|
||||
{
|
||||
link: 'visualize',
|
||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||
return (
|
||||
pathNameSerialized.startsWith(prepend('/app/visualize')) ||
|
||||
pathNameSerialized.startsWith(prepend('/app/lens')) ||
|
||||
pathNameSerialized.startsWith(prepend('/app/maps'))
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
link: 'management:triggersActions',
|
||||
title: i18n.translate('xpack.serverlessSearch.nav.alerts', {
|
||||
defaultMessage: 'Alerts',
|
||||
}),
|
||||
},
|
||||
|
||||
{
|
||||
id: 'content',
|
||||
title: i18n.translate('xpack.serverlessSearch.nav.content', {
|
||||
defaultMessage: 'Content',
|
||||
}),
|
||||
children: [
|
||||
{
|
||||
title: i18n.translate('xpack.serverlessSearch.nav.content.indices', {
|
||||
defaultMessage: 'Index Management',
|
||||
}),
|
||||
link: 'management:index_management',
|
||||
breadcrumbStatus:
|
||||
'hidden' /* management sub-pages set their breadcrumbs themselves */,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.serverlessSearch.nav.content.pipelines', {
|
||||
defaultMessage: 'Pipelines',
|
||||
}),
|
||||
link: 'management:ingest_pipelines',
|
||||
breadcrumbStatus:
|
||||
'hidden' /* management sub-pages set their breadcrumbs themselves */,
|
||||
},
|
||||
],
|
||||
isGroupTitle: true,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.serverlessSearch.nav.content.indices', {
|
||||
defaultMessage: 'Index Management',
|
||||
}),
|
||||
link: 'management:index_management',
|
||||
breadcrumbStatus: 'hidden' /* management sub-pages set their breadcrumbs themselves */,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.serverlessSearch.nav.content.pipelines', {
|
||||
defaultMessage: 'Pipelines',
|
||||
}),
|
||||
link: 'management:ingest_pipelines',
|
||||
breadcrumbStatus: 'hidden' /* management sub-pages set their breadcrumbs themselves */,
|
||||
},
|
||||
|
||||
{
|
||||
id: 'security',
|
||||
title: i18n.translate('xpack.serverlessSearch.nav.security', {
|
||||
defaultMessage: 'Security',
|
||||
}),
|
||||
children: [
|
||||
{
|
||||
link: 'management:api_keys',
|
||||
breadcrumbStatus:
|
||||
'hidden' /* management sub-pages set their breadcrumbs themselves */,
|
||||
},
|
||||
],
|
||||
isGroupTitle: true,
|
||||
},
|
||||
{
|
||||
link: 'management:api_keys',
|
||||
breadcrumbStatus: 'hidden' /* management sub-pages set their breadcrumbs themselves */,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -125,30 +126,25 @@ const navigationTree: NavigationTreeDefinition = {
|
|||
breadcrumbStatus: 'hidden',
|
||||
children: [
|
||||
{
|
||||
id: 'settings',
|
||||
children: [
|
||||
{
|
||||
link: 'management',
|
||||
title: i18n.translate('xpack.serverlessSearch.nav.mngt', {
|
||||
defaultMessage: 'Management',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkDeployment',
|
||||
cloudLink: 'deployment',
|
||||
title: i18n.translate('xpack.serverlessSearch.nav.performance', {
|
||||
defaultMessage: 'Performance',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkUserAndRoles',
|
||||
cloudLink: 'userAndRoles',
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkBilling',
|
||||
cloudLink: 'billingAndSub',
|
||||
},
|
||||
],
|
||||
link: 'management',
|
||||
title: i18n.translate('xpack.serverlessSearch.nav.mngt', {
|
||||
defaultMessage: 'Management',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkDeployment',
|
||||
cloudLink: 'deployment',
|
||||
title: i18n.translate('xpack.serverlessSearch.nav.performance', {
|
||||
defaultMessage: 'Performance',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkUserAndRoles',
|
||||
cloudLink: 'userAndRoles',
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkBilling',
|
||||
cloudLink: 'billingAndSub',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -23,6 +23,7 @@ export function SvlCommonNavigationProvider(ctx: FtrProviderContext) {
|
|||
const testSubjects = ctx.getService('testSubjects');
|
||||
const browser = ctx.getService('browser');
|
||||
const retry = ctx.getService('retry');
|
||||
const log = ctx.getService('log');
|
||||
|
||||
async function getByVisibleText(
|
||||
selector: string | (() => Promise<WebElementWrapper[]>),
|
||||
|
@ -93,16 +94,20 @@ export function SvlCommonNavigationProvider(ctx: FtrProviderContext) {
|
|||
}
|
||||
},
|
||||
async expectSectionExists(sectionId: NavigationId) {
|
||||
log.debug('ServerlessCommonNavigation.sidenav.expectSectionExists', sectionId);
|
||||
await testSubjects.existOrFail(`~nav-bucket-${sectionId}`);
|
||||
},
|
||||
async isSectionOpen(sectionId: NavigationId) {
|
||||
await this.expectSectionExists(sectionId);
|
||||
const section = await testSubjects.find(`~nav-bucket-${sectionId}`);
|
||||
const collapseBtn = await section.findByCssSelector(`[aria-controls="${sectionId}"]`);
|
||||
const collapseBtn = await section.findByCssSelector(
|
||||
`[aria-controls="${sectionId}"][aria-expanded]`
|
||||
);
|
||||
const isExpanded = await collapseBtn.getAttribute('aria-expanded');
|
||||
return isExpanded === 'true';
|
||||
},
|
||||
async expectSectionOpen(sectionId: NavigationId) {
|
||||
log.debug('ServerlessCommonNavigation.sidenav.expectSectionOpen', sectionId);
|
||||
await this.expectSectionExists(sectionId);
|
||||
await retry.waitFor(`section ${sectionId} to be open`, async () => {
|
||||
const isOpen = await this.isSectionOpen(sectionId);
|
||||
|
@ -117,11 +122,14 @@ export function SvlCommonNavigationProvider(ctx: FtrProviderContext) {
|
|||
});
|
||||
},
|
||||
async openSection(sectionId: NavigationId) {
|
||||
log.debug('ServerlessCommonNavigation.sidenav.openSection', sectionId);
|
||||
await this.expectSectionExists(sectionId);
|
||||
const isOpen = await this.isSectionOpen(sectionId);
|
||||
if (isOpen) return;
|
||||
const section = await testSubjects.find(`~nav-bucket-${sectionId}`);
|
||||
const collapseBtn = await section.findByCssSelector(`[aria-controls="${sectionId}"]`);
|
||||
const collapseBtn = await section.findByCssSelector(
|
||||
`[aria-controls="${sectionId}"][aria-expanded]`
|
||||
);
|
||||
await collapseBtn.click();
|
||||
await this.expectSectionOpen(sectionId);
|
||||
},
|
||||
|
@ -130,7 +138,9 @@ export function SvlCommonNavigationProvider(ctx: FtrProviderContext) {
|
|||
const isOpen = await this.isSectionOpen(sectionId);
|
||||
if (!isOpen) return;
|
||||
const section = await testSubjects.find(`~nav-bucket-${sectionId}`);
|
||||
const collapseBtn = await section.findByCssSelector(`[aria-controls="${sectionId}"]`);
|
||||
const collapseBtn = await section.findByCssSelector(
|
||||
`[aria-controls="${sectionId}"][aria-expanded]`
|
||||
);
|
||||
await collapseBtn.click();
|
||||
await this.expectSectionClosed(sectionId);
|
||||
},
|
||||
|
@ -143,6 +153,10 @@ export function SvlCommonNavigationProvider(ctx: FtrProviderContext) {
|
|||
await testSubjects.click('~breadcrumb-home');
|
||||
},
|
||||
async expectBreadcrumbExists(by: { deepLinkId: AppDeepLinkId } | { text: string }) {
|
||||
log.debug(
|
||||
'ServerlessCommonNavigation.breadcrumbs.expectBreadcrumbExists',
|
||||
JSON.stringify(by)
|
||||
);
|
||||
if ('deepLinkId' in by) {
|
||||
await testSubjects.existOrFail(`~breadcrumb-deepLinkId-${by.deepLinkId}`);
|
||||
} else {
|
||||
|
@ -161,6 +175,10 @@ export function SvlCommonNavigationProvider(ctx: FtrProviderContext) {
|
|||
}
|
||||
},
|
||||
async expectBreadcrumbTexts(expectedBreadcrumbTexts: string[]) {
|
||||
log.debug(
|
||||
'ServerlessCommonNavigation.breadcrumbs.expectBreadcrumbTexts',
|
||||
JSON.stringify(expectedBreadcrumbTexts)
|
||||
);
|
||||
await retry.try(async () => {
|
||||
const breadcrumbsContainer = await testSubjects.find('breadcrumbs');
|
||||
const breadcrumbs = await breadcrumbsContainer.findAllByTestSubject('~breadcrumb');
|
||||
|
|
|
@ -48,9 +48,8 @@ export default function ({ getPageObject, getService }: FtrProviderContext) {
|
|||
// navigate to discover
|
||||
await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'discover' });
|
||||
await svlCommonNavigation.sidenav.expectLinkActive({ deepLinkId: 'discover' });
|
||||
await svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({ text: `Explore` });
|
||||
await svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({ deepLinkId: 'discover' });
|
||||
await expect(await browser.getCurrentUrl()).contain('/app/discover');
|
||||
expect(await browser.getCurrentUrl()).contain('/app/discover');
|
||||
|
||||
// navigate to a different section
|
||||
await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'management:index_management' });
|
||||
|
@ -73,20 +72,16 @@ export default function ({ getPageObject, getService }: FtrProviderContext) {
|
|||
|
||||
it("management apps from the sidenav hide the 'stack management' root from the breadcrumbs", async () => {
|
||||
await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'management:triggersActions' });
|
||||
await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts(['Explore', 'Alerts', 'Rules']);
|
||||
await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts(['Alerts', 'Rules']);
|
||||
|
||||
await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'management:index_management' });
|
||||
await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts([
|
||||
'Content',
|
||||
'Index Management',
|
||||
'Indices',
|
||||
]);
|
||||
await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts(['Index Management', 'Indices']);
|
||||
|
||||
await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'management:ingest_pipelines' });
|
||||
await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts(['Content', 'Ingest Pipelines']);
|
||||
await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts(['Ingest Pipelines']);
|
||||
|
||||
await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'management:api_keys' });
|
||||
await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts(['Security', 'API keys']);
|
||||
await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts(['API keys']);
|
||||
});
|
||||
|
||||
it('navigate management', async () => {
|
||||
|
@ -104,7 +99,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) {
|
|||
await svlCommonNavigation.search.clickOnOption(0);
|
||||
await svlCommonNavigation.search.hideSearch();
|
||||
|
||||
await expect(await browser.getCurrentUrl()).contain('/app/discover');
|
||||
expect(await browser.getCurrentUrl()).contain('/app/discover');
|
||||
});
|
||||
|
||||
it('does not show cases in sidebar navigation', async () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue