mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -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';
|
import { HeaderActionMenu } from '../header/header_action_menu';
|
||||||
|
|
||||||
interface AppMenuBarProps {
|
interface AppMenuBarProps {
|
||||||
isOpen: boolean;
|
|
||||||
headerActionMenuMounter: { mount: MountPoint<HTMLElement> | undefined };
|
headerActionMenuMounter: { mount: MountPoint<HTMLElement> | undefined };
|
||||||
}
|
}
|
||||||
export const AppMenuBar = ({ headerActionMenuMounter }: AppMenuBarProps) => {
|
export const AppMenuBar = ({ headerActionMenuMounter }: AppMenuBarProps) => {
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import { EuiHeader } from '@elastic/eui';
|
import { EuiHeader } from '@elastic/eui';
|
||||||
import { applicationServiceMock } from '@kbn/core-application-browser-mocks';
|
import { applicationServiceMock } from '@kbn/core-application-browser-mocks';
|
||||||
import { docLinksServiceMock } from '@kbn/core-doc-links-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 React from 'react';
|
||||||
import * as Rx from 'rxjs';
|
import * as Rx from 'rxjs';
|
||||||
import { ProjectHeader, Props as ProjectHeaderProps } from './header';
|
import { ProjectHeader, Props as ProjectHeaderProps } from './header';
|
||||||
|
@ -45,35 +45,10 @@ describe('Header', () => {
|
||||||
</ProjectHeader>
|
</ProjectHeader>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(await screen.findByTestId('toggleNavButton')).toBeVisible();
|
expect(await screen.findByTestId('euiCollapsibleNavButton')).toBeVisible();
|
||||||
expect(await screen.findByText('Hello, world!')).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 () => {
|
it('displays the link to projects', async () => {
|
||||||
render(
|
render(
|
||||||
<ProjectHeader {...mockProps}>
|
<ProjectHeader {...mockProps}>
|
||||||
|
@ -81,7 +56,7 @@ describe('Header', () => {
|
||||||
</ProjectHeader>
|
</ProjectHeader>
|
||||||
);
|
);
|
||||||
|
|
||||||
const projectsLink = await screen.getByTestId('projectsLink');
|
const projectsLink = screen.getByTestId('projectsLink');
|
||||||
expect(projectsLink).toHaveAttribute('href', '/projects/');
|
expect(projectsLink).toHaveAttribute('href', '/projects/');
|
||||||
expect(projectsLink).toHaveTextContent('My Project');
|
expect(projectsLink).toHaveTextContent('My Project');
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,10 +12,7 @@ import {
|
||||||
EuiHeaderLogo,
|
EuiHeaderLogo,
|
||||||
EuiHeaderSection,
|
EuiHeaderSection,
|
||||||
EuiHeaderSectionItem,
|
EuiHeaderSectionItem,
|
||||||
EuiHeaderSectionItemButton,
|
|
||||||
EuiIcon,
|
|
||||||
EuiLoadingSpinner,
|
EuiLoadingSpinner,
|
||||||
htmlIdGenerator,
|
|
||||||
useEuiTheme,
|
useEuiTheme,
|
||||||
EuiThemeComputed,
|
EuiThemeComputed,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
|
@ -35,8 +32,7 @@ import { MountPoint } from '@kbn/core-mount-utils-browser';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
|
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
|
||||||
import { Router } from '@kbn/shared-ux-router';
|
import { Router } from '@kbn/shared-ux-router';
|
||||||
import React, { createRef, useCallback, useState } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
|
||||||
import useObservable from 'react-use/lib/useObservable';
|
import useObservable from 'react-use/lib/useObservable';
|
||||||
import { debounceTime, Observable, of } from 'rxjs';
|
import { debounceTime, Observable, of } from 'rxjs';
|
||||||
import { useHeaderActionMenuMounter } from '../header/header_action_menu';
|
import { useHeaderActionMenuMounter } from '../header/header_action_menu';
|
||||||
|
@ -66,13 +62,6 @@ const getHeaderCss = ({ size }: EuiThemeComputed) => ({
|
||||||
top: 2px;
|
top: 2px;
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
nav: {
|
|
||||||
toggleNavButton: css`
|
|
||||||
border-right: 1px solid #d3dae6;
|
|
||||||
margin-left: -1px;
|
|
||||||
padding-right: ${size.xs};
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
projectName: {
|
projectName: {
|
||||||
link: css`
|
link: css`
|
||||||
/* TODO: make header layout more flexible? */
|
/* TODO: make header layout more flexible? */
|
||||||
|
@ -123,7 +112,6 @@ export interface Props {
|
||||||
prependBasePath: (url: string) => string;
|
prependBasePath: (url: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LOCAL_STORAGE_IS_OPEN_KEY = 'PROJECT_NAVIGATION_OPEN' as const;
|
|
||||||
const LOADING_DEBOUNCE_TIME = 80;
|
const LOADING_DEBOUNCE_TIME = 80;
|
||||||
|
|
||||||
type LogoProps = Pick<Props, 'application' | 'homeHref$' | 'loadingCount$' | 'prependBasePath'> & {
|
type LogoProps = Pick<Props, 'application' | 'homeHref$' | 'loadingCount$' | 'prependBasePath'> & {
|
||||||
|
@ -186,9 +174,6 @@ export const ProjectHeader = ({
|
||||||
docLinks,
|
docLinks,
|
||||||
...observables
|
...observables
|
||||||
}: Props) => {
|
}: 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 headerActionMenuMounter = useHeaderActionMenuMounter(observables.actionMenu$);
|
||||||
const projectsUrl = useObservable(observables.projectsUrl$);
|
const projectsUrl = useObservable(observables.projectsUrl$);
|
||||||
const projectName = useObservable(observables.projectName$);
|
const projectName = useObservable(observables.projectName$);
|
||||||
|
@ -210,34 +195,9 @@ export const ProjectHeader = ({
|
||||||
<div id="globalHeaderBars" data-test-subj="headerGlobalNav" className="header__bars">
|
<div id="globalHeaderBars" data-test-subj="headerGlobalNav" className="header__bars">
|
||||||
<EuiHeader position="fixed" className="header__firstBar">
|
<EuiHeader position="fixed" className="header__firstBar">
|
||||||
<EuiHeaderSection grow={false}>
|
<EuiHeaderSection grow={false}>
|
||||||
<EuiHeaderSectionItem css={headerCss.nav.toggleNavButton}>
|
<Router history={application.history}>
|
||||||
<Router history={application.history}>
|
<ProjectNavigation>{children}</ProjectNavigation>
|
||||||
<ProjectNavigation
|
</Router>
|
||||||
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>
|
|
||||||
|
|
||||||
<EuiHeaderSectionItem>
|
<EuiHeaderSectionItem>
|
||||||
<Logo
|
<Logo
|
||||||
|
@ -297,7 +257,7 @@ export const ProjectHeader = ({
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{headerActionMenuMounter.mount && (
|
{headerActionMenuMounter.mount && (
|
||||||
<AppMenuBar isOpen={isOpen ?? false} headerActionMenuMounter={headerActionMenuMounter} />
|
<AppMenuBar headerActionMenuMounter={headerActionMenuMounter} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,55 +6,25 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { EuiCollapsibleNavBeta } from '@elastic/eui';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { css } from '@emotion/react';
|
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||||
import { EuiCollapsibleNav, EuiCollapsibleNavProps } from '@elastic/eui';
|
|
||||||
|
|
||||||
const SIZE_EXPANDED = 248;
|
const LOCAL_STORAGE_IS_COLLAPSED_KEY = 'PROJECT_NAVIGATION_COLLAPSED' as const;
|
||||||
const SIZE_COLLAPSED = 0;
|
|
||||||
|
|
||||||
export interface ProjectNavigationProps {
|
export const ProjectNavigation: React.FC = ({ children }) => {
|
||||||
isOpen: boolean;
|
const [isCollapsed, setIsCollapsed] = useLocalStorage(LOCAL_STORAGE_IS_COLLAPSED_KEY, false);
|
||||||
closeNav: () => void;
|
const onCollapseToggle = (nextIsCollapsed: boolean) => {
|
||||||
button: EuiCollapsibleNavProps['button'];
|
setIsCollapsed(nextIsCollapsed);
|
||||||
}
|
};
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<EuiCollapsibleNavBeta
|
||||||
{
|
initialIsCollapsed={isCollapsed}
|
||||||
/* must render the tree to initialize the navigation, even if it shouldn't be visible */
|
onCollapseToggle={onCollapseToggle}
|
||||||
!isOpen && <div hidden>{children}</div>
|
css={isCollapsed ? { display: 'none;' } : {}}
|
||||||
}
|
>
|
||||||
<EuiCollapsibleNav
|
{children}
|
||||||
className="projectLayoutSideNav"
|
</EuiCollapsibleNavBeta>
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,8 +5,10 @@
|
||||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ComponentType } from 'react';
|
import type { ComponentType } from 'react';
|
||||||
import type { Location } from 'history';
|
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 DevToolsApp, DeepLinkId as DevToolsLink } from '@kbn/deeplinks-devtools';
|
||||||
import type {
|
import type {
|
||||||
AppId as AnalyticsApp,
|
AppId as AnalyticsApp,
|
||||||
|
@ -68,6 +70,8 @@ export interface ChromeProjectNavigationNode {
|
||||||
deepLink?: ChromeNavLink;
|
deepLink?: ChromeNavLink;
|
||||||
/** Optional icon for the navigation node. Note: not all navigation depth will render the icon */
|
/** Optional icon for the navigation node. Note: not all navigation depth will render the icon */
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
/** Optional flag to indicate if the node must be treated as a group title */
|
||||||
|
isGroupTitle?: boolean;
|
||||||
/** Optional children of the navigation node */
|
/** Optional children of the navigation node */
|
||||||
children?: ChromeProjectNavigationNode[];
|
children?: ChromeProjectNavigationNode[];
|
||||||
/**
|
/**
|
||||||
|
@ -88,6 +92,8 @@ export interface ChromeProjectNavigationNode {
|
||||||
* @default 'visible'
|
* @default 'visible'
|
||||||
*/
|
*/
|
||||||
breadcrumbStatus?: 'hidden' | 'visible';
|
breadcrumbStatus?: 'hidden' | 'visible';
|
||||||
|
|
||||||
|
accordionProps?: Partial<EuiAccordionProps>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -139,7 +145,12 @@ export interface NodeDefinition<
|
||||||
cloudLink?: CloudLinkId;
|
cloudLink?: CloudLinkId;
|
||||||
/** Optional icon for the navigation node. Note: not all navigation depth will render the icon */
|
/** Optional icon for the navigation node. Note: not all navigation depth will render the icon */
|
||||||
icon?: string;
|
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>>;
|
children?: NonEmptyArray<NodeDefinition<LinkId, Id, ChildrenId>>;
|
||||||
/**
|
/**
|
||||||
* Use href for absolute links only. Internal links should use "link".
|
* Use href for absolute links only. Internal links should use "link".
|
||||||
|
@ -155,6 +166,8 @@ export interface NodeDefinition<
|
||||||
* @default 'visible'
|
* @default 'visible'
|
||||||
*/
|
*/
|
||||||
breadcrumbStatus?: 'hidden' | 'visible';
|
breadcrumbStatus?: 'hidden' | 'visible';
|
||||||
|
|
||||||
|
accordionProps?: Partial<EuiAccordionProps>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,18 +21,13 @@ export const defaultNavigation: AnalyticsNodeDefinition = {
|
||||||
icon: 'stats',
|
icon: 'stats',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: 'root',
|
link: 'discover',
|
||||||
children: [
|
},
|
||||||
{
|
{
|
||||||
link: 'discover',
|
link: 'dashboards',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
link: 'dashboards',
|
link: 'visualize',
|
||||||
},
|
|
||||||
{
|
|
||||||
link: 'visualize',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,21 +21,16 @@ export const defaultNavigation: DevToolsNodeDefinition = {
|
||||||
icon: 'editorCodeBlock',
|
icon: 'editorCodeBlock',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: 'root',
|
link: 'dev_tools:console',
|
||||||
children: [
|
},
|
||||||
{
|
{
|
||||||
link: 'dev_tools:console',
|
link: 'dev_tools:searchprofiler',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
link: 'dev_tools:searchprofiler',
|
link: 'dev_tools:grokdebugger',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
link: 'dev_tools:grokdebugger',
|
link: 'dev_tools:painless_lab',
|
||||||
},
|
|
||||||
{
|
|
||||||
link: 'dev_tools:painless_lab',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,13 +29,7 @@ export const defaultNavigation: ManagementNodeDefinition = {
|
||||||
icon: 'gear',
|
icon: 'gear',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: 'root',
|
link: 'monitoring',
|
||||||
title: '',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
link: 'monitoring',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'integration_management',
|
id: 'integration_management',
|
||||||
|
|
|
@ -28,16 +28,10 @@ export const defaultNavigation: MlNodeDefinition = {
|
||||||
icon: 'machineLearningApp',
|
icon: 'machineLearningApp',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: '',
|
link: 'ml:overview',
|
||||||
id: 'root',
|
},
|
||||||
children: [
|
{
|
||||||
{
|
link: 'ml:notifications',
|
||||||
link: 'ml:overview',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
link: 'ml:notifications',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n.translate('defaultNavigation.ml.anomalyDetection', {
|
title: i18n.translate('defaultNavigation.ml.anomalyDetection', {
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { NavigationServices } from '../../types';
|
||||||
type Arguments = NavigationServices;
|
type Arguments = NavigationServices;
|
||||||
export type Params = Pick<
|
export type Params = Pick<
|
||||||
Arguments,
|
Arguments,
|
||||||
'navIsOpen' | 'recentlyAccessed$' | 'navLinks$' | 'onProjectNavigationChange'
|
'navIsOpen' | 'recentlyAccessed$' | 'activeNodes$' | 'navLinks$' | 'onProjectNavigationChange'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export class StorybookMock extends AbstractStorybookMock<{}, NavigationServices> {
|
export class StorybookMock extends AbstractStorybookMock<{}, NavigationServices> {
|
||||||
|
@ -43,7 +43,7 @@ export class StorybookMock extends AbstractStorybookMock<{}, NavigationServices>
|
||||||
recentlyAccessed$: params.recentlyAccessed$ ?? new BehaviorSubject([]),
|
recentlyAccessed$: params.recentlyAccessed$ ?? new BehaviorSubject([]),
|
||||||
navLinks$: params.navLinks$ ?? new BehaviorSubject([]),
|
navLinks$: params.navLinks$ ?? new BehaviorSubject([]),
|
||||||
onProjectNavigationChange: params.onProjectNavigationChange ?? (() => undefined),
|
onProjectNavigationChange: params.onProjectNavigationChange ?? (() => undefined),
|
||||||
activeNodes$: new BehaviorSubject([]),
|
activeNodes$: params.activeNodes$ ?? new BehaviorSubject([]),
|
||||||
cloudLinks: {
|
cloudLinks: {
|
||||||
billingAndSub: {
|
billingAndSub: {
|
||||||
title: 'Billing & Subscriptions',
|
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",
|
"group1",
|
||||||
"item1",
|
"item1",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Item 1",
|
"title": "Item 1",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
@ -33,7 +32,6 @@ Array [
|
||||||
"group1",
|
"group1",
|
||||||
"item2",
|
"item2",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Title from deeplink!",
|
"title": "Title from deeplink!",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
@ -52,7 +50,6 @@ Array [
|
||||||
"group1",
|
"group1",
|
||||||
"item3",
|
"item3",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Deeplink title overriden",
|
"title": "Deeplink title overriden",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -69,77 +66,58 @@ Array [
|
||||||
Object {
|
Object {
|
||||||
"children": Array [
|
"children": Array [
|
||||||
Object {
|
Object {
|
||||||
"children": Array [
|
"children": undefined,
|
||||||
Object {
|
"deepLink": Object {
|
||||||
"children": undefined,
|
"baseUrl": "/mocked",
|
||||||
"deepLink": Object {
|
"href": "http://mocked/discover",
|
||||||
"baseUrl": "/mocked",
|
"id": "discover",
|
||||||
"href": "http://mocked/discover",
|
"title": "Deeplink discover",
|
||||||
"id": "discover",
|
"url": "/mocked/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,
|
|
||||||
"href": undefined,
|
"href": undefined,
|
||||||
"id": "root",
|
"id": "discover",
|
||||||
"isActive": false,
|
"isActive": false,
|
||||||
"path": Array [
|
"path": Array [
|
||||||
"rootNav:analytics",
|
"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,
|
"deepLink": undefined,
|
||||||
|
@ -156,57 +134,40 @@ Array [
|
||||||
Object {
|
Object {
|
||||||
"children": Array [
|
"children": Array [
|
||||||
Object {
|
Object {
|
||||||
"children": Array [
|
"children": undefined,
|
||||||
Object {
|
"deepLink": Object {
|
||||||
"children": undefined,
|
"baseUrl": "/mocked",
|
||||||
"deepLink": Object {
|
"href": "http://mocked/ml:overview",
|
||||||
"baseUrl": "/mocked",
|
"id": "ml:overview",
|
||||||
"href": "http://mocked/ml:overview",
|
"title": "Deeplink ml:overview",
|
||||||
"id": "ml:overview",
|
"url": "/mocked/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,
|
|
||||||
"href": undefined,
|
"href": undefined,
|
||||||
"id": "root",
|
"id": "ml:overview",
|
||||||
"isActive": false,
|
"isActive": false,
|
||||||
"path": Array [
|
"path": Array [
|
||||||
"rootNav:ml",
|
"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 {
|
Object {
|
||||||
"children": Array [
|
"children": Array [
|
||||||
|
@ -227,7 +188,6 @@ Array [
|
||||||
"anomaly_detection",
|
"anomaly_detection",
|
||||||
"ml:anomalyDetection",
|
"ml:anomalyDetection",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Jobs",
|
"title": "Jobs",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
@ -247,7 +207,6 @@ Array [
|
||||||
"anomaly_detection",
|
"anomaly_detection",
|
||||||
"ml:anomalyExplorer",
|
"ml:anomalyExplorer",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Deeplink ml:anomalyExplorer",
|
"title": "Deeplink ml:anomalyExplorer",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
@ -267,7 +226,6 @@ Array [
|
||||||
"anomaly_detection",
|
"anomaly_detection",
|
||||||
"ml:singleMetricViewer",
|
"ml:singleMetricViewer",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Deeplink ml:singleMetricViewer",
|
"title": "Deeplink ml:singleMetricViewer",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
@ -287,7 +245,6 @@ Array [
|
||||||
"anomaly_detection",
|
"anomaly_detection",
|
||||||
"ml:settings",
|
"ml:settings",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Deeplink ml:settings",
|
"title": "Deeplink ml:settings",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -320,7 +277,6 @@ Array [
|
||||||
"data_frame_analytics",
|
"data_frame_analytics",
|
||||||
"ml:dataFrameAnalytics",
|
"ml:dataFrameAnalytics",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Jobs",
|
"title": "Jobs",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
@ -340,7 +296,6 @@ Array [
|
||||||
"data_frame_analytics",
|
"data_frame_analytics",
|
||||||
"ml:resultExplorer",
|
"ml:resultExplorer",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Deeplink ml:resultExplorer",
|
"title": "Deeplink ml:resultExplorer",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
@ -360,7 +315,6 @@ Array [
|
||||||
"data_frame_analytics",
|
"data_frame_analytics",
|
||||||
"ml:analyticsMap",
|
"ml:analyticsMap",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Deeplink ml:analyticsMap",
|
"title": "Deeplink ml:analyticsMap",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -393,7 +347,6 @@ Array [
|
||||||
"model_management",
|
"model_management",
|
||||||
"ml:nodesOverview",
|
"ml:nodesOverview",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Deeplink ml:nodesOverview",
|
"title": "Deeplink ml:nodesOverview",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
@ -413,7 +366,6 @@ Array [
|
||||||
"model_management",
|
"model_management",
|
||||||
"ml:nodes",
|
"ml:nodes",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Deeplink ml:nodes",
|
"title": "Deeplink ml:nodes",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -446,7 +398,6 @@ Array [
|
||||||
"data_visualizer",
|
"data_visualizer",
|
||||||
"ml:fileUpload",
|
"ml:fileUpload",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "File",
|
"title": "File",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
@ -466,7 +417,6 @@ Array [
|
||||||
"data_visualizer",
|
"data_visualizer",
|
||||||
"ml:indexDataVisualizer",
|
"ml:indexDataVisualizer",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Data view",
|
"title": "Data view",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
@ -486,7 +436,6 @@ Array [
|
||||||
"data_visualizer",
|
"data_visualizer",
|
||||||
"ml:dataDrift",
|
"ml:dataDrift",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Data drift",
|
"title": "Data drift",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -519,7 +468,6 @@ Array [
|
||||||
"aiops_labs",
|
"aiops_labs",
|
||||||
"ml:logRateAnalysis",
|
"ml:logRateAnalysis",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Deeplink ml:logRateAnalysis",
|
"title": "Deeplink ml:logRateAnalysis",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
@ -539,7 +487,6 @@ Array [
|
||||||
"aiops_labs",
|
"aiops_labs",
|
||||||
"ml:logPatternAnalysis",
|
"ml:logPatternAnalysis",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Deeplink ml:logPatternAnalysis",
|
"title": "Deeplink ml:logPatternAnalysis",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
@ -559,7 +506,6 @@ Array [
|
||||||
"aiops_labs",
|
"aiops_labs",
|
||||||
"ml:changePointDetections",
|
"ml:changePointDetections",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Deeplink ml:changePointDetections",
|
"title": "Deeplink ml:changePointDetections",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -608,65 +554,46 @@ Array [
|
||||||
"breadcrumbStatus": "hidden",
|
"breadcrumbStatus": "hidden",
|
||||||
"children": Array [
|
"children": Array [
|
||||||
Object {
|
Object {
|
||||||
"children": Array [
|
"children": undefined,
|
||||||
Object {
|
"deepLink": Object {
|
||||||
"children": undefined,
|
"baseUrl": "/mocked",
|
||||||
"deepLink": Object {
|
"href": "http://mocked/management",
|
||||||
"baseUrl": "/mocked",
|
"id": "management",
|
||||||
"href": "http://mocked/management",
|
"title": "Deeplink management",
|
||||||
"id": "management",
|
"url": "/mocked/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,
|
|
||||||
"href": undefined,
|
"href": undefined,
|
||||||
"id": "settings",
|
"id": "management",
|
||||||
"isActive": false,
|
"isActive": false,
|
||||||
"path": Array [
|
"path": Array [
|
||||||
"project_settings_project_nav",
|
"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,
|
"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(
|
const { findByTestId } = render(
|
||||||
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
|
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
|
||||||
<Navigation>
|
<Navigation>
|
||||||
<Navigation.Group id="group1">
|
<Navigation.Group id="group1" defaultIsCollapsed={false}>
|
||||||
<Navigation.Item id="item1" title="Item 1" href="https://foo" />
|
<Navigation.Item id="item1" title="Item 1" href="https://foo" />
|
||||||
<Navigation.Item id="item2" title="Item 2" 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.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.Item id="item1" title="Group 1A_1 Item 1" href="https://foo" />
|
||||||
</Navigation.Group>
|
</Navigation.Group>
|
||||||
</Navigation.Group>
|
</Navigation.Group>
|
||||||
|
@ -62,10 +62,10 @@ describe('<Navigation />', () => {
|
||||||
expect(await findByTestId(/nav-item-group1.item2/)).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\s/)).toBeVisible();
|
||||||
expect(await findByTestId(/nav-item-group1.group1A.item1/)).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
|
// 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();
|
expect(await findByTestId(/nav-item-group1.group1A.group1A_1.item1/)).toBeVisible();
|
||||||
|
|
||||||
|
@ -76,56 +76,56 @@ describe('<Navigation />', () => {
|
||||||
|
|
||||||
expect(navTree.navigationTree).toEqual([
|
expect(navTree.navigationTree).toEqual([
|
||||||
{
|
{
|
||||||
id: 'group1',
|
|
||||||
path: ['group1'],
|
|
||||||
title: '',
|
|
||||||
isActive: false,
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: 'item1',
|
|
||||||
title: 'Item 1',
|
|
||||||
href: 'https://foo',
|
href: 'https://foo',
|
||||||
|
id: 'item1',
|
||||||
isActive: false,
|
isActive: false,
|
||||||
path: ['group1', 'item1'],
|
path: ['group1', 'item1'],
|
||||||
|
title: 'Item 1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'item2',
|
|
||||||
title: 'Item 2',
|
|
||||||
href: 'https://foo',
|
href: 'https://foo',
|
||||||
|
id: 'item2',
|
||||||
isActive: false,
|
isActive: false,
|
||||||
path: ['group1', 'item2'],
|
path: ['group1', 'item2'],
|
||||||
|
title: 'Item 2',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'group1A',
|
|
||||||
title: 'Group1A',
|
|
||||||
isActive: false,
|
|
||||||
path: ['group1', 'group1A'],
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: 'item1',
|
|
||||||
href: 'https://foo',
|
href: 'https://foo',
|
||||||
title: 'Group 1A Item 1',
|
id: 'item1',
|
||||||
isActive: false,
|
isActive: false,
|
||||||
path: ['group1', 'group1A', 'item1'],
|
path: ['group1', 'group1A', 'item1'],
|
||||||
|
title: 'Group 1A Item 1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'group1A_1',
|
|
||||||
title: 'Group1A_1',
|
|
||||||
isActive: false,
|
|
||||||
path: ['group1', 'group1A', 'group1A_1'],
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: 'item1',
|
|
||||||
title: 'Group 1A_1 Item 1',
|
|
||||||
isActive: false,
|
|
||||||
href: 'https://foo',
|
href: 'https://foo',
|
||||||
|
id: 'item1',
|
||||||
|
isActive: false,
|
||||||
path: ['group1', 'group1A', 'group1A_1', 'item1'],
|
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}
|
onProjectNavigationChange={onProjectNavigationChange}
|
||||||
>
|
>
|
||||||
<Navigation>
|
<Navigation>
|
||||||
<Navigation.Group id="root">
|
<Navigation.Group id="root" defaultIsCollapsed={false}>
|
||||||
<Navigation.Group id="group1">
|
<Navigation.Group id="group1" defaultIsCollapsed={false}>
|
||||||
{/* Title from deeplink */}
|
{/* Title from deeplink */}
|
||||||
<Navigation.Item<any> id="item1" link="item1" />
|
<Navigation.Item<any> id="item1" link="item1" />
|
||||||
{/* Should not appear */}
|
{/* Should not appear */}
|
||||||
|
@ -279,13 +279,13 @@ describe('<Navigation />', () => {
|
||||||
id: 'root',
|
id: 'root',
|
||||||
path: ['root'],
|
path: ['root'],
|
||||||
title: '',
|
title: '',
|
||||||
isActive: false,
|
isActive: true,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: 'group1',
|
id: 'group1',
|
||||||
path: ['root', 'group1'],
|
path: ['root', 'group1'],
|
||||||
title: '',
|
title: '',
|
||||||
isActive: false,
|
isActive: true,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: 'item1',
|
id: 'item1',
|
||||||
|
@ -326,11 +326,11 @@ describe('<Navigation />', () => {
|
||||||
onProjectNavigationChange={onProjectNavigationChange}
|
onProjectNavigationChange={onProjectNavigationChange}
|
||||||
>
|
>
|
||||||
<Navigation>
|
<Navigation>
|
||||||
<Navigation.Group id="root">
|
<Navigation.Group id="root" defaultIsCollapsed={false}>
|
||||||
<Navigation.Group id="group1">
|
<Navigation.Group id="group1" defaultIsCollapsed={false}>
|
||||||
<Navigation.Item<any> id="item1" link="notRegistered" />
|
<Navigation.Item<any> id="item1" link="notRegistered" />
|
||||||
</Navigation.Group>
|
</Navigation.Group>
|
||||||
<Navigation.Group id="group2">
|
<Navigation.Group id="group2" defaultIsCollapsed={false}>
|
||||||
<Navigation.Item<any> id="item1" link="item1" />
|
<Navigation.Item<any> id="item1" link="item1" />
|
||||||
</Navigation.Group>
|
</Navigation.Group>
|
||||||
</Navigation.Group>
|
</Navigation.Group>
|
||||||
|
@ -352,128 +352,39 @@ describe('<Navigation />', () => {
|
||||||
|
|
||||||
expect(navTree.navigationTree).toEqual([
|
expect(navTree.navigationTree).toEqual([
|
||||||
{
|
{
|
||||||
id: 'root',
|
|
||||||
path: ['root'],
|
|
||||||
title: '',
|
|
||||||
isActive: false,
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: 'group1',
|
id: 'group1',
|
||||||
|
isActive: true,
|
||||||
path: ['root', 'group1'],
|
path: ['root', 'group1'],
|
||||||
title: '',
|
title: '',
|
||||||
isActive: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'group2',
|
|
||||||
path: ['root', 'group2'],
|
|
||||||
title: '',
|
|
||||||
isActive: false,
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
deepLink: {
|
||||||
|
baseUrl: '',
|
||||||
|
href: '',
|
||||||
|
id: 'item1',
|
||||||
|
title: 'Title from deeplink',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
id: 'item1',
|
id: 'item1',
|
||||||
|
isActive: false,
|
||||||
path: ['root', 'group2', 'item1'],
|
path: ['root', 'group2', 'item1'],
|
||||||
title: 'Title from deeplink',
|
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',
|
id: 'root',
|
||||||
|
isActive: true,
|
||||||
path: ['root'],
|
path: ['root'],
|
||||||
title: '',
|
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>
|
</NavigationProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(await findByTestId(/nav-item-group1.item1/)).toHaveClass(
|
expect((await findByTestId(/nav-item-group1.item1/)).dataset.testSubj).toMatch(
|
||||||
'euiSideNavItemButton-isSelected'
|
/nav-item-isActive/
|
||||||
);
|
);
|
||||||
expect(await findByTestId(/nav-item-group1.item2/)).not.toHaveClass(
|
expect((await findByTestId(/nav-item-group1.item2/)).dataset.testSubj).not.toMatch(
|
||||||
'euiSideNavItemButton-isSelected'
|
/nav-item-isActive/
|
||||||
);
|
);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
@ -673,11 +584,11 @@ describe('<Navigation />', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(await findByTestId(/nav-item-group1.item1/)).not.toHaveClass(
|
expect((await findByTestId(/nav-item-group1.item1/)).dataset.testSubj).not.toMatch(
|
||||||
'euiSideNavItemButton-isSelected'
|
/nav-item-isActive/
|
||||||
);
|
);
|
||||||
expect(await findByTestId(/nav-item-group1.item2/)).toHaveClass(
|
expect((await findByTestId(/nav-item-group1.item2/)).dataset.testSubj).toMatch(
|
||||||
'euiSideNavItemButton-isSelected'
|
/nav-item-isActive/
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -730,8 +641,8 @@ describe('<Navigation />', () => {
|
||||||
|
|
||||||
jest.advanceTimersByTime(SET_NAVIGATION_DELAY);
|
jest.advanceTimersByTime(SET_NAVIGATION_DELAY);
|
||||||
|
|
||||||
expect(await findByTestId(/nav-item-group1.item1/)).toHaveClass(
|
expect((await findByTestId(/nav-item-group1.item1/)).dataset.testSubj).toMatch(
|
||||||
'euiSideNavItemButton-isSelected'
|
/nav-item-isActive/
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -743,7 +654,7 @@ describe('<Navigation />', () => {
|
||||||
const { findByTestId } = render(
|
const { findByTestId } = render(
|
||||||
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
|
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
|
||||||
<Navigation>
|
<Navigation>
|
||||||
<Navigation.Group id="group1">
|
<Navigation.Group id="group1" defaultIsCollapsed={false}>
|
||||||
<Navigation.Item id="cloudLink1" cloudLink="userAndRoles" />
|
<Navigation.Item id="cloudLink1" cloudLink="userAndRoles" />
|
||||||
<Navigation.Item id="cloudLink2" cloudLink="performance" />
|
<Navigation.Item id="cloudLink2" cloudLink="performance" />
|
||||||
<Navigation.Item id="cloudLink3" cloudLink="billingAndSub" />
|
<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.cloudLink2/)).toBeVisible();
|
||||||
expect(await findByTestId(/nav-item-group1.cloudLink3/)).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'
|
'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'
|
'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'
|
'Mock Billing & SubscriptionsExternal link'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -85,7 +85,7 @@ function NavigationGroupInternalComp<
|
||||||
)}
|
)}
|
||||||
{/* We render the children so they mount and can register themselves but
|
{/* 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
|
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}
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,12 +6,12 @@
|
||||||
* Side Public License, v 1.
|
* 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 { useNavigation as useNavigationServices } from '../../services';
|
||||||
import type { ChromeProjectNavigationNodeEnhanced, NodeProps } from '../types';
|
|
||||||
import { useInitNavNode } from '../hooks';
|
import { useInitNavNode } from '../hooks';
|
||||||
|
import type { NodeProps } from '../types';
|
||||||
import { useNavigation } from './navigation';
|
import { useNavigation } from './navigation';
|
||||||
|
|
||||||
export interface Props<
|
export interface Props<
|
||||||
|
@ -22,10 +22,6 @@ export interface Props<
|
||||||
unstyled?: boolean;
|
unstyled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isReactElement(element: ReactNode): element is ReactElement {
|
|
||||||
return React.isValidElement(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
function NavigationItemComp<
|
function NavigationItemComp<
|
||||||
LinkId extends AppDeepLinkId = AppDeepLinkId,
|
LinkId extends AppDeepLinkId = AppDeepLinkId,
|
||||||
Id extends string = string,
|
Id extends string = string,
|
||||||
|
@ -33,7 +29,7 @@ function NavigationItemComp<
|
||||||
>(props: Props<LinkId, Id, ChildrenId>) {
|
>(props: Props<LinkId, Id, ChildrenId>) {
|
||||||
const { cloudLinks } = useNavigationServices();
|
const { cloudLinks } = useNavigationServices();
|
||||||
const navigationContext = useNavigation();
|
const navigationContext = useNavigation();
|
||||||
const navNodeRef = React.useRef<ChromeProjectNavigationNodeEnhanced | null>(null);
|
const navNodeRef = React.useRef<ChromeProjectNavigationNode | null>(null);
|
||||||
|
|
||||||
const { children, node } = useMemo(() => {
|
const { children, node } = useMemo(() => {
|
||||||
const { children: _children, ...rest } = props;
|
const { children: _children, ...rest } = props;
|
||||||
|
@ -44,14 +40,7 @@ function NavigationItemComp<
|
||||||
}, [props]);
|
}, [props]);
|
||||||
const unstyled = props.unstyled ?? navigationContext.unstyled;
|
const unstyled = props.unstyled ?? navigationContext.unstyled;
|
||||||
|
|
||||||
let renderItem: (() => ReactElement) | undefined;
|
const { navNode } = useInitNavNode({ ...node, children }, { cloudLinks });
|
||||||
|
|
||||||
if (!unstyled && children && (typeof children === 'function' || isReactElement(children))) {
|
|
||||||
renderItem =
|
|
||||||
typeof children === 'function' ? () => children(navNodeRef.current) : () => children;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { navNode } = useInitNavNode({ ...node, children, renderItem }, { cloudLinks });
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
navNodeRef.current = navNode;
|
navNodeRef.current = navNode;
|
||||||
|
|
|
@ -7,28 +7,22 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC, useEffect, useState } from 'react';
|
import React, { FC, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EuiCollapsibleNavGroup,
|
EuiCollapsibleNavItem,
|
||||||
EuiIcon,
|
EuiCollapsibleNavItemProps,
|
||||||
EuiLink,
|
EuiCollapsibleNavSubItemGroupTitle,
|
||||||
EuiSideNav,
|
|
||||||
EuiSideNavItemType,
|
|
||||||
EuiText,
|
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
|
import { ChromeProjectNavigationNode } from '@kbn/core-chrome-browser';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import type { BasePathService, NavigateToUrlFn } from '../../../types/internal';
|
import type { BasePathService, NavigateToUrlFn } from '../../../types/internal';
|
||||||
import { navigationStyles as styles } from '../../styles';
|
|
||||||
import { useNavigation as useServices } from '../../services';
|
import { useNavigation as useServices } from '../../services';
|
||||||
import { ChromeProjectNavigationNodeEnhanced } from '../types';
|
|
||||||
import { isAbsoluteLink } from '../../utils';
|
import { isAbsoluteLink } from '../../utils';
|
||||||
import { GroupAsLink } from './group_as_link';
|
|
||||||
|
|
||||||
type RenderItem = EuiSideNavItemType<unknown>['renderItem'];
|
|
||||||
|
|
||||||
const navigationNodeToEuiItem = (
|
const navigationNodeToEuiItem = (
|
||||||
item: ChromeProjectNavigationNodeEnhanced,
|
item: ChromeProjectNavigationNode,
|
||||||
{ navigateToUrl, basePath }: { navigateToUrl: NavigateToUrlFn; basePath: BasePathService }
|
{ navigateToUrl, basePath }: { navigateToUrl: NavigateToUrlFn; basePath: BasePathService }
|
||||||
): EuiSideNavItemType<unknown> => {
|
): EuiCollapsibleNavSubItemGroupTitle | EuiCollapsibleNavItemProps => {
|
||||||
const href = item.deepLink?.url ?? item.href;
|
const href = item.deepLink?.url ?? item.href;
|
||||||
const id = item.path ? item.path.join('.') : item.id;
|
const id = item.path ? item.path.join('.') : item.id;
|
||||||
const isExternal = Boolean(href) && isAbsoluteLink(href!);
|
const isExternal = Boolean(href) && isAbsoluteLink(href!);
|
||||||
|
@ -39,24 +33,16 @@ const navigationNodeToEuiItem = (
|
||||||
[`nav-item-isActive`]: isSelected,
|
[`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 {
|
return {
|
||||||
id,
|
id,
|
||||||
name: item.title,
|
isGroupTitle: item.isGroupTitle,
|
||||||
|
title: item.title,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
accordionProps: {
|
||||||
|
...item.accordionProps,
|
||||||
|
initialIsOpen: true, // FIXME open state is controlled on component mount
|
||||||
|
},
|
||||||
|
linkProps: { external: isExternal },
|
||||||
onClick:
|
onClick:
|
||||||
href !== undefined
|
href !== undefined
|
||||||
? (event: React.MouseEvent) => {
|
? (event: React.MouseEvent) => {
|
||||||
|
@ -65,20 +51,18 @@ const navigationNodeToEuiItem = (
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
href,
|
href,
|
||||||
renderItem: getRenderItem(),
|
|
||||||
items: item.children?.map((_item) =>
|
items: item.children?.map((_item) =>
|
||||||
navigationNodeToEuiItem(_item, { navigateToUrl, basePath })
|
navigationNodeToEuiItem(_item, { navigateToUrl, basePath })
|
||||||
),
|
),
|
||||||
['data-test-subj']: dataTestSubj,
|
['data-test-subj']: dataTestSubj,
|
||||||
...(item.icon && {
|
icon: item.icon,
|
||||||
icon: <EuiIcon type={item.icon} size="s" />,
|
iconProps: { size: 's' },
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
navNode: ChromeProjectNavigationNodeEnhanced;
|
navNode: ChromeProjectNavigationNode;
|
||||||
items?: ChromeProjectNavigationNodeEnhanced[];
|
items?: ChromeProjectNavigationNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NavigationSectionUI: FC<Props> = ({ navNode, items = [] }) => {
|
export const NavigationSectionUI: FC<Props> = ({ navNode, items = [] }) => {
|
||||||
|
@ -90,8 +74,12 @@ export const NavigationSectionUI: FC<Props> = ({ navNode, items = [] }) => {
|
||||||
const [doCollapseFromActiveState, setDoCollapseFromActiveState] = useState(true);
|
const [doCollapseFromActiveState, setDoCollapseFromActiveState] = useState(true);
|
||||||
|
|
||||||
// If the item has no link and no cildren, we don't want to render it
|
// 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);
|
const hasLink = Boolean(item.deepLink) || Boolean(item.href);
|
||||||
|
if (isGroupTitle) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (hasLink) {
|
if (hasLink) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -128,49 +116,39 @@ export const NavigationSectionUI: FC<Props> = ({ navNode, items = [] }) => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const propsForGroupAsLink = groupIsLink
|
const propsForGroupAsLink: Partial<EuiCollapsibleNavItemProps> = groupIsLink
|
||||||
? {
|
? {
|
||||||
buttonElement: 'div' as const,
|
linkProps: {
|
||||||
// If we don't force the state there is a little UI animation as if the
|
href: groupHref,
|
||||||
// accordion was openin/closing. We don't want any animation when it is a link.
|
onClick: (e: React.MouseEvent) => {
|
||||||
forceState: 'closed' as const,
|
e.preventDefault();
|
||||||
buttonContent: (
|
e.stopPropagation();
|
||||||
<GroupAsLink
|
navigateToUrl(groupHref);
|
||||||
title={title}
|
},
|
||||||
iconType={icon}
|
},
|
||||||
href={groupHref}
|
|
||||||
navigateToUrl={navigateToUrl}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
arrowProps: { style: { display: 'none' } },
|
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiCollapsibleNavGroup
|
<EuiCollapsibleNavItem
|
||||||
id={id}
|
id={id}
|
||||||
title={title}
|
title={title}
|
||||||
iconType={icon}
|
icon={icon}
|
||||||
iconSize="m"
|
iconProps={{ size: 'm' }}
|
||||||
isCollapsible
|
accordionProps={{
|
||||||
initialIsOpen={isActive}
|
initialIsOpen: isActive,
|
||||||
onToggle={(isOpen) => {
|
forceState: isCollapsed ? 'closed' : 'open',
|
||||||
setIsCollapsed(!isOpen);
|
onToggle: (isOpen) => {
|
||||||
setDoCollapseFromActiveState(false);
|
setIsCollapsed(!isOpen);
|
||||||
|
setDoCollapseFromActiveState(false);
|
||||||
|
},
|
||||||
|
...navNode.accordionProps,
|
||||||
}}
|
}}
|
||||||
forceState={isCollapsed ? 'closed' : 'open'}
|
|
||||||
data-test-subj={`nav-bucket-${id}`}
|
data-test-subj={`nav-bucket-${id}`}
|
||||||
{...propsForGroupAsLink}
|
{...propsForGroupAsLink}
|
||||||
>
|
items={filteredItems.map((item) =>
|
||||||
<EuiText color="default">
|
navigationNodeToEuiItem(item, { navigateToUrl, basePath })
|
||||||
<EuiSideNav
|
)}
|
||||||
mobileBreakpoints={/* turn off responsive behavior */ []}
|
/>
|
||||||
items={filteredItems.map((item) =>
|
|
||||||
navigationNodeToEuiItem(item, { navigateToUrl, basePath })
|
|
||||||
)}
|
|
||||||
css={styles.euiSideNavItems}
|
|
||||||
/>
|
|
||||||
</EuiText>
|
|
||||||
</EuiCollapsibleNavGroup>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
import { EuiFlyoutBody, EuiFlyoutFooter } from '@elastic/eui';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -21,17 +21,12 @@ export const NavigationUI: FC<Props> = ({ children, unstyled, footerChildren, da
|
||||||
{unstyled ? (
|
{unstyled ? (
|
||||||
<>{children}</>
|
<>{children}</>
|
||||||
) : (
|
) : (
|
||||||
<EuiFlexGroup
|
<>
|
||||||
direction="column"
|
<EuiFlyoutBody scrollableTabIndex={-1} data-test-subj={dataTestSubj}>
|
||||||
gutterSize="none"
|
{children}
|
||||||
style={{ overflowY: 'auto' }}
|
</EuiFlyoutBody>
|
||||||
justifyContent="spaceBetween"
|
{footerChildren && <EuiFlyoutFooter>{footerChildren}</EuiFlyoutFooter>}
|
||||||
data-test-subj={dataTestSubj}
|
</>
|
||||||
>
|
|
||||||
<EuiFlexItem grow={false}>{children}</EuiFlexItem>
|
|
||||||
|
|
||||||
{footerChildren && <EuiFlexItem grow={false}>{footerChildren}</EuiFlexItem>}
|
|
||||||
</EuiFlexGroup>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,14 +6,13 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EuiCollapsibleNavGroup, EuiSideNav, EuiSideNavItemType } from '@elastic/eui';
|
import { EuiCollapsibleNavItem } from '@elastic/eui';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import useObservable from 'react-use/lib/useObservable';
|
import useObservable from 'react-use/lib/useObservable';
|
||||||
import type { Observable } from 'rxjs';
|
import type { Observable } from 'rxjs';
|
||||||
|
|
||||||
import { RecentItem } from '../../../types/internal';
|
import { RecentItem } from '../../../types/internal';
|
||||||
import { useNavigation as useServices } from '../../services';
|
import { useNavigation as useServices } from '../../services';
|
||||||
import { navigationStyles as styles } from '../../styles';
|
|
||||||
|
|
||||||
import { getI18nStrings } from '../i18n_strings';
|
import { getI18nStrings } from '../i18n_strings';
|
||||||
|
|
||||||
|
@ -42,40 +41,31 @@ export const RecentlyAccessed: FC<Props> = ({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const navItems: Array<EuiSideNavItemType<unknown>> = [
|
const navItems = recentlyAccessed.map((recent) => {
|
||||||
{
|
const { id, label, link } = recent;
|
||||||
name: '', // no list header title
|
const href = basePath.prepend(link);
|
||||||
id: 'recents_root',
|
|
||||||
items: recentlyAccessed.map((recent) => {
|
|
||||||
const { id, label, link } = recent;
|
|
||||||
const href = basePath.prepend(link);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name: label,
|
title: label,
|
||||||
href,
|
href,
|
||||||
onClick: (e: React.MouseEvent) => {
|
onClick: (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigateToUrl(href);
|
navigateToUrl(href);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
});
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiCollapsibleNavGroup
|
<EuiCollapsibleNavItem
|
||||||
title={strings.recentlyAccessed}
|
title={strings.recentlyAccessed}
|
||||||
iconType="clock"
|
icon="clock"
|
||||||
isCollapsible={true}
|
iconProps={{ size: 'm' }}
|
||||||
initialIsOpen={!defaultIsCollapsed}
|
accordionProps={{
|
||||||
|
initialIsOpen: !defaultIsCollapsed,
|
||||||
|
}}
|
||||||
data-test-subj={`nav-bucket-recentlyAccessed`}
|
data-test-subj={`nav-bucket-recentlyAccessed`}
|
||||||
>
|
items={navItems}
|
||||||
<EuiSideNav
|
/>
|
||||||
items={navItems}
|
|
||||||
css={styles.euiSideNavItems}
|
|
||||||
mobileBreakpoints={/* turn off responsive behavior */ []}
|
|
||||||
/>
|
|
||||||
</EuiCollapsibleNavGroup>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -80,7 +80,7 @@ describe('<DefaultNavigation />', () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const { findByTestId } = render(
|
const { findAllByTestId } = render(
|
||||||
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
|
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
|
||||||
<DefaultNavigation navigationTree={{ body: navigationBody }} />
|
<DefaultNavigation navigationTree={{ body: navigationBody }} />
|
||||||
</NavigationProvider>
|
</NavigationProvider>
|
||||||
|
@ -90,16 +90,8 @@ describe('<DefaultNavigation />', () => {
|
||||||
jest.advanceTimersByTime(SET_NAVIGATION_DELAY);
|
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
|
// Click the last group to expand and show the last depth
|
||||||
(await findByTestId(/nav-item-group1.group1A.group1A_1/)).click();
|
(await findAllByTestId(/nav-item-group1.group1A.group1A_1/))[0].click();
|
||||||
|
|
||||||
expect(await findByTestId(/nav-item-group1.group1A.group1A_1.item1/)).toBeVisible();
|
|
||||||
|
|
||||||
expect(onProjectNavigationChange).toHaveBeenCalled();
|
expect(onProjectNavigationChange).toHaveBeenCalled();
|
||||||
const lastCall =
|
const lastCall =
|
||||||
|
@ -120,7 +112,6 @@ describe('<DefaultNavigation />', () => {
|
||||||
"group1",
|
"group1",
|
||||||
"item1",
|
"item1",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Item 1",
|
"title": "Item 1",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
@ -133,7 +124,6 @@ describe('<DefaultNavigation />', () => {
|
||||||
"group1",
|
"group1",
|
||||||
"item2",
|
"item2",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Item 2",
|
"title": "Item 2",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
@ -149,7 +139,6 @@ describe('<DefaultNavigation />', () => {
|
||||||
"group1A",
|
"group1A",
|
||||||
"item1",
|
"item1",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Group 1A Item 1",
|
"title": "Group 1A Item 1",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
@ -166,7 +155,6 @@ describe('<DefaultNavigation />', () => {
|
||||||
"group1A_1",
|
"group1A_1",
|
||||||
"item1",
|
"item1",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Group 1A_1 Item 1",
|
"title": "Group 1A_1 Item 1",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -291,7 +279,6 @@ describe('<DefaultNavigation />', () => {
|
||||||
"group1",
|
"group1",
|
||||||
"item1",
|
"item1",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Title from deeplink",
|
"title": "Title from deeplink",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
@ -311,7 +298,6 @@ describe('<DefaultNavigation />', () => {
|
||||||
"group1",
|
"group1",
|
||||||
"item2",
|
"item2",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Overwrite deeplink title",
|
"title": "Overwrite deeplink title",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -394,7 +380,6 @@ describe('<DefaultNavigation />', () => {
|
||||||
"group1",
|
"group1",
|
||||||
"item1",
|
"item1",
|
||||||
],
|
],
|
||||||
"renderItem": undefined,
|
|
||||||
"title": "Absolute link",
|
"title": "Absolute link",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -556,11 +541,11 @@ describe('<DefaultNavigation />', () => {
|
||||||
jest.advanceTimersByTime(SET_NAVIGATION_DELAY);
|
jest.advanceTimersByTime(SET_NAVIGATION_DELAY);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(await findByTestId(/nav-item-group1.item1/)).toHaveClass(
|
expect((await findByTestId(/nav-item-group1.item1/)).dataset.testSubj).toMatch(
|
||||||
'euiSideNavItemButton-isSelected'
|
/nav-item-isActive/
|
||||||
);
|
);
|
||||||
expect(await findByTestId(/nav-item-group1.item2/)).not.toHaveClass(
|
expect((await findByTestId(/nav-item-group1.item2/)).dataset.testSubj).not.toMatch(
|
||||||
'euiSideNavItemButton-isSelected'
|
/nav-item-isActive/
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -619,8 +604,8 @@ describe('<DefaultNavigation />', () => {
|
||||||
jest.advanceTimersByTime(SET_NAVIGATION_DELAY);
|
jest.advanceTimersByTime(SET_NAVIGATION_DELAY);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(await findByTestId(/nav-item-group1.item1/)).toHaveClass(
|
expect((await findByTestId(/nav-item-group1.item1/)).dataset.testSubj).toMatch(
|
||||||
'euiSideNavItemButton-isSelected'
|
/nav-item-isActive/
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -703,17 +688,12 @@ describe('<DefaultNavigation />', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await (
|
(await findByTestId(/nav-item-project_settings_project_nav.cloudLinkUserAndRoles/))
|
||||||
await findByTestId(
|
.textContent
|
||||||
/nav-item-project_settings_project_nav.settings.cloudLinkUserAndRoles/
|
|
||||||
)
|
|
||||||
).textContent
|
|
||||||
).toBe('Mock Users & RolesExternal link');
|
).toBe('Mock Users & RolesExternal link');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await (
|
(await findByTestId(/nav-item-project_settings_project_nav.cloudLinkBilling/)).textContent
|
||||||
await findByTestId(/nav-item-project_settings_project_nav.settings.cloudLinkBilling/)
|
|
||||||
).textContent
|
|
||||||
).toBe('Mock Billing & SubscriptionsExternal link');
|
).toBe('Mock Billing & SubscriptionsExternal link');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -72,23 +72,18 @@ const getDefaultNavigationTree = (
|
||||||
breadcrumbStatus: 'hidden',
|
breadcrumbStatus: 'hidden',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: 'settings',
|
link: 'management',
|
||||||
children: [
|
title: i18n.translate('sharedUXPackages.chrome.sideNavigation.mngt', {
|
||||||
{
|
defaultMessage: 'Management',
|
||||||
link: 'management',
|
}),
|
||||||
title: i18n.translate('sharedUXPackages.chrome.sideNavigation.mngt', {
|
},
|
||||||
defaultMessage: 'Management',
|
{
|
||||||
}),
|
id: 'cloudLinkUserAndRoles',
|
||||||
},
|
cloudLink: 'userAndRoles',
|
||||||
{
|
},
|
||||||
id: 'cloudLinkUserAndRoles',
|
{
|
||||||
cloudLink: 'userAndRoles',
|
id: 'cloudLinkBilling',
|
||||||
},
|
cloudLink: 'billingAndSub',
|
||||||
{
|
|
||||||
id: 'cloudLinkBilling',
|
|
||||||
cloudLink: 'billingAndSub',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,13 +19,7 @@ import { CloudLinks } from '../../cloud_links';
|
||||||
import { useNavigation as useNavigationServices } from '../../services';
|
import { useNavigation as useNavigationServices } from '../../services';
|
||||||
import { isAbsoluteLink } from '../../utils';
|
import { isAbsoluteLink } from '../../utils';
|
||||||
import { useNavigation } from '../components/navigation';
|
import { useNavigation } from '../components/navigation';
|
||||||
import {
|
import { NodeProps, NodePropsEnhanced, RegisterFunction, UnRegisterFunction } from '../types';
|
||||||
ChromeProjectNavigationNodeEnhanced,
|
|
||||||
NodeProps,
|
|
||||||
NodePropsEnhanced,
|
|
||||||
RegisterFunction,
|
|
||||||
UnRegisterFunction,
|
|
||||||
} from '../types';
|
|
||||||
import { useRegisterTreeNode } from './use_register_tree_node';
|
import { useRegisterTreeNode } from './use_register_tree_node';
|
||||||
|
|
||||||
function getIdFromNavigationNode<
|
function getIdFromNavigationNode<
|
||||||
|
@ -135,7 +129,7 @@ function createInternalNavNode<
|
||||||
path: string[] | null,
|
path: string[] | null,
|
||||||
isActive: boolean,
|
isActive: boolean,
|
||||||
{ cloudLinks }: { cloudLinks: CloudLinks }
|
{ cloudLinks }: { cloudLinks: CloudLinks }
|
||||||
): ChromeProjectNavigationNodeEnhanced | null {
|
): ChromeProjectNavigationNode | null {
|
||||||
validateNodeProps(_navNode);
|
validateNodeProps(_navNode);
|
||||||
|
|
||||||
const { children, link, cloudLink, ...navNode } = _navNode;
|
const { children, link, cloudLink, ...navNode } = _navNode;
|
||||||
|
@ -185,9 +179,9 @@ export const useInitNavNode = <
|
||||||
/**
|
/**
|
||||||
* Map of children nodes
|
* Map of children nodes
|
||||||
*/
|
*/
|
||||||
const [childrenNodes, setChildrenNodes] = useState<
|
const [childrenNodes, setChildrenNodes] = useState<Record<string, ChromeProjectNavigationNode>>(
|
||||||
Record<string, ChromeProjectNavigationNodeEnhanced>
|
{}
|
||||||
>({});
|
);
|
||||||
|
|
||||||
const isMounted = useRef(false);
|
const isMounted = useRef(false);
|
||||||
|
|
||||||
|
|
|
@ -6,84 +6,61 @@
|
||||||
* Side Public License, v 1.
|
* 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 { 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 {
|
import {
|
||||||
EuiButton,
|
EuiButton,
|
||||||
EuiButtonIcon,
|
EuiCollapsibleNavBeta,
|
||||||
EuiCollapsibleNav,
|
EuiCollapsibleNavBetaProps,
|
||||||
EuiFlexGroup,
|
EuiFlexGroup,
|
||||||
EuiFlexItem,
|
EuiFlexItem,
|
||||||
|
EuiHeader,
|
||||||
|
EuiHeaderSection,
|
||||||
EuiLink,
|
EuiLink,
|
||||||
|
EuiPageTemplate,
|
||||||
EuiText,
|
EuiText,
|
||||||
EuiThemeProvider,
|
|
||||||
EuiTitle,
|
EuiTitle,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { css } from '@emotion/react';
|
|
||||||
|
import type { ChromeNavLink, ChromeProjectNavigationNode } from '@kbn/core-chrome-browser';
|
||||||
import { NavigationStorybookMock, navLinksMock } from '../../mocks';
|
import { NavigationStorybookMock, navLinksMock } from '../../mocks';
|
||||||
import mdx from '../../README.mdx';
|
import mdx from '../../README.mdx';
|
||||||
import { NavigationProvider } from '../services';
|
|
||||||
import { DefaultNavigation } from './default_navigation';
|
|
||||||
import type { NavigationServices } from '../../types';
|
import type { NavigationServices } from '../../types';
|
||||||
|
import { NavigationProvider } from '../services';
|
||||||
import { Navigation } from './components';
|
import { Navigation } from './components';
|
||||||
import type { NonEmptyArray, ProjectNavigationDefinition } from './types';
|
import { DefaultNavigation } from './default_navigation';
|
||||||
import { getPresets } from './nav_tree_presets';
|
import { getPresets } from './nav_tree_presets';
|
||||||
|
import type { GroupDefinition, NonEmptyArray, ProjectNavigationDefinition } from './types';
|
||||||
|
|
||||||
const storybookMock = new NavigationStorybookMock();
|
const storybookMock = new NavigationStorybookMock();
|
||||||
|
|
||||||
const SIZE_OPEN = 248;
|
const NavigationWrapper: FC<
|
||||||
const SIZE_CLOSED = 40;
|
PropsWithChildren<{ clickAction?: EventHandler<MouseEvent>; clickActionText?: string }> &
|
||||||
|
Partial<EuiCollapsibleNavBetaProps>
|
||||||
const NavigationWrapper: FC = ({ children }) => {
|
> = (props) => {
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiThemeProvider>
|
<>
|
||||||
<EuiCollapsibleNav
|
<EuiHeader position="fixed">
|
||||||
css={collabsibleNavCSS}
|
<EuiHeaderSection side={props?.side}>
|
||||||
isOpen={true}
|
<EuiCollapsibleNavBeta {...props} />
|
||||||
showButtonIfDocked={true}
|
</EuiHeaderSection>
|
||||||
onClose={toggleOpen}
|
</EuiHeader>
|
||||||
isDocked={true}
|
<EuiPageTemplate>
|
||||||
size={isOpen ? SIZE_OPEN : SIZE_CLOSED}
|
<EuiPageTemplate.Section>
|
||||||
hideCloseButton={false}
|
{props.clickAction ? (
|
||||||
button={<CollapseButton />}
|
<EuiButton color="text" onClick={props.clickAction}>
|
||||||
>
|
{props.clickActionText ?? 'Click me'}
|
||||||
{isOpen && children}
|
</EuiButton>
|
||||||
</EuiCollapsibleNav>
|
) : (
|
||||||
</EuiThemeProvider>
|
<p>Hello world</p>
|
||||||
|
)}
|
||||||
|
</EuiPageTemplate.Section>
|
||||||
|
</EuiPageTemplate>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -123,30 +100,25 @@ const simpleNavigationDefinition: ProjectNavigationDefinition = {
|
||||||
defaultIsCollapsed: false,
|
defaultIsCollapsed: false,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: 'root',
|
id: 'item1',
|
||||||
children: [
|
title: 'Get started',
|
||||||
{
|
},
|
||||||
id: 'item1',
|
{
|
||||||
title: 'Get started',
|
id: 'item2',
|
||||||
},
|
title: 'Alerts',
|
||||||
{
|
},
|
||||||
id: 'item2',
|
{
|
||||||
title: 'Alerts',
|
id: 'item3',
|
||||||
},
|
title: 'Dashboards',
|
||||||
{
|
},
|
||||||
id: 'item3',
|
{
|
||||||
title: 'Dashboards',
|
id: 'item4',
|
||||||
},
|
title: 'External link',
|
||||||
{
|
href: 'https://elastic.co',
|
||||||
id: 'item4',
|
},
|
||||||
title: 'External link',
|
{
|
||||||
href: 'https://elastic.co',
|
id: 'item5',
|
||||||
},
|
title: 'Another link',
|
||||||
{
|
|
||||||
id: 'item5',
|
|
||||||
title: 'Another link',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'group:settings',
|
id: 'group:settings',
|
||||||
|
@ -205,21 +177,16 @@ const navigationDefinition: ProjectNavigationDefinition = {
|
||||||
defaultIsCollapsed: false,
|
defaultIsCollapsed: false,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: 'root',
|
id: 'item1',
|
||||||
children: [
|
title: 'Get started',
|
||||||
{
|
},
|
||||||
id: 'item1',
|
{
|
||||||
title: 'Get started',
|
id: 'item2',
|
||||||
},
|
title: 'Alerts',
|
||||||
{
|
},
|
||||||
id: 'item2',
|
{
|
||||||
title: 'Alerts',
|
id: 'item3',
|
||||||
},
|
title: 'Some other node',
|
||||||
{
|
|
||||||
id: 'item3',
|
|
||||||
title: 'Some other node',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'group:settings',
|
id: 'group:settings',
|
||||||
|
@ -333,24 +300,22 @@ export const WithUIComponents = (args: NavigationServices) => {
|
||||||
icon="logoObservability"
|
icon="logoObservability"
|
||||||
defaultIsCollapsed={false}
|
defaultIsCollapsed={false}
|
||||||
>
|
>
|
||||||
<Navigation.Group id="root">
|
<Navigation.Item<any> id="item1" link="item1" />
|
||||||
<Navigation.Item<any> id="item1" link="item1" />
|
<Navigation.Item id="item2" title="Alerts">
|
||||||
<Navigation.Item id="item2" title="Alerts">
|
{(navNode) => {
|
||||||
{(navNode) => {
|
return (
|
||||||
return (
|
<div className="euiSideNavItemButton">
|
||||||
<div className="euiSideNavItemButton">
|
<EuiText size="s">{`Render prop: ${navNode.id} - ${navNode.title}`}</EuiText>
|
||||||
<EuiText size="s">{`Render prop: ${navNode.id} - ${navNode.title}`}</EuiText>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}}
|
||||||
}}
|
</Navigation.Item>
|
||||||
</Navigation.Item>
|
<Navigation.Item id="item3" title="Title in ReactNode">
|
||||||
<Navigation.Item id="item3" title="Title in ReactNode">
|
<div className="euiSideNavItemButton">
|
||||||
<div className="euiSideNavItemButton">
|
<EuiLink>Title in ReactNode</EuiLink>
|
||||||
<EuiLink>Title in ReactNode</EuiLink>
|
</div>
|
||||||
</div>
|
</Navigation.Item>
|
||||||
</Navigation.Item>
|
<Navigation.Item id="item4" title="External link" href="https://elastic.co" />
|
||||||
<Navigation.Item id="item4" title="External link" href="https://elastic.co" />
|
|
||||||
</Navigation.Group>
|
|
||||||
|
|
||||||
<Navigation.Group id="group:settings" title="Settings">
|
<Navigation.Group id="group:settings" title="Settings">
|
||||||
<Navigation.Item id="logs" title="Logs" />
|
<Navigation.Item id="logs" title="Logs" />
|
||||||
|
@ -370,12 +335,10 @@ export const WithUIComponents = (args: NavigationServices) => {
|
||||||
breadcrumbStatus="hidden"
|
breadcrumbStatus="hidden"
|
||||||
icon="gear"
|
icon="gear"
|
||||||
>
|
>
|
||||||
<Navigation.Group id="settings">
|
<Navigation.Item link="management" title="Management" />
|
||||||
<Navigation.Item link="management" title="Management" />
|
<Navigation.Item id="cloudLinkUserAndRoles" cloudLink="userAndRoles" />
|
||||||
<Navigation.Item id="cloudLinkUserAndRoles" cloudLink="userAndRoles" />
|
<Navigation.Item id="cloudLinkPerformance" cloudLink="performance" />
|
||||||
<Navigation.Item id="cloudLinkPerformance" cloudLink="performance" />
|
<Navigation.Item id="cloudLinkBilling" cloudLink="billingAndSub" />
|
||||||
<Navigation.Item id="cloudLinkBilling" cloudLink="billingAndSub" />
|
|
||||||
</Navigation.Group>
|
|
||||||
</Navigation.Group>
|
</Navigation.Group>
|
||||||
</Navigation.Footer>
|
</Navigation.Footer>
|
||||||
</Navigation>
|
</Navigation>
|
||||||
|
@ -551,3 +514,121 @@ export const CreativeUI = (args: NavigationServices) => {
|
||||||
</NavigationWrapper>
|
</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.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ReactElement, ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import type { EuiAccordionProps } from '@elastic/eui';
|
||||||
import type {
|
import type {
|
||||||
AppDeepLinkId,
|
AppDeepLinkId,
|
||||||
ChromeProjectNavigationNode,
|
ChromeProjectNavigationNode,
|
||||||
NodeDefinition,
|
NodeDefinition,
|
||||||
} from '@kbn/core-chrome-browser';
|
} from '@kbn/core-chrome-browser';
|
||||||
|
|
||||||
import type { RecentlyAccessedProps } from './components';
|
import type { RecentlyAccessedProps } from './components';
|
||||||
|
|
||||||
export type NonEmptyArray<T> = [T, ...T[]];
|
export type NonEmptyArray<T> = [T, ...T[]];
|
||||||
|
@ -45,11 +46,6 @@ export interface NodePropsEnhanced<
|
||||||
Id extends string = string,
|
Id extends string = string,
|
||||||
ChildrenId extends string = Id
|
ChildrenId extends string = Id
|
||||||
> extends NodeProps<LinkId, Id, ChildrenId> {
|
> 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
|
* 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.
|
* even if the URL does not match any of the nodes in the group.
|
||||||
|
@ -57,17 +53,6 @@ export interface NodePropsEnhanced<
|
||||||
isActive?: boolean;
|
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 */
|
/** The preset that can be pass to the NavigationBucket component */
|
||||||
export type NavigationGroupPreset = 'analytics' | 'devtools' | 'ml' | 'management';
|
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.
|
* `true`: the group will be collapsed event if any of its children nodes matches the current URL.
|
||||||
*/
|
*/
|
||||||
defaultIsCollapsed?: boolean;
|
defaultIsCollapsed?: boolean;
|
||||||
|
/*
|
||||||
|
* Pass props to the EUI accordion component used to represent a nav group
|
||||||
|
*/
|
||||||
|
accordionProps?: Partial<EuiAccordionProps>;
|
||||||
preset?: NavigationGroupPreset;
|
preset?: NavigationGroupPreset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,7 +161,7 @@ export type UnRegisterFunction = (id: string) => void;
|
||||||
*
|
*
|
||||||
* A function to register a navigation node on its parent.
|
* 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. */
|
/** The function to unregister the node. */
|
||||||
unregister: UnRegisterFunction;
|
unregister: UnRegisterFunction;
|
||||||
/** The full path of the node in the navigation tree. */
|
/** The full path of the node in the navigation tree. */
|
||||||
|
|
|
@ -25,135 +25,134 @@ const navigationTree: NavigationTreeDefinition = {
|
||||||
title: 'Observability',
|
title: 'Observability',
|
||||||
icon: 'logoObservability',
|
icon: 'logoObservability',
|
||||||
defaultIsCollapsed: false,
|
defaultIsCollapsed: false,
|
||||||
|
accordionProps: {
|
||||||
|
arrowProps: { css: { display: 'none' } },
|
||||||
|
},
|
||||||
breadcrumbStatus: 'hidden',
|
breadcrumbStatus: 'hidden',
|
||||||
children: [
|
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: [
|
children: [
|
||||||
{
|
{
|
||||||
title: i18n.translate('xpack.serverlessObservability.nav.logExplorer', {
|
title: i18n.translate('xpack.serverlessObservability.nav.ml.jobs', {
|
||||||
defaultMessage: 'Log Explorer',
|
defaultMessage: 'Anomaly detection',
|
||||||
}),
|
}),
|
||||||
link: 'observability-log-explorer',
|
link: 'ml:anomalyDetection',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n.translate('xpack.serverlessObservability.nav.dashboards', {
|
title: i18n.translate('xpack.serverlessObservability.ml.logRateAnalysis', {
|
||||||
defaultMessage: 'Dashboards',
|
defaultMessage: 'Log rate analysis',
|
||||||
}),
|
}),
|
||||||
link: 'dashboards',
|
link: 'ml:logRateAnalysis',
|
||||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||||
return pathNameSerialized.startsWith(prepend('/app/dashboards'));
|
return pathNameSerialized.includes(prepend('/app/ml/aiops/log_rate_analysis'));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
link: 'observability-overview:alerts',
|
title: i18n.translate('xpack.serverlessObservability.ml.changePointDetection', {
|
||||||
},
|
defaultMessage: 'Change point detection',
|
||||||
{
|
|
||||||
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',
|
|
||||||
}),
|
}),
|
||||||
children: [
|
link: 'ml:changePointDetections',
|
||||||
{
|
|
||||||
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',
|
|
||||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||||
return (
|
return pathNameSerialized.includes(prepend('/app/ml/aiops/change_point_detection'));
|
||||||
pathNameSerialized.startsWith(prepend('/app/visualize')) ||
|
},
|
||||||
pathNameSerialized.startsWith(prepend('/app/lens')) ||
|
},
|
||||||
pathNameSerialized.startsWith(prepend('/app/maps'))
|
{
|
||||||
);
|
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',
|
id: 'groups-spacer-2',
|
||||||
children: [
|
isGroupTitle: true,
|
||||||
{
|
},
|
||||||
title: i18n.translate('xpack.serverlessObservability.nav.getStarted', {
|
{
|
||||||
defaultMessage: 'Add data',
|
link: 'observability-overview:cases',
|
||||||
}),
|
},
|
||||||
link: 'observabilityOnboarding',
|
{
|
||||||
},
|
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',
|
breadcrumbStatus: 'hidden',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: 'settings',
|
link: 'management',
|
||||||
children: [
|
title: i18n.translate('xpack.serverlessObservability.nav.mngt', {
|
||||||
{
|
defaultMessage: 'Management',
|
||||||
link: 'management',
|
}),
|
||||||
title: i18n.translate('xpack.serverlessObservability.nav.mngt', {
|
},
|
||||||
defaultMessage: 'Management',
|
{
|
||||||
}),
|
link: 'integrations',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
link: 'integrations',
|
link: 'fleet',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
link: 'fleet',
|
id: 'cloudLinkUserAndRoles',
|
||||||
},
|
cloudLink: 'userAndRoles',
|
||||||
{
|
},
|
||||||
id: 'cloudLinkUserAndRoles',
|
{
|
||||||
cloudLink: 'userAndRoles',
|
id: 'cloudLinkBilling',
|
||||||
},
|
cloudLink: 'billingAndSub',
|
||||||
{
|
|
||||||
id: 'cloudLinkBilling',
|
|
||||||
cloudLink: 'billingAndSub',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -25,6 +25,9 @@ const navigationTree: NavigationTreeDefinition = {
|
||||||
title: 'Elasticsearch',
|
title: 'Elasticsearch',
|
||||||
icon: 'logoElasticsearch',
|
icon: 'logoElasticsearch',
|
||||||
defaultIsCollapsed: false,
|
defaultIsCollapsed: false,
|
||||||
|
accordionProps: {
|
||||||
|
arrowProps: { css: { display: 'none' } },
|
||||||
|
},
|
||||||
breadcrumbStatus: 'hidden',
|
breadcrumbStatus: 'hidden',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
@ -39,77 +42,75 @@ const navigationTree: NavigationTreeDefinition = {
|
||||||
title: i18n.translate('xpack.serverlessSearch.nav.devTools', {
|
title: i18n.translate('xpack.serverlessSearch.nav.devTools', {
|
||||||
defaultMessage: 'Dev Tools',
|
defaultMessage: 'Dev Tools',
|
||||||
}),
|
}),
|
||||||
children: [{ link: 'dev_tools:console' }, { link: 'dev_tools:searchprofiler' }],
|
isGroupTitle: true,
|
||||||
},
|
},
|
||||||
|
{ link: 'dev_tools:console' },
|
||||||
|
{ link: 'dev_tools:searchprofiler' },
|
||||||
{
|
{
|
||||||
id: 'explore',
|
id: 'explore',
|
||||||
title: i18n.translate('xpack.serverlessSearch.nav.explore', {
|
title: i18n.translate('xpack.serverlessSearch.nav.explore', {
|
||||||
defaultMessage: 'Explore',
|
defaultMessage: 'Explore',
|
||||||
}),
|
}),
|
||||||
children: [
|
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',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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',
|
id: 'content',
|
||||||
title: i18n.translate('xpack.serverlessSearch.nav.content', {
|
title: i18n.translate('xpack.serverlessSearch.nav.content', {
|
||||||
defaultMessage: 'Content',
|
defaultMessage: 'Content',
|
||||||
}),
|
}),
|
||||||
children: [
|
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 */,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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',
|
id: 'security',
|
||||||
title: i18n.translate('xpack.serverlessSearch.nav.security', {
|
title: i18n.translate('xpack.serverlessSearch.nav.security', {
|
||||||
defaultMessage: 'Security',
|
defaultMessage: 'Security',
|
||||||
}),
|
}),
|
||||||
children: [
|
isGroupTitle: true,
|
||||||
{
|
},
|
||||||
link: 'management:api_keys',
|
{
|
||||||
breadcrumbStatus:
|
link: 'management:api_keys',
|
||||||
'hidden' /* management sub-pages set their breadcrumbs themselves */,
|
breadcrumbStatus: 'hidden' /* management sub-pages set their breadcrumbs themselves */,
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -125,30 +126,25 @@ const navigationTree: NavigationTreeDefinition = {
|
||||||
breadcrumbStatus: 'hidden',
|
breadcrumbStatus: 'hidden',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: 'settings',
|
link: 'management',
|
||||||
children: [
|
title: i18n.translate('xpack.serverlessSearch.nav.mngt', {
|
||||||
{
|
defaultMessage: 'Management',
|
||||||
link: 'management',
|
}),
|
||||||
title: i18n.translate('xpack.serverlessSearch.nav.mngt', {
|
},
|
||||||
defaultMessage: 'Management',
|
{
|
||||||
}),
|
id: 'cloudLinkDeployment',
|
||||||
},
|
cloudLink: 'deployment',
|
||||||
{
|
title: i18n.translate('xpack.serverlessSearch.nav.performance', {
|
||||||
id: 'cloudLinkDeployment',
|
defaultMessage: 'Performance',
|
||||||
cloudLink: 'deployment',
|
}),
|
||||||
title: i18n.translate('xpack.serverlessSearch.nav.performance', {
|
},
|
||||||
defaultMessage: 'Performance',
|
{
|
||||||
}),
|
id: 'cloudLinkUserAndRoles',
|
||||||
},
|
cloudLink: 'userAndRoles',
|
||||||
{
|
},
|
||||||
id: 'cloudLinkUserAndRoles',
|
{
|
||||||
cloudLink: 'userAndRoles',
|
id: 'cloudLinkBilling',
|
||||||
},
|
cloudLink: 'billingAndSub',
|
||||||
{
|
|
||||||
id: 'cloudLinkBilling',
|
|
||||||
cloudLink: 'billingAndSub',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,6 +23,7 @@ export function SvlCommonNavigationProvider(ctx: FtrProviderContext) {
|
||||||
const testSubjects = ctx.getService('testSubjects');
|
const testSubjects = ctx.getService('testSubjects');
|
||||||
const browser = ctx.getService('browser');
|
const browser = ctx.getService('browser');
|
||||||
const retry = ctx.getService('retry');
|
const retry = ctx.getService('retry');
|
||||||
|
const log = ctx.getService('log');
|
||||||
|
|
||||||
async function getByVisibleText(
|
async function getByVisibleText(
|
||||||
selector: string | (() => Promise<WebElementWrapper[]>),
|
selector: string | (() => Promise<WebElementWrapper[]>),
|
||||||
|
@ -93,16 +94,20 @@ export function SvlCommonNavigationProvider(ctx: FtrProviderContext) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async expectSectionExists(sectionId: NavigationId) {
|
async expectSectionExists(sectionId: NavigationId) {
|
||||||
|
log.debug('ServerlessCommonNavigation.sidenav.expectSectionExists', sectionId);
|
||||||
await testSubjects.existOrFail(`~nav-bucket-${sectionId}`);
|
await testSubjects.existOrFail(`~nav-bucket-${sectionId}`);
|
||||||
},
|
},
|
||||||
async isSectionOpen(sectionId: NavigationId) {
|
async isSectionOpen(sectionId: NavigationId) {
|
||||||
await this.expectSectionExists(sectionId);
|
await this.expectSectionExists(sectionId);
|
||||||
const section = await testSubjects.find(`~nav-bucket-${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');
|
const isExpanded = await collapseBtn.getAttribute('aria-expanded');
|
||||||
return isExpanded === 'true';
|
return isExpanded === 'true';
|
||||||
},
|
},
|
||||||
async expectSectionOpen(sectionId: NavigationId) {
|
async expectSectionOpen(sectionId: NavigationId) {
|
||||||
|
log.debug('ServerlessCommonNavigation.sidenav.expectSectionOpen', sectionId);
|
||||||
await this.expectSectionExists(sectionId);
|
await this.expectSectionExists(sectionId);
|
||||||
await retry.waitFor(`section ${sectionId} to be open`, async () => {
|
await retry.waitFor(`section ${sectionId} to be open`, async () => {
|
||||||
const isOpen = await this.isSectionOpen(sectionId);
|
const isOpen = await this.isSectionOpen(sectionId);
|
||||||
|
@ -117,11 +122,14 @@ export function SvlCommonNavigationProvider(ctx: FtrProviderContext) {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async openSection(sectionId: NavigationId) {
|
async openSection(sectionId: NavigationId) {
|
||||||
|
log.debug('ServerlessCommonNavigation.sidenav.openSection', sectionId);
|
||||||
await this.expectSectionExists(sectionId);
|
await this.expectSectionExists(sectionId);
|
||||||
const isOpen = await this.isSectionOpen(sectionId);
|
const isOpen = await this.isSectionOpen(sectionId);
|
||||||
if (isOpen) return;
|
if (isOpen) return;
|
||||||
const section = await testSubjects.find(`~nav-bucket-${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]`
|
||||||
|
);
|
||||||
await collapseBtn.click();
|
await collapseBtn.click();
|
||||||
await this.expectSectionOpen(sectionId);
|
await this.expectSectionOpen(sectionId);
|
||||||
},
|
},
|
||||||
|
@ -130,7 +138,9 @@ export function SvlCommonNavigationProvider(ctx: FtrProviderContext) {
|
||||||
const isOpen = await this.isSectionOpen(sectionId);
|
const isOpen = await this.isSectionOpen(sectionId);
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
const section = await testSubjects.find(`~nav-bucket-${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]`
|
||||||
|
);
|
||||||
await collapseBtn.click();
|
await collapseBtn.click();
|
||||||
await this.expectSectionClosed(sectionId);
|
await this.expectSectionClosed(sectionId);
|
||||||
},
|
},
|
||||||
|
@ -143,6 +153,10 @@ export function SvlCommonNavigationProvider(ctx: FtrProviderContext) {
|
||||||
await testSubjects.click('~breadcrumb-home');
|
await testSubjects.click('~breadcrumb-home');
|
||||||
},
|
},
|
||||||
async expectBreadcrumbExists(by: { deepLinkId: AppDeepLinkId } | { text: string }) {
|
async expectBreadcrumbExists(by: { deepLinkId: AppDeepLinkId } | { text: string }) {
|
||||||
|
log.debug(
|
||||||
|
'ServerlessCommonNavigation.breadcrumbs.expectBreadcrumbExists',
|
||||||
|
JSON.stringify(by)
|
||||||
|
);
|
||||||
if ('deepLinkId' in by) {
|
if ('deepLinkId' in by) {
|
||||||
await testSubjects.existOrFail(`~breadcrumb-deepLinkId-${by.deepLinkId}`);
|
await testSubjects.existOrFail(`~breadcrumb-deepLinkId-${by.deepLinkId}`);
|
||||||
} else {
|
} else {
|
||||||
|
@ -161,6 +175,10 @@ export function SvlCommonNavigationProvider(ctx: FtrProviderContext) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async expectBreadcrumbTexts(expectedBreadcrumbTexts: string[]) {
|
async expectBreadcrumbTexts(expectedBreadcrumbTexts: string[]) {
|
||||||
|
log.debug(
|
||||||
|
'ServerlessCommonNavigation.breadcrumbs.expectBreadcrumbTexts',
|
||||||
|
JSON.stringify(expectedBreadcrumbTexts)
|
||||||
|
);
|
||||||
await retry.try(async () => {
|
await retry.try(async () => {
|
||||||
const breadcrumbsContainer = await testSubjects.find('breadcrumbs');
|
const breadcrumbsContainer = await testSubjects.find('breadcrumbs');
|
||||||
const breadcrumbs = await breadcrumbsContainer.findAllByTestSubject('~breadcrumb');
|
const breadcrumbs = await breadcrumbsContainer.findAllByTestSubject('~breadcrumb');
|
||||||
|
|
|
@ -48,9 +48,8 @@ export default function ({ getPageObject, getService }: FtrProviderContext) {
|
||||||
// navigate to discover
|
// navigate to discover
|
||||||
await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'discover' });
|
await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'discover' });
|
||||||
await svlCommonNavigation.sidenav.expectLinkActive({ deepLinkId: 'discover' });
|
await svlCommonNavigation.sidenav.expectLinkActive({ deepLinkId: 'discover' });
|
||||||
await svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({ text: `Explore` });
|
|
||||||
await svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({ deepLinkId: 'discover' });
|
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
|
// navigate to a different section
|
||||||
await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'management:index_management' });
|
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 () => {
|
it("management apps from the sidenav hide the 'stack management' root from the breadcrumbs", async () => {
|
||||||
await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'management:triggersActions' });
|
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.sidenav.clickLink({ deepLinkId: 'management:index_management' });
|
||||||
await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts([
|
await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts(['Index Management', 'Indices']);
|
||||||
'Content',
|
|
||||||
'Index Management',
|
|
||||||
'Indices',
|
|
||||||
]);
|
|
||||||
|
|
||||||
await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'management:ingest_pipelines' });
|
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.sidenav.clickLink({ deepLinkId: 'management:api_keys' });
|
||||||
await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts(['Security', 'API keys']);
|
await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts(['API keys']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('navigate management', async () => {
|
it('navigate management', async () => {
|
||||||
|
@ -104,7 +99,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) {
|
||||||
await svlCommonNavigation.search.clickOnOption(0);
|
await svlCommonNavigation.search.clickOnOption(0);
|
||||||
await svlCommonNavigation.search.hideSearch();
|
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 () => {
|
it('does not show cases in sidebar navigation', async () => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue