[Serverless nav] Handle groups and vertical space (#169251)

This commit is contained in:
Sébastien Loix 2023-10-23 16:18:58 +01:00 committed by GitHub
parent 3156d8b9e3
commit 33fe17e56b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 690 additions and 336 deletions

View file

@ -43,4 +43,5 @@ export type {
NodeDefinition,
NodeDefinitionWithChildren,
NodeRenderAs,
EuiThemeSize,
} from './src';

View file

@ -42,4 +42,5 @@ export type {
NodeDefinition,
NodeDefinitionWithChildren,
RenderAs as NodeRenderAs,
EuiThemeSize,
} from './project_navigation';

View file

@ -8,7 +8,7 @@
import type { ComponentType } from 'react';
import type { Location } from 'history';
import { EuiAccordionProps, IconType } from '@elastic/eui';
import type { EuiAccordionProps, EuiThemeSizes, IconType } from '@elastic/eui';
import type { AppId as DevToolsApp, DeepLinkId as DevToolsLink } from '@kbn/deeplinks-devtools';
import type {
AppId as AnalyticsApp,
@ -53,6 +53,8 @@ export type SideNavNodeStatus = 'hidden' | 'visible';
export type RenderAs = 'block' | 'accordion' | 'panelOpener' | 'item';
export type EuiThemeSize = Exclude<typeof EuiThemeSizes[number], 'base' | 'xxs' | 'xxxl' | 'xxxxl'>;
export type GetIsActiveFn = (params: {
/** The current path name including the basePath + hash value but **without** any query params */
pathNameSerialized: string;
@ -93,16 +95,15 @@ interface NodeDefinitionBase {
* Optional function to get the active state. This function is called whenever the location changes.
*/
getIsActive?: GetIsActiveFn;
/**
* Add vertical space before this node
*/
spaceBefore?: EuiThemeSize | null;
/**
* ----------------------------------------------------------------------------------------------
* ------------------------------- GROUP NODES ONLY PROPS ---------------------------------------
* ----------------------------------------------------------------------------------------------
*/
/**
* ["group" nodes only] Optional flag to indicate if the node must be treated as a group title.
* Can not be used with `children`
*/
isGroupTitle?: boolean;
/**
* ["group" nodes only] Property to indicate how the group should be rendered.
* - Accordion: wraps the items in an EuiAccordion

View file

@ -19,6 +19,7 @@ export const defaultNavigation: AnalyticsNodeDefinition = {
defaultMessage: 'Data exploration',
}),
icon: 'stats',
renderAs: 'accordion',
children: [
{
link: 'discover',

View file

@ -19,6 +19,7 @@ export const defaultNavigation: DevToolsNodeDefinition = {
}),
id: 'rootNav:devtools',
icon: 'editorCodeBlock',
renderAs: 'accordion',
children: [
{
link: 'dev_tools:console',

View file

@ -27,6 +27,7 @@ export const defaultNavigation: ManagementNodeDefinition = {
defaultMessage: 'Management',
}),
icon: 'gear',
renderAs: 'accordion',
children: [
{
link: 'monitoring',
@ -36,6 +37,7 @@ export const defaultNavigation: ManagementNodeDefinition = {
title: i18n.translate('defaultNavigation.management.integrationManagement', {
defaultMessage: 'Integration management',
}),
renderAs: 'accordion',
children: [
{
link: 'integrations',
@ -53,12 +55,14 @@ export const defaultNavigation: ManagementNodeDefinition = {
title: i18n.translate('defaultNavigation.management.stackManagement', {
defaultMessage: 'Stack management',
}),
renderAs: 'accordion',
children: [
{
id: 'ingest',
title: i18n.translate('defaultNavigation.management.ingest', {
defaultMessage: 'Ingest',
}),
renderAs: 'accordion',
children: [
{
link: 'management:ingest_pipelines',
@ -73,6 +77,7 @@ export const defaultNavigation: ManagementNodeDefinition = {
title: i18n.translate('defaultNavigation.management.stackManagementData', {
defaultMessage: 'Data',
}),
renderAs: 'accordion',
children: [
{
link: 'management:index_management',
@ -87,6 +92,7 @@ export const defaultNavigation: ManagementNodeDefinition = {
title: i18n.translate('defaultNavigation.management.alertAndInsights', {
defaultMessage: 'Alerts and insights',
}),
renderAs: 'accordion',
children: [
{
// Rules
@ -108,6 +114,7 @@ export const defaultNavigation: ManagementNodeDefinition = {
{
id: 'kibana',
title: 'Kibana',
renderAs: 'accordion',
children: [
{
link: 'management:dataViews',

View file

@ -38,6 +38,7 @@ export const defaultNavigation: MlNodeDefinition = {
defaultMessage: 'Anomaly Detection',
}),
id: 'anomaly_detection',
renderAs: 'accordion',
children: [
{
title: i18n.translate('defaultNavigation.ml.jobs', {
@ -61,6 +62,7 @@ export const defaultNavigation: MlNodeDefinition = {
title: i18n.translate('defaultNavigation.ml.dataFrameAnalytics', {
defaultMessage: 'Data Frame Analytics',
}),
renderAs: 'accordion',
children: [
{
title: 'Jobs',
@ -79,6 +81,7 @@ export const defaultNavigation: MlNodeDefinition = {
title: i18n.translate('defaultNavigation.ml.modelManagement', {
defaultMessage: 'Model Management',
}),
renderAs: 'accordion',
children: [
{
link: 'ml:nodesOverview',
@ -93,6 +96,7 @@ export const defaultNavigation: MlNodeDefinition = {
title: i18n.translate('defaultNavigation.ml.dataVisualizer', {
defaultMessage: 'Data Visualizer',
}),
renderAs: 'accordion',
children: [
{
title: i18n.translate('defaultNavigation.ml.file', {
@ -119,6 +123,7 @@ export const defaultNavigation: MlNodeDefinition = {
title: i18n.translate('defaultNavigation.ml.aiopsLabs', {
defaultMessage: 'AIOps labs',
}),
renderAs: 'accordion',
children: [
{
link: 'ml:logRateAnalysis',

View file

@ -143,6 +143,7 @@ Array [
"path": Array [
"rootNav:analytics",
],
"renderAs": "accordion",
"sideNavStatus": "visible",
"title": "Data exploration",
"type": "navGroup",
@ -285,6 +286,7 @@ Array [
"rootNav:ml",
"anomaly_detection",
],
"renderAs": "accordion",
"sideNavStatus": "visible",
"title": "Anomaly Detection",
},
@ -363,6 +365,7 @@ Array [
"rootNav:ml",
"data_frame_analytics",
],
"renderAs": "accordion",
"sideNavStatus": "visible",
"title": "Data Frame Analytics",
},
@ -420,6 +423,7 @@ Array [
"rootNav:ml",
"model_management",
],
"renderAs": "accordion",
"sideNavStatus": "visible",
"title": "Model Management",
},
@ -498,6 +502,7 @@ Array [
"rootNav:ml",
"data_visualizer",
],
"renderAs": "accordion",
"sideNavStatus": "visible",
"title": "Data Visualizer",
},
@ -576,6 +581,7 @@ Array [
"rootNav:ml",
"aiops_labs",
],
"renderAs": "accordion",
"sideNavStatus": "visible",
"title": "AIOps labs",
},

View file

@ -15,6 +15,8 @@ import { render } from '@testing-library/react';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { BehaviorSubject, of, type Observable } from 'rxjs';
import { EuiThemeProvider } from '@elastic/eui';
import { getServicesMock } from '../../../mocks/src/jest';
import { NavigationProvider } from '../../services';
import { Navigation } from './navigation';
@ -38,20 +40,32 @@ describe('<Navigation />', () => {
const onProjectNavigationChange = jest.fn();
const { findByTestId } = render(
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
<Navigation>
<Navigation.Group id="group1" defaultIsCollapsed={false}>
<Navigation.Item id="item1" title="Item 1" href="https://foo" />
<Navigation.Item id="item2" title="Item 2" href="https://foo" />
<Navigation.Group id="group1A" title="Group1A" defaultIsCollapsed={false}>
<Navigation.Item id="item1" title="Group 1A Item 1" href="https://foo" />
<Navigation.Group id="group1A_1" title="Group1A_1" defaultIsCollapsed={false}>
<Navigation.Item id="item1" title="Group 1A_1 Item 1" href="https://foo" />
<EuiThemeProvider>
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
<Navigation>
<Navigation.Group id="group1" renderAs="accordion" defaultIsCollapsed={false}>
<Navigation.Item id="item1" title="Item 1" href="https://foo" />
<Navigation.Item id="item2" title="Item 2" href="https://foo" />
<Navigation.Group
id="group1A"
renderAs="accordion"
title="Group1A"
defaultIsCollapsed={false}
>
<Navigation.Item id="item1" title="Group 1A Item 1" href="https://foo" />
<Navigation.Group
id="group1A_1"
renderAs="accordion"
title="Group1A_1"
defaultIsCollapsed={false}
>
<Navigation.Item id="item1" title="Group 1A_1 Item 1" href="https://foo" />
</Navigation.Group>
</Navigation.Group>
</Navigation.Group>
</Navigation.Group>
</Navigation>
</NavigationProvider>
</Navigation>
</NavigationProvider>
</EuiThemeProvider>
);
await act(async () => {
@ -152,6 +166,7 @@ describe('<Navigation />', () => {
"group1A",
"group1A_1",
],
"renderAs": "accordion",
"sideNavStatus": "visible",
"title": "Group1A_1",
},
@ -165,6 +180,7 @@ describe('<Navigation />', () => {
"group1",
"group1A",
],
"renderAs": "accordion",
"sideNavStatus": "visible",
"title": "Group1A",
},
@ -177,6 +193,7 @@ describe('<Navigation />', () => {
"path": Array [
"group1",
],
"renderAs": "accordion",
"sideNavStatus": "visible",
"title": "",
},
@ -198,23 +215,25 @@ describe('<Navigation />', () => {
const onProjectNavigationChange = jest.fn();
render(
<NavigationProvider
{...services}
navLinks$={navLinks$}
onProjectNavigationChange={onProjectNavigationChange}
>
<Navigation>
<Navigation.Group id="root">
<Navigation.Group id="group1">
{/* Title from deeplink */}
<Navigation.Item<any> id="item1" link="item1" />
<Navigation.Item<any> id="item2" link="item1" title="Overwrite deeplink title" />
<Navigation.Item id="item3" title="Title in props" />
<Navigation.Item id="item4">Title in children</Navigation.Item>
<EuiThemeProvider>
<NavigationProvider
{...services}
navLinks$={navLinks$}
onProjectNavigationChange={onProjectNavigationChange}
>
<Navigation>
<Navigation.Group id="root">
<Navigation.Group id="group1">
{/* Title from deeplink */}
<Navigation.Item<any> id="item1" link="item1" />
<Navigation.Item<any> id="item2" link="item1" title="Overwrite deeplink title" />
<Navigation.Item id="item3" title="Title in props" />
<Navigation.Item id="item4">Title in children</Navigation.Item>
</Navigation.Group>
</Navigation.Group>
</Navigation.Group>
</Navigation>
</NavigationProvider>
</Navigation>
</NavigationProvider>
</EuiThemeProvider>
);
await act(async () => {
@ -347,22 +366,28 @@ describe('<Navigation />', () => {
const onProjectNavigationChange = jest.fn();
const { findByTestId } = render(
<NavigationProvider
{...services}
navLinks$={navLinks$}
onProjectNavigationChange={onProjectNavigationChange}
>
<Navigation>
<Navigation.Group id="root" defaultIsCollapsed={false}>
<Navigation.Group id="group1" defaultIsCollapsed={false}>
{/* Title from deeplink */}
<Navigation.Item<any> id="item1" link="item1" />
{/* Should not appear */}
<Navigation.Item<any> id="unknownLink" link="unknown" title="Should NOT be there" />
<EuiThemeProvider>
<NavigationProvider
{...services}
navLinks$={navLinks$}
onProjectNavigationChange={onProjectNavigationChange}
>
<Navigation>
<Navigation.Group id="root" defaultIsCollapsed={false}>
<Navigation.Group id="group1" defaultIsCollapsed={false}>
{/* Title from deeplink */}
<Navigation.Item<any> id="item1" link="item1" />
{/* Should not appear */}
<Navigation.Item<any>
id="unknownLink"
link="unknown"
title="Should NOT be there"
/>
</Navigation.Group>
</Navigation.Group>
</Navigation.Group>
</Navigation>
</NavigationProvider>
</Navigation>
</NavigationProvider>
</EuiThemeProvider>
);
await act(async () => {
@ -446,22 +471,24 @@ describe('<Navigation />', () => {
const onProjectNavigationChange = jest.fn();
const { queryByTestId } = render(
<NavigationProvider
{...services}
navLinks$={navLinks$}
onProjectNavigationChange={onProjectNavigationChange}
>
<Navigation>
<Navigation.Group id="root" defaultIsCollapsed={false}>
<Navigation.Group id="group1" defaultIsCollapsed={false}>
<Navigation.Item<any> id="item1" link="notRegistered" />
<EuiThemeProvider>
<NavigationProvider
{...services}
navLinks$={navLinks$}
onProjectNavigationChange={onProjectNavigationChange}
>
<Navigation>
<Navigation.Group id="root" defaultIsCollapsed={false}>
<Navigation.Group id="group1" defaultIsCollapsed={false}>
<Navigation.Item<any> id="item1" link="notRegistered" />
</Navigation.Group>
<Navigation.Group id="group2" defaultIsCollapsed={false}>
<Navigation.Item<any> id="item1" link="item1" />
</Navigation.Group>
</Navigation.Group>
<Navigation.Group id="group2" defaultIsCollapsed={false}>
<Navigation.Item<any> id="item1" link="item1" />
</Navigation.Group>
</Navigation.Group>
</Navigation>
</NavigationProvider>
</Navigation>
</NavigationProvider>
</EuiThemeProvider>
);
await act(async () => {
@ -550,14 +577,16 @@ describe('<Navigation />', () => {
const onProjectNavigationChange = jest.fn();
render(
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
<Navigation>
<Navigation.Group preset="analytics" />
<Navigation.Group preset="ml" />
<Navigation.Group preset="devtools" />
<Navigation.Group preset="management" />
</Navigation>
</NavigationProvider>
<EuiThemeProvider>
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
<Navigation>
<Navigation.Group preset="analytics" />
<Navigation.Group preset="ml" />
<Navigation.Group preset="devtools" />
<Navigation.Group preset="management" />
</Navigation>
</NavigationProvider>
</EuiThemeProvider>
);
await act(async () => {
@ -581,15 +610,17 @@ describe('<Navigation />', () => {
]);
const { findByTestId } = render(
<NavigationProvider {...services} recentlyAccessed$={recentlyAccessed$}>
<Navigation>
<Navigation.Group id="root">
<Navigation.Group id="group1">
<Navigation.RecentlyAccessed />
<EuiThemeProvider>
<NavigationProvider {...services} recentlyAccessed$={recentlyAccessed$}>
<Navigation>
<Navigation.Group id="root">
<Navigation.Group id="group1">
<Navigation.RecentlyAccessed />
</Navigation.Group>
</Navigation.Group>
</Navigation.Group>
</Navigation>
</NavigationProvider>
</Navigation>
</NavigationProvider>
</EuiThemeProvider>
);
await act(async () => {
@ -606,13 +637,15 @@ describe('<Navigation />', () => {
const onProjectNavigationChange = jest.fn();
render(
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
<Navigation>
<Navigation.Group id="group1">
<Navigation.Item id="item1" title="Item 1" href="https://example.com" />
</Navigation.Group>
</Navigation>
</NavigationProvider>
<EuiThemeProvider>
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
<Navigation>
<Navigation.Group id="group1">
<Navigation.Item id="item1" title="Item 1" href="https://example.com" />
</Navigation.Group>
</Navigation>
</NavigationProvider>
</EuiThemeProvider>
);
await act(async () => {
@ -670,13 +703,15 @@ describe('<Navigation />', () => {
const expectToThrow = () => {
render(
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
<Navigation>
<Navigation.Group id="group1">
<Navigation.Item id="item1" title="Item 1" href="../dashboards" />
</Navigation.Group>
</Navigation>
</NavigationProvider>
<EuiThemeProvider>
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
<Navigation>
<Navigation.Group id="group1">
<Navigation.Item id="item1" title="Item 1" href="../dashboards" />
</Navigation.Group>
</Navigation>
</NavigationProvider>
</EuiThemeProvider>
);
};
@ -722,14 +757,16 @@ describe('<Navigation />', () => {
const getActiveNodes$ = () => activeNodes$;
const { findByTestId } = render(
<NavigationProvider {...services} activeNodes$={getActiveNodes$()} navLinks$={navLinks$}>
<Navigation>
<Navigation.Group id="group1">
<Navigation.Item<any> link="item1" title="Item 1" />
<Navigation.Item<any> link="item2" title="Item 2" />
</Navigation.Group>
</Navigation>
</NavigationProvider>
<EuiThemeProvider>
<NavigationProvider {...services} activeNodes$={getActiveNodes$()} navLinks$={navLinks$}>
<Navigation>
<Navigation.Group id="group1">
<Navigation.Item<any> link="item1" title="Item 1" />
<Navigation.Item<any> link="item2" title="Item 2" />
</Navigation.Group>
</Navigation>
</NavigationProvider>
</EuiThemeProvider>
);
expect((await findByTestId(/nav-item-group1.item1/)).dataset.testSubj).toMatch(
@ -791,24 +828,26 @@ describe('<Navigation />', () => {
};
const { findByTestId } = render(
<NavigationProvider
{...services}
activeNodes$={getActiveNodes$()}
navLinks$={navLinks$}
onProjectNavigationChange={onProjectNavigationChange}
>
<Navigation>
<Navigation.Group id="group1">
<Navigation.Item<any>
link="item1"
title="Item 1"
getIsActive={() => {
return true;
}}
/>
</Navigation.Group>
</Navigation>
</NavigationProvider>
<EuiThemeProvider>
<NavigationProvider
{...services}
activeNodes$={getActiveNodes$()}
navLinks$={navLinks$}
onProjectNavigationChange={onProjectNavigationChange}
>
<Navigation>
<Navigation.Group id="group1">
<Navigation.Item<any>
link="item1"
title="Item 1"
getIsActive={() => {
return true;
}}
/>
</Navigation.Group>
</Navigation>
</NavigationProvider>
</EuiThemeProvider>
);
jest.advanceTimersByTime(SET_NAVIGATION_DELAY);
@ -824,15 +863,17 @@ describe('<Navigation />', () => {
const onProjectNavigationChange = jest.fn();
const { findByTestId } = render(
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
<Navigation>
<Navigation.Group id="group1" defaultIsCollapsed={false}>
<Navigation.Item id="cloudLink1" cloudLink="userAndRoles" />
<Navigation.Item id="cloudLink2" cloudLink="performance" />
<Navigation.Item id="cloudLink3" cloudLink="billingAndSub" />
</Navigation.Group>
</Navigation>
</NavigationProvider>
<EuiThemeProvider>
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
<Navigation>
<Navigation.Group id="group1" defaultIsCollapsed={false}>
<Navigation.Item id="cloudLink1" cloudLink="userAndRoles" />
<Navigation.Item id="cloudLink2" cloudLink="performance" />
<Navigation.Item id="cloudLink3" cloudLink="billingAndSub" />
</Navigation.Group>
</Navigation>
</NavigationProvider>
</EuiThemeProvider>
);
expect(await findByTestId(/nav-item-group1.cloudLink1/)).toBeVisible();

View file

@ -19,6 +19,7 @@ import {
type EuiThemeComputed,
useEuiTheme,
transparentize,
useIsWithinMinBreakpoint,
} from '@elastic/eui';
import type { ChromeProjectNavigationNode } from '@kbn/core-chrome-browser';
import type { NavigateToUrlFn } from '../../../types/internal';
@ -53,6 +54,8 @@ export const NavigationItemOpenPanel: FC<Props> = ({ item, navigateToUrl }: Prop
const { title, deepLink, isActive, children } = item;
const id = nodePathToString(item);
const href = deepLink?.url ?? item.href;
const isNotMobile = useIsWithinMinBreakpoint('s');
const isIconVisible = isNotMobile && !!children && children.length > 0;
const itemClassNames = classNames(
'sideNavItem',
@ -73,12 +76,16 @@ export const NavigationItemOpenPanel: FC<Props> = ({ item, navigateToUrl }: Prop
);
const onIconClick = useCallback(() => {
openPanel(item);
}, [openPanel, item]);
if (selectedNode?.id === item.id) {
closePanel();
} else {
openPanel(item);
}
}, [openPanel, closePanel, item, selectedNode]);
return (
<EuiFlexGroup alignItems="center" gutterSize="xs">
<EuiFlexItem>
<EuiFlexItem style={{ flexBasis: isIconVisible ? '80%' : '100%' }}>
<EuiListGroup gutterSize="none">
<EuiListGroupItem
label={title}
@ -92,8 +99,8 @@ export const NavigationItemOpenPanel: FC<Props> = ({ item, navigateToUrl }: Prop
/>
</EuiListGroup>
</EuiFlexItem>
{!!children && children.length > 0 && (
<EuiFlexItem grow={0}>
{isIconVisible && (
<EuiFlexItem grow={0} style={{ flexBasis: '15%' }}>
<EuiButtonIcon
display={nodePathToString(selectedNode) === id ? 'base' : 'empty'}
size="s"
@ -104,7 +111,7 @@ export const NavigationItemOpenPanel: FC<Props> = ({ item, navigateToUrl }: Prop
aria-label={i18n.translate('sharedUXPackages.chrome.sideNavigation.togglePanel', {
defaultMessage: 'Toggle panel navigation',
})}
data-test-subj={`solutionSideNavItemButton-${id}`}
data-test-subj={`panelOpener-${id}`}
/>
</EuiFlexItem>
)}

View file

@ -9,14 +9,16 @@
import React, { FC } from 'react';
import {
EuiAccordionProps,
EuiCollapsibleNavItem,
EuiCollapsibleNavItemProps,
EuiCollapsibleNavSubItemProps,
EuiTitle,
EuiCollapsibleNavItem,
EuiSpacer,
type EuiAccordionProps,
type EuiCollapsibleNavItemProps,
type EuiCollapsibleNavSubItemProps,
} from '@elastic/eui';
import type { ChromeProjectNavigationNode } from '@kbn/core-chrome-browser';
import classnames from 'classnames';
import type { EuiThemeSize, RenderAs } from '@kbn/core-chrome-browser/src/project_navigation';
import type { NavigateToUrlFn } from '../../../types/internal';
import { useNavigation as useServices } from '../../services';
@ -29,6 +31,8 @@ const nodeHasLink = (navNode: ChromeProjectNavigationNode) =>
const nodeHasChildren = (navNode: ChromeProjectNavigationNode) => Boolean(navNode.children?.length);
const DEFAULT_SPACE_BETWEEN_LEVEL_1_GROUPS: EuiThemeSize = 'm';
/**
* Predicate to determine if a node should be visible in the main side nav.
* If it is not visible it will be filtered out and not rendered.
@ -36,11 +40,6 @@ const nodeHasChildren = (navNode: ChromeProjectNavigationNode) => Boolean(navNod
const itemIsVisible = (item: ChromeProjectNavigationNode) => {
if (item.sideNavStatus === 'hidden') return false;
const isGroupTitle = Boolean(item.isGroupTitle);
if (isGroupTitle) {
return true;
}
if (nodeHasLink(item)) {
return true;
}
@ -52,6 +51,12 @@ const itemIsVisible = (item: ChromeProjectNavigationNode) => {
return false;
};
const getRenderAs = (navNode: ChromeProjectNavigationNode): RenderAs => {
if (navNode.renderAs) return navNode.renderAs;
if (!navNode.children) return 'item';
return 'block';
};
const filterChildren = (
children?: ChromeProjectNavigationNode[]
): ChromeProjectNavigationNode[] | undefined => {
@ -60,13 +65,15 @@ const filterChildren = (
};
const serializeNavNode = (navNode: ChromeProjectNavigationNode) => {
const serialized = {
const serialized: ChromeProjectNavigationNode = {
...navNode,
id: nodePathToString(navNode),
children: filterChildren(navNode.children),
href: getNavigationNodeHref(navNode),
};
serialized.renderAs = getRenderAs(serialized);
return {
navNode: serialized,
hasChildren: nodeHasChildren(serialized),
@ -83,6 +90,53 @@ const isEuiCollapsibleNavItemProps = (
);
};
const renderBlockTitle: (
{ title }: ChromeProjectNavigationNode,
{ spaceBefore }: { spaceBefore: EuiThemeSize | null }
) => Required<EuiCollapsibleNavSubItemProps>['renderItem'] =
({ title }, { spaceBefore }) =>
() =>
(
<EuiTitle
size="xxxs"
className="eui-textTruncate"
css={({ euiTheme }: any) => {
return {
marginTop: spaceBefore ? euiTheme.size[spaceBefore] : undefined,
// marginTop: euiTheme.size.base,
paddingBlock: euiTheme.size.xs,
paddingInline: euiTheme.size.s,
};
}}
>
<div>{title}</div>
</EuiTitle>
);
const renderGroup = (
navGroup: ChromeProjectNavigationNode,
groupItems: Array<EuiCollapsibleNavItemProps | EuiCollapsibleNavSubItemProps>,
{ spaceBefore = DEFAULT_SPACE_BETWEEN_LEVEL_1_GROUPS }: { spaceBefore?: EuiThemeSize | null } = {}
): Required<EuiCollapsibleNavItemProps>['items'] => {
let itemPrepend: EuiCollapsibleNavItemProps | EuiCollapsibleNavSubItemProps | null = null;
if (!!navGroup.title) {
itemPrepend = {
renderItem: renderBlockTitle(navGroup, { spaceBefore }),
};
} else if (spaceBefore) {
itemPrepend = {
renderItem: () => <EuiSpacer size={spaceBefore} />,
};
}
if (!itemPrepend) {
return groupItems;
}
return [itemPrepend, ...groupItems];
};
// Generate the EuiCollapsible props for both the root component (EuiCollapsibleNavItem) and its
// "items" props. Both are compatible with the exception of "renderItem" which is only used for
// sub items.
@ -101,10 +155,22 @@ const nodeToEuiCollapsibleNavProps = (
isSideNavCollapsed: boolean;
treeDepth: number;
}
): { props: EuiCollapsibleNavItemProps | EuiCollapsibleNavSubItemProps; isVisible: boolean } => {
): {
items: Array<EuiCollapsibleNavItemProps | EuiCollapsibleNavSubItemProps>;
isVisible: boolean;
} => {
const { navNode, isItem, hasChildren, hasLink } = serializeNavNode(_navNode);
const { id, title, href, icon, renderAs, isActive, deepLink, isGroupTitle } = navNode;
const {
id,
title,
href,
icon,
renderAs,
isActive,
deepLink,
spaceBefore: _spaceBefore,
} = navNode;
const isExternal = Boolean(href) && isAbsoluteLink(href!);
const isSelected = hasChildren ? false : isActive;
const dataTestSubj = classnames(`nav-item`, `nav-item-${id}`, {
@ -113,34 +179,25 @@ const nodeToEuiCollapsibleNavProps = (
[`nav-item-isActive`]: isSelected,
});
// Note: this can be replaced with an `isGroup` API or whatever you prefer
// Could also probably be pulled out to a separate component vs inlined
if (isGroupTitle) {
const props: EuiCollapsibleNavSubItemProps = {
renderItem: () => (
<EuiTitle
size="xxxs"
className="eui-textTruncate"
css={({ euiTheme }: any) => ({
marginTop: euiTheme.size.base,
paddingBlock: euiTheme.size.xs,
paddingInline: euiTheme.size.s,
})}
>
<div id={id} data-test-subj={dataTestSubj}>
{title}
</div>
</EuiTitle>
),
};
return { props, isVisible: true };
let spaceBefore = _spaceBefore;
if (spaceBefore === undefined && treeDepth === 1 && hasChildren) {
// For groups at level 1 that don't have a space specified we default to add a "m"
// space. For all other groups, unless specified, there is no vertical space.
spaceBefore = DEFAULT_SPACE_BETWEEN_LEVEL_1_GROUPS;
}
if (renderAs === 'panelOpener') {
const props: EuiCollapsibleNavSubItemProps = {
renderItem: () => <NavigationItemOpenPanel item={navNode} navigateToUrl={navigateToUrl} />,
};
return { props, isVisible: true };
const items: EuiCollapsibleNavSubItemProps[] = [
{
renderItem: () => <NavigationItemOpenPanel item={navNode} navigateToUrl={navigateToUrl} />,
},
];
if (spaceBefore) {
items.unshift({
renderItem: () => <EuiSpacer size={spaceBefore!} />,
});
}
return { items, isVisible: true };
}
const onClick = (e: React.MouseEvent) => {
@ -152,7 +209,7 @@ const nodeToEuiCollapsibleNavProps = (
}
};
const items: EuiCollapsibleNavItemProps['items'] = isItem
const subItems: EuiCollapsibleNavItemProps['items'] | undefined = isItem
? undefined
: navNode.children
?.map((child) =>
@ -165,7 +222,11 @@ const nodeToEuiCollapsibleNavProps = (
})
)
.filter(({ isVisible }) => isVisible)
.map((res) => res.props);
.map((res) => {
const itemsFlattened: EuiCollapsibleNavItemProps['items'] = res.items.flat();
return itemsFlattened;
})
.flat();
const linkProps: EuiCollapsibleNavItemProps['linkProps'] | undefined = hasLink
? {
@ -190,22 +251,43 @@ const nodeToEuiCollapsibleNavProps = (
...navNode.accordionProps,
};
const props: EuiCollapsibleNavItemProps = {
id,
title,
isSelected,
accordionProps,
linkProps,
onClick,
href,
items,
['data-test-subj']: dataTestSubj,
icon,
iconProps: { size: treeDepth === 0 ? 'm' : 's' },
};
if (renderAs === 'block' && treeDepth > 0 && subItems) {
// Render as a group block (bold title + list of links underneath)
return {
items: [...renderGroup(navNode, subItems, { spaceBefore: spaceBefore ?? null })],
isVisible: subItems.length > 0,
};
}
// Render as an accordion or a link (handled by EUI) depending if
// "items" is undefined or not. If it is undefined --> a link, otherwise an
// accordion is rendered.
const items: Array<EuiCollapsibleNavItemProps | EuiCollapsibleNavSubItemProps> = [
{
id,
title,
isSelected,
accordionProps,
linkProps,
onClick,
href,
items: subItems,
['data-test-subj']: dataTestSubj,
icon,
iconProps: { size: treeDepth === 0 ? 'm' : 's' },
},
];
const hasVisibleChildren = (items?.length ?? 0) > 0;
return { props, isVisible: isItem || hasVisibleChildren };
const isVisible = isItem || hasVisibleChildren;
if (isVisible && spaceBefore) {
items.unshift({
renderItem: () => <EuiSpacer size={spaceBefore!} />,
});
}
return { items, isVisible };
};
interface Props {
@ -216,7 +298,7 @@ export const NavigationSectionUI: FC<Props> = ({ navNode }) => {
const { navigateToUrl, isSideNavCollapsed } = useServices();
const { open: openPanel, close: closePanel } = usePanel();
const { props, isVisible } = nodeToEuiCollapsibleNavProps(navNode, {
const { items, isVisible } = nodeToEuiCollapsibleNavProps(navNode, {
navigateToUrl,
openPanel,
closePanel,
@ -224,6 +306,8 @@ export const NavigationSectionUI: FC<Props> = ({ navNode }) => {
treeDepth: 0,
});
const [props] = items;
if (!isEuiCollapsibleNavItemProps(props)) {
throw new Error(`Invalid EuiCollapsibleNavItem props for node ${props.id}`);
}

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { ChromeProjectNavigationNode } from '@kbn/core-chrome-browser';
import React, { Fragment, type FC } from 'react';
@ -69,13 +69,10 @@ export const DefaultContent: FC<Props> = ({ selectedNode }) => {
(child) => child.sideNavStatus !== 'hidden'
);
const serializedChildren = serializeChildren({ ...selectedNode, children: filteredChildren });
const totalChildren = serializedChildren?.length ?? 0;
const firstChildIsGroup = !!serializedChildren?.[0]?.children;
const firstGroupTitle = firstChildIsGroup && serializedChildren?.[0]?.title;
const firstGroupHasTitle = !!firstGroupTitle;
return (
<EuiFlexGroup direction="column" gutterSize="m" alignItems="flexStart">
{/* Panel title */}
<EuiFlexItem>
{typeof selectedNode.title === 'string' ? (
<EuiTitle size="xxs">
@ -86,10 +83,9 @@ export const DefaultContent: FC<Props> = ({ selectedNode }) => {
)}
</EuiFlexItem>
{/* Panel navigation */}
<EuiFlexItem style={{ width: '100%' }}>
<>
{firstGroupHasTitle && <EuiSpacer size="l" />}
{serializedChildren && (
<>
{serializedChildren.map((child, i) => {
@ -103,9 +99,6 @@ export const DefaultContent: FC<Props> = ({ selectedNode }) => {
isFirstInList={i === 0}
hasHorizontalRuleBefore={hasHorizontalRuleBefore}
/>
{i < totalChildren - 1 && (
<EuiSpacer size={child.appendHorizontalRule ? 'm' : 'l'} />
)}
</Fragment>
) : (
<PanelNavItem key={child.id} item={child} />

View file

@ -22,7 +22,7 @@ import { getNavPanelStyles, getPanelWrapperStyles } from './styles';
export const NavigationPanel: FC = () => {
const { euiTheme } = useEuiTheme();
const { isOpen, close, getContent } = usePanel();
const { isOpen, close, getContent, selectedNode } = usePanel();
// ESC key closes PanelNav
const onKeyDown = useCallback(
@ -34,9 +34,15 @@ export const NavigationPanel: FC = () => {
[close]
);
const onOutsideClick = useCallback(() => {
close();
}, [close]);
const onOutsideClick = useCallback(
({ target }: Event) => {
// Only close if we are not clicking on the currently selected nav node
if ((target as HTMLButtonElement).dataset.testSubj !== `panelOpener-${selectedNode?.id}`) {
close();
}
},
[close, selectedNode]
);
const panelWrapperClasses = getPanelWrapperStyles();
const sideNavPanelStyles = getNavPanelStyles(euiTheme);

View file

@ -5,7 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { FC, Fragment, useCallback } from 'react';
import React, { FC, useCallback } from 'react';
import {
EuiListGroup,
EuiTitle,
@ -19,7 +19,7 @@ import {
} from '@elastic/eui';
import { css } from '@emotion/css';
import { ChromeProjectNavigationNode } from '@kbn/core-chrome-browser';
import type { ChromeProjectNavigationNode } from '@kbn/core-chrome-browser';
import { PanelNavItem } from './panel_nav_item';
const accordionButtonClassName = 'sideNavPanelAccordion__button';
@ -42,6 +42,16 @@ const getClassnames = (euiTheme: EuiThemeComputed<{}>) => ({
`,
});
const someChildIsVisible = (children: ChromeProjectNavigationNode[]) => {
return children.some((child) => {
if (child.renderAs === 'item') return true;
if (child.children) {
return child.children.every(({ sideNavStatus }) => sideNavStatus !== 'hidden');
}
return true;
});
};
interface Props {
navNode: ChromeProjectNavigationNode;
/** Flag to indicate if the group is the first in the list of groups when looping */
@ -52,15 +62,24 @@ interface Props {
export const PanelGroup: FC<Props> = ({ navNode, isFirstInList, hasHorizontalRuleBefore }) => {
const { euiTheme } = useEuiTheme();
const { id, title, appendHorizontalRule } = navNode;
const { id, title, appendHorizontalRule, spaceBefore: _spaceBefore } = navNode;
const filteredChildren = navNode.children?.filter((child) => child.sideNavStatus !== 'hidden');
const totalChildren = filteredChildren?.length ?? 0;
const classNames = getClassnames(euiTheme);
const hasTitle = !!title && title !== '';
const removePaddingTop = !hasTitle && !isFirstInList;
const someChildIsGroup = filteredChildren?.some((child) => !!child.children);
const firstChildIsGroup = !!filteredChildren?.[0]?.children;
let spaceBefore = _spaceBefore;
if (spaceBefore === undefined) {
if (!hasTitle && isFirstInList) {
// If the first group has no title, we don't add any space.
spaceBefore = null;
} else {
spaceBefore = hasHorizontalRuleBefore ? 'm' : 'l';
}
}
const renderChildren = useCallback(() => {
if (!filteredChildren) return null;
@ -69,21 +88,19 @@ export const PanelGroup: FC<Props> = ({ navNode, isFirstInList, hasHorizontalRul
return isItem ? (
<PanelNavItem key={item.id} item={item} />
) : (
<Fragment key={item.id}>
<PanelGroup navNode={item} />
{i < totalChildren - 1 && <EuiSpacer />}
</Fragment>
<PanelGroup navNode={item} key={item.id} />
);
});
}, [filteredChildren, totalChildren]);
}, [filteredChildren]);
if (!filteredChildren?.length) {
if (!filteredChildren?.length || !someChildIsVisible(filteredChildren)) {
return null;
}
if (navNode.renderAs === 'accordion') {
return (
<>
{spaceBefore !== null && <EuiSpacer size={spaceBefore} />}
<EuiAccordion
id={id}
buttonContent={title}
@ -91,7 +108,7 @@ export const PanelGroup: FC<Props> = ({ navNode, isFirstInList, hasHorizontalRul
buttonClassName={accordionButtonClassName}
>
<>
<EuiSpacer size={firstChildIsGroup ? 'l' : 's'} />
{!firstChildIsGroup && <EuiSpacer size="s" />}
{renderChildren()}
</>
</EuiAccordion>
@ -102,9 +119,10 @@ export const PanelGroup: FC<Props> = ({ navNode, isFirstInList, hasHorizontalRul
return (
<>
{spaceBefore !== null && <EuiSpacer size={spaceBefore} />}
{hasTitle && (
<EuiTitle size="xxxs" className={classNames.title}>
<h2>{navNode.title}</h2>
<h2>{title}</h2>
</EuiTitle>
)}
<EuiListGroup

View file

@ -14,6 +14,7 @@ import type {
ChromeProjectNavigation,
ChromeProjectNavigationNode,
} from '@kbn/core-chrome-browser';
import { EuiThemeProvider } from '@elastic/eui';
import { getServicesMock } from '../../mocks/src/jest';
import { NavigationProvider } from '../services';
@ -81,9 +82,11 @@ describe('<DefaultNavigation />', () => {
];
const { findAllByTestId } = render(
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
<DefaultNavigation navigationTree={{ body: navigationBody }} />
</NavigationProvider>
<EuiThemeProvider>
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
<DefaultNavigation navigationTree={{ body: navigationBody }} />
</NavigationProvider>
</EuiThemeProvider>
);
await act(async () => {
@ -252,13 +255,15 @@ describe('<DefaultNavigation />', () => {
];
render(
<NavigationProvider
{...services}
navLinks$={navLinks$}
onProjectNavigationChange={onProjectNavigationChange}
>
<DefaultNavigation navigationTree={{ body: navigationBody }} />
</NavigationProvider>
<EuiThemeProvider>
<NavigationProvider
{...services}
navLinks$={navLinks$}
onProjectNavigationChange={onProjectNavigationChange}
>
<DefaultNavigation navigationTree={{ body: navigationBody }} />
</NavigationProvider>
</EuiThemeProvider>
);
await act(async () => {
@ -494,9 +499,11 @@ describe('<DefaultNavigation />', () => {
];
render(
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
<DefaultNavigation navigationTree={{ body: navigationBody }} />
</NavigationProvider>
<EuiThemeProvider>
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
<DefaultNavigation navigationTree={{ body: navigationBody }} />
</NavigationProvider>
</EuiThemeProvider>
);
await act(async () => {
@ -590,9 +597,11 @@ describe('<DefaultNavigation />', () => {
const expectToThrow = () => {
render(
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
<DefaultNavigation navigationTree={{ body: navigationBody }} />
</NavigationProvider>
<EuiThemeProvider>
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
<DefaultNavigation navigationTree={{ body: navigationBody }} />
</NavigationProvider>
</EuiThemeProvider>
);
};
@ -615,9 +624,11 @@ describe('<DefaultNavigation />', () => {
];
const { findByTestId } = render(
<NavigationProvider {...services} recentlyAccessed$={recentlyAccessed$}>
<DefaultNavigation navigationTree={{ body: navigationBody }} />
</NavigationProvider>
<EuiThemeProvider>
<NavigationProvider {...services} recentlyAccessed$={recentlyAccessed$}>
<DefaultNavigation navigationTree={{ body: navigationBody }} />
</NavigationProvider>
</EuiThemeProvider>
);
await act(async () => {
@ -683,9 +694,11 @@ describe('<DefaultNavigation />', () => {
const getActiveNodes$ = () => activeNodes$;
const { findByTestId } = render(
<NavigationProvider {...services} navLinks$={navLinks$} activeNodes$={getActiveNodes$()}>
<DefaultNavigation navigationTree={{ body: navigationBody }} />
</NavigationProvider>
<EuiThemeProvider>
<NavigationProvider {...services} navLinks$={navLinks$} activeNodes$={getActiveNodes$()}>
<DefaultNavigation navigationTree={{ body: navigationBody }} />
</NavigationProvider>
</EuiThemeProvider>
);
await act(async () => {
@ -741,14 +754,16 @@ describe('<DefaultNavigation />', () => {
};
const { findByTestId } = render(
<NavigationProvider
{...services}
navLinks$={navLinks$}
activeNodes$={getActiveNodes$()}
onProjectNavigationChange={onProjectNavigationChange}
>
<DefaultNavigation navigationTree={{ body: navigationBody }} />
</NavigationProvider>
<EuiThemeProvider>
<NavigationProvider
{...services}
navLinks$={navLinks$}
activeNodes$={getActiveNodes$()}
onProjectNavigationChange={onProjectNavigationChange}
>
<DefaultNavigation navigationTree={{ body: navigationBody }} />
</NavigationProvider>
</EuiThemeProvider>
);
await act(async () => {
@ -804,13 +819,15 @@ describe('<DefaultNavigation />', () => {
];
render(
<NavigationProvider
{...services}
navLinks$={navLinks$}
onProjectNavigationChange={onProjectNavigationChange}
>
<DefaultNavigation projectNavigationTree={projectNavigationTree} />
</NavigationProvider>
<EuiThemeProvider>
<NavigationProvider
{...services}
navLinks$={navLinks$}
onProjectNavigationChange={onProjectNavigationChange}
>
<DefaultNavigation projectNavigationTree={projectNavigationTree} />
</NavigationProvider>
</EuiThemeProvider>
);
await act(async () => {
@ -833,9 +850,11 @@ describe('<DefaultNavigation />', () => {
describe('cloud links', () => {
test('render the cloud link', async () => {
const { findByTestId } = render(
<NavigationProvider {...services}>
<DefaultNavigation projectNavigationTree={[]} />
</NavigationProvider>
<EuiThemeProvider>
<NavigationProvider {...services}>
<DefaultNavigation projectNavigationTree={[]} />
</NavigationProvider>
</EuiThemeProvider>
);
expect(

View file

@ -201,6 +201,154 @@ export const SimpleObjectDefinition = (args: NavigationServices) => {
);
};
const groupExamplesDefinition: ProjectNavigationDefinition<any> = {
navigationTree: {
body: [
// My custom project
{
type: 'navGroup',
id: 'example_projet',
title: 'Example project',
icon: 'logoObservability',
defaultIsCollapsed: false,
children: [
{
title: 'Block group',
children: [
{
id: 'item1',
link: 'item1',
title: 'Item 1',
},
{
id: 'item2',
link: 'item1',
title: 'Item 2',
},
{
id: 'item3',
link: 'item1',
title: 'Item 3',
},
],
},
{
title: 'Accordion group',
renderAs: 'accordion',
children: [
{
id: 'item1',
link: 'item1',
title: 'Item 1',
},
{
id: 'item2',
link: 'item1',
title: 'Item 2',
},
{
id: 'item3',
link: 'item1',
title: 'Item 3',
},
],
},
{
children: [
{
id: 'item1',
link: 'item1',
title: 'Block group',
},
{
id: 'item2',
link: 'item1',
title: 'without',
},
{
id: 'item3',
link: 'item1',
title: 'title',
},
],
},
{
id: 'group:settings',
link: 'item1',
title: 'Panel group',
renderAs: 'panelOpener',
children: [
{
title: 'Group 1',
children: [
{
link: 'group:settings.logs',
title: 'Logs',
},
{
link: 'group:settings.signals',
title: 'Signals',
},
{
id: 'group:settings.signals-2',
link: 'group:settings.signals',
title: 'Signals - should NOT appear',
sideNavStatus: 'hidden', // Should not appear
},
{
link: 'group:settings.tracing',
title: 'Tracing',
},
],
},
{
id: 'group.nestedGroup',
link: 'group:settings.tracing',
title: 'Group 2',
children: [
{
id: 'item1',
link: 'group:settings.signals',
title: 'Some link title',
},
],
},
],
},
],
},
],
footer: [
{
type: 'navGroup',
...getPresets('devtools'),
},
],
},
};
export const GroupsExamples = (args: NavigationServices) => {
const services = storybookMock.getServices({
...args,
navLinks$: of([...navLinksMock, ...deepLinks]),
onProjectNavigationChange: (updated) => {
action('Update chrome navigation')(JSON.stringify(updated, null, 2));
},
recentlyAccessed$: of([
{ label: 'This is an example', link: '/app/example/39859', id: '39850' },
{ label: 'Another example', link: '/app/example/5235', id: '5235' },
]),
});
return (
<NavigationWrapper>
<NavigationProvider {...services}>
<DefaultNavigation {...groupExamplesDefinition} />
</NavigationProvider>
</NavigationWrapper>
);
};
const navigationDefinition: ProjectNavigationDefinition<any> = {
navigationTree: {
body: [
@ -216,6 +364,26 @@ const navigationDefinition: ProjectNavigationDefinition<any> = {
link: 'item1',
title: 'Get started',
},
{
title: 'Group 1',
children: [
{
id: 'item1',
link: 'item1',
title: 'Item 1',
},
{
id: 'item2',
link: 'item1',
title: 'Item 2',
},
{
id: 'item3',
link: 'item1',
title: 'Item 3',
},
],
},
{
link: 'item2',
title: 'Alerts',
@ -683,7 +851,7 @@ const navigationDefinitionWithPanel: ProjectNavigationDefinition<any> = {
id: 'root',
children: [
{
title: 'Should act as item 1',
title: 'Group renders as "item" (1)',
link: 'item1',
renderAs: 'item',
children: [
@ -699,7 +867,7 @@ const navigationDefinitionWithPanel: ProjectNavigationDefinition<any> = {
},
{
link: 'group:settings.logs',
title: 'Normal item',
title: 'Item 2',
},
{
link: 'group:settings.logs2',
@ -723,25 +891,7 @@ const navigationDefinitionWithPanel: ProjectNavigationDefinition<any> = {
],
},
{
title: 'Should act as item 2',
renderAs: 'item', // This group renders as a normal item
children: [
{
link: 'group:settings.logs',
title: 'Logs',
},
{
link: 'group:settings.signals',
title: 'Signals',
},
],
},
],
},
{
children: [
{
title: 'Another group as Item',
title: 'Group renders as "item" (2)',
id: 'group2.renderAsItem',
renderAs: 'item',
children: [
@ -797,7 +947,7 @@ const navigationDefinitionWithPanel: ProjectNavigationDefinition<any> = {
children: [
{
id: 'group2-B',
title: 'Group 2 (render as Item)',
title: 'Group renders as "item" (3)',
renderAs: 'item', // This group renders as a normal item
children: [
{
@ -995,6 +1145,19 @@ export const WithUIComponents = (args: NavigationServices) => {
</Navigation.Item>
<Navigation.Item id="item4" title="External link" href="https://elastic.co" />
<Navigation.Group<any> id="group:block" title="This is a block group">
<Navigation.Group id="group1">
<Navigation.Item<any> link="group:settings.logs" title="Logs" />
<Navigation.Item<any> link="group:settings.signals" title="Signals" withBadge />
<Navigation.Item<any> link="group:settings.tracing" title="Tracing" />
</Navigation.Group>
<Navigation.Group id="group2" title="Nested group" renderAs="accordion">
<Navigation.Item<any> link="group:settings.logs" title="Logs" />
<Navigation.Item<any> link="group:settings.signals" title="Signals" />
<Navigation.Item<any> link="group:settings.tracing" title="Tracing" />
</Navigation.Group>
</Navigation.Group>
<Navigation.Group<any>
id="group:openPanel"
link="item1"

View file

@ -79,9 +79,11 @@ const navigationTree: NavigationTreeDefinition = {
{
id: 'aiops',
title: 'AIOps',
renderAs: 'accordion',
accordionProps: {
arrowProps: { css: { display: 'none' } },
},
spaceBefore: null,
children: [
{
title: i18n.translate('xpack.serverlessObservability.nav.ml.jobs', {
@ -115,15 +117,12 @@ const navigationTree: NavigationTreeDefinition = {
},
],
},
{
id: 'groups-spacer-1',
isGroupTitle: true,
},
{
id: 'apm',
title: i18n.translate('xpack.serverlessObservability.nav.applications', {
defaultMessage: 'Applications',
}),
renderAs: 'accordion',
accordionProps: {
arrowProps: { css: { display: 'none' } },
},
@ -154,6 +153,7 @@ const navigationTree: NavigationTreeDefinition = {
title: i18n.translate('xpack.serverlessObservability.nav.infrastructure', {
defaultMessage: 'Infrastructure',
}),
renderAs: 'accordion',
accordionProps: {
arrowProps: { css: { display: 'none' } },
},
@ -172,10 +172,6 @@ const navigationTree: NavigationTreeDefinition = {
},
],
},
{
id: 'groups-spacer-2',
isGroupTitle: true,
},
],
},
],
@ -186,7 +182,6 @@ const navigationTree: NavigationTreeDefinition = {
defaultMessage: 'Get Started',
}),
link: 'observabilityOnboarding',
isGroupTitle: true,
icon: 'launch',
},
{

View file

@ -45,35 +45,36 @@ const navigationTree: NavigationTreeDefinition = {
title: i18n.translate('xpack.serverlessSearch.nav.explore', {
defaultMessage: 'Explore',
}),
isGroupTitle: true,
},
{
link: 'discover',
},
{
link: 'dashboards',
getIsActive: ({ pathNameSerialized, prepend }) => {
return pathNameSerialized.startsWith(prepend('/app/dashboards'));
},
},
{
link: 'visualize',
title: i18n.translate('xpack.serverlessSearch.nav.visualize', {
defaultMessage: 'Visualizations',
}),
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',
}),
children: [
{
link: 'discover',
},
{
link: 'dashboards',
getIsActive: ({ pathNameSerialized, prepend }) => {
return pathNameSerialized.startsWith(prepend('/app/dashboards'));
},
},
{
link: 'visualize',
title: i18n.translate('xpack.serverlessSearch.nav.visualize', {
defaultMessage: 'Visualizations',
}),
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',
}),
},
],
},
{
@ -81,33 +82,37 @@ const navigationTree: NavigationTreeDefinition = {
title: i18n.translate('xpack.serverlessSearch.nav.content', {
defaultMessage: 'Content',
}),
isGroupTitle: true,
children: [
{
title: i18n.translate('xpack.serverlessSearch.nav.content.indices', {
defaultMessage: 'Index Management',
}),
link: 'management:index_management',
breadcrumbStatus:
'hidden' /* management sub-pages set their breadcrumbs themselves */,
},
{
title: i18n.translate('xpack.serverlessSearch.nav.content.pipelines', {
defaultMessage: 'Pipelines',
}),
link: 'management:ingest_pipelines',
breadcrumbStatus:
'hidden' /* management sub-pages set their breadcrumbs themselves */,
},
],
},
{
title: i18n.translate('xpack.serverlessSearch.nav.content.indices', {
defaultMessage: 'Index Management',
}),
link: 'management:index_management',
breadcrumbStatus: 'hidden' /* management sub-pages set their breadcrumbs themselves */,
},
{
title: i18n.translate('xpack.serverlessSearch.nav.content.pipelines', {
defaultMessage: 'Pipelines',
}),
link: 'management:ingest_pipelines',
breadcrumbStatus: 'hidden' /* management sub-pages set their breadcrumbs themselves */,
},
{
id: 'security',
title: i18n.translate('xpack.serverlessSearch.nav.security', {
defaultMessage: 'Security',
}),
isGroupTitle: true,
},
{
link: 'management:api_keys',
breadcrumbStatus: 'hidden' /* management sub-pages set their breadcrumbs themselves */,
children: [
{
link: 'management:api_keys',
breadcrumbStatus:
'hidden' /* management sub-pages set their breadcrumbs themselves */,
},
],
},
],
},