[8.x] [Solution Side Nav] Remove PanelContentProvider & support optional title in nav node (#218156) (#218330)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Solution Side Nav] Remove PanelContentProvider & support optional
title in nav node
(#218156)](https://github.com/elastic/kibana/pull/218156)

<!--- Backport version: 9.6.6 -->

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

<!--BACKPORT [{"author":{"name":"Tim
Sullivan","email":"tsullivan@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-04-15T15:04:01Z","message":"[Solution
Side Nav] Remove PanelContentProvider & support optional title in nav
node (#218156)\n\nEpic:
https://github.com/elastic/kibana-team/issues/1439\nNeeded for
https://github.com/elastic/kibana/pull/218050 (adjustments to\ntypes for
`title` field in `ChromeProjectNavigationNode`)\n\n## Summary\n\n1.
`PanelContentProvider` was used for security solution, but is no\nlonger
used. This removes it to simplify the interfaces for panel .\n2. Allow
title of `navGroup` to be optional. This allows the correct\ndesign for
nav items in the footer, which are child-items of a nav group\nwith no
title\n\n## Checklist\n\nCheck the PR satisfies following conditions.
\n\nReviewers should verify this PR satisfies this list as well.\n\n-
[x]\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\nwas
added for features that require explanation or tutorials\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"b9c2b57c23d1d74d4fef025d64b820216ddce272","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:SharedUX","backport:version","v9.1.0","v8.19.0"],"title":"[Solution
Side Nav] Remove PanelContentProvider & support optional title in nav
node","number":218156,"url":"https://github.com/elastic/kibana/pull/218156","mergeCommit":{"message":"[Solution
Side Nav] Remove PanelContentProvider & support optional title in nav
node (#218156)\n\nEpic:
https://github.com/elastic/kibana-team/issues/1439\nNeeded for
https://github.com/elastic/kibana/pull/218050 (adjustments to\ntypes for
`title` field in `ChromeProjectNavigationNode`)\n\n## Summary\n\n1.
`PanelContentProvider` was used for security solution, but is no\nlonger
used. This removes it to simplify the interfaces for panel .\n2. Allow
title of `navGroup` to be optional. This allows the correct\ndesign for
nav items in the footer, which are child-items of a nav group\nwith no
title\n\n## Checklist\n\nCheck the PR satisfies following conditions.
\n\nReviewers should verify this PR satisfies this list as well.\n\n-
[x]\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\nwas
added for features that require explanation or tutorials\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"b9c2b57c23d1d74d4fef025d64b820216ddce272"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/218156","number":218156,"mergeCommit":{"message":"[Solution
Side Nav] Remove PanelContentProvider & support optional title in nav
node (#218156)\n\nEpic:
https://github.com/elastic/kibana-team/issues/1439\nNeeded for
https://github.com/elastic/kibana/pull/218050 (adjustments to\ntypes for
`title` field in `ChromeProjectNavigationNode`)\n\n## Summary\n\n1.
`PanelContentProvider` was used for security solution, but is no\nlonger
used. This removes it to simplify the interfaces for panel .\n2. Allow
title of `navGroup` to be optional. This allows the correct\ndesign for
nav items in the footer, which are child-items of a nav group\nwith no
title\n\n## Checklist\n\nCheck the PR satisfies following conditions.
\n\nReviewers should verify this PR satisfies this list as well.\n\n-
[x]\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\nwas
added for features that require explanation or tutorials\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"b9c2b57c23d1d74d4fef025d64b820216ddce272"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
This commit is contained in:
Tim Sullivan 2025-04-16 01:01:37 -07:00 committed by GitHub
parent d35ae396a6
commit a12abe91dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 202 additions and 296 deletions

View file

@ -237,7 +237,6 @@ describe('initNavigation()', () => {
const nodesBody = treeDefinition.body as ChromeProjectNavigationNode[];
expect(nodesBody[1]).toEqual({
id: 'node-1', // auto generated
title: '',
path: 'node-1',
type: 'navGroup',
isExternalLink: false,
@ -246,7 +245,6 @@ describe('initNavigation()', () => {
{
id: 'node-0', // auto generated
path: 'node-1.node-0',
title: '',
isExternalLink: false,
sideNavStatus: 'visible',
children: [
@ -283,7 +281,6 @@ describe('initNavigation()', () => {
{
id: 'node-0', // auto generated
path: 'node-4.node-0',
title: '',
isExternalLink: false,
sideNavStatus: 'visible',
children: [

View file

@ -106,7 +106,7 @@ function extractParentPaths(key: string, navTree: Record<string, ChromeProjectNa
}
return arr
.reduce<string[]>((acc, currentValue, currentIndex) => {
.reduce<string[]>((acc, _currentValue, currentIndex) => {
acc.push(arr.slice(0, currentIndex + 1).join(''));
return acc;
}, [])
@ -244,7 +244,7 @@ function getNodeStatus(
function getTitleForNode(
navNode: { title?: string; deepLink?: { title: string }; cloudLink?: CloudLinkId },
{ deepLink, cloudLinks }: { deepLink?: ChromeNavLink; cloudLinks: CloudLinks }
): string {
): string | undefined {
if (navNode.title) {
return navNode.title;
}
@ -257,7 +257,7 @@ function getTitleForNode(
return cloudLinks[navNode.cloudLink]?.title ?? '';
}
return '';
return; // title is optional in EuiCollapsibleNavItemProps
}
function validateNodeProps<

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { ComponentType, MouseEventHandler, ReactNode } from 'react';
import type { ComponentType, MouseEventHandler } from 'react';
import type { Location } from 'history';
import type { EuiSideNavItemType, EuiThemeSizes, IconType } from '@elastic/eui';
import type { Observable } from 'rxjs';
@ -228,7 +228,7 @@ export interface ChromeProjectNavigationNode extends NodeDefinitionBase {
/** Optional id, if not passed a "link" must be provided. */
id: string;
/** Optional title. If not provided and a "link" is provided the title will be the Deep link title */
title: string;
title?: string;
/** Path in the tree of the node */
path: string;
/** App id or deeplink id */
@ -251,10 +251,8 @@ export interface ChromeProjectNavigationNode extends NodeDefinitionBase {
export type PanelSelectedNode = Pick<
ChromeProjectNavigationNode,
'id' | 'children' | 'path' | 'sideNavStatus' | 'deepLink'
> & {
title: string | ReactNode;
};
'id' | 'children' | 'path' | 'sideNavStatus' | 'deepLink' | 'title'
>;
/** @public */
export interface SideNavCompProps {

View file

@ -31,7 +31,7 @@ describe('Active node', () => {
title: 'Item 1',
path: 'group1.item1',
},
],
] as ChromeProjectNavigationNode[],
]);
return activeNodes$;

View file

@ -7,6 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import './setup_jest_mocks';
import { of } from 'rxjs';
import type {
@ -340,6 +342,96 @@ describe('builds navigation tree', () => {
expect(queryByTestId(/nav-item-root.group2.item1/)).toBeVisible();
});
test('should allow ChromeProjectNavigationNode title to be missing', () => {
const navTree: NavigationTreeDefinitionUI = {
id: 'es',
body: [
{
id: 'root',
path: 'root',
children: [
{
id: 'item1',
title: 'Item 1',
href: '/app/item1',
path: 'root.item1',
},
],
},
],
};
const renderComponent = () => {
renderNavigation({ navTreeDef: of(navTree) });
};
expect(renderComponent).not.toThrow();
});
test('should allow ChromeProjectNavigationNode to use renderItem at sub-level', () => {
const navTree: NavigationTreeDefinitionUI = {
id: 'es',
body: [
{
id: 'root',
path: 'root',
children: [
{
id: 'item1',
path: 'root.item1',
renderItem: () => <>This is a renderItem</>,
},
],
},
],
};
const renderComponent = () => {
renderNavigation({ navTreeDef: of(navTree) });
};
expect(renderComponent).not.toThrow();
});
test('should error for ChromeProjectNavigationNode missing both title and children', () => {
const navTree: NavigationTreeDefinitionUI = {
id: 'es',
body: [
{
id: 'root',
path: 'root',
children: [],
},
],
};
const renderComponent = () => {
renderNavigation({ navTreeDef: of(navTree) });
};
expect(renderComponent).toThrow('Invalid EuiCollapsibleNavItem props for node root');
});
test('should error for using renderItem in ChromeProjectNavigationNode at the top level', () => {
const navTree: NavigationTreeDefinitionUI = {
id: 'es',
body: [
{
id: 'root',
path: 'root',
title: `You can't see me`,
renderItem: () => <>This is a renderItem</>,
},
],
};
const renderComponent = () => {
renderNavigation({ navTreeDef: of(navTree) });
};
expect(renderComponent).toThrow('Invalid EuiCollapsibleNavItem props for node root');
});
test('should render recently accessed items', async () => {
const recentlyAccessed$ = of([
{ label: 'This is an example', link: '/app/example/39859', id: '39850' },

View file

@ -11,14 +11,10 @@ import './setup_jest_mocks';
import { userEvent } from '@testing-library/user-event';
import React from 'react';
import { BehaviorSubject, of } from 'rxjs';
import { of } from 'rxjs';
import type {
ChromeProjectNavigationNode,
NavigationTreeDefinitionUI,
} from '@kbn/core-chrome-browser';
import type { NavigationTreeDefinitionUI } from '@kbn/core-chrome-browser';
import { PanelContentProvider } from '../src/ui';
import { renderNavigation } from './utils';
describe('Panel', () => {
@ -209,93 +205,6 @@ describe('Panel', () => {
});
});
describe('custom content', () => {
test('should render custom component inside the panel', async () => {
const panelContentProvider: PanelContentProvider = (_id) => {
return {
content: ({ closePanel, selectedNode, activeNodes }) => {
const [path0 = []] = activeNodes;
return (
<div data-test-subj="customPanelContent">
<p data-test-subj="customPanelSelectedNode">{selectedNode.path}</p>
<ul data-test-subj="customPanelActiveNodes">
{path0.map((node) => (
<li key={node.id}>{node.id}</li>
))}
</ul>
<button data-test-subj="customPanelCloseBtn" onClick={closePanel}>
Close panel
</button>
</div>
);
},
};
};
const activeNodes$ = new BehaviorSubject<ChromeProjectNavigationNode[][]>([
[
{
id: 'activeGroup1',
title: 'Group 1',
path: 'activeGroup1',
},
{
id: 'activeItem1',
title: 'Item 1',
path: 'activeGroup1.activeItem1',
},
],
]);
const navTree: NavigationTreeDefinitionUI = {
id: 'es',
body: [
{
id: 'root',
title: 'Root',
path: 'root',
isCollapsible: false,
children: [
{
id: 'group1',
title: 'Group 1',
path: 'root.group1',
href: '/app/item1',
renderAs: 'panelOpener',
children: [
{ id: 'item1', title: 'Item 1', href: '/app/item1', path: 'root.group1.item1' },
],
},
],
},
],
};
const { queryByTestId } = renderNavigation({
navTreeDef: of(navTree),
panelContentProvider,
services: { activeNodes$ },
});
expect(queryByTestId(/sideNavPanel/)).toBeNull();
expect(queryByTestId(/customPanelContent/)).toBeNull();
queryByTestId(/nav-item-root.group1/)?.click(); // open the panel
expect(queryByTestId(/sideNavPanel/)).not.toBeNull();
expect(queryByTestId(/customPanelContent/)).not.toBeNull();
expect(queryByTestId(/customPanelContent/)).toBeVisible();
// Test that the selected node is correclty passed
expect(queryByTestId(/customPanelSelectedNode/)?.textContent).toBe('root.group1');
// Test that the active nodes are correclty passed
expect(queryByTestId(/customPanelActiveNodes/)?.textContent).toBe('activeGroup1activeItem1');
// Test that handler to close the panel is correctly passed
queryByTestId(/customPanelCloseBtn/)?.click(); // close the panel
expect(queryByTestId(/customPanelContent/)).toBeNull();
expect(queryByTestId(/sideNavPanel/)).toBeNull();
});
});
describe('auto generated content', () => {
test('should rendre block groups with title', async () => {
const navTree: NavigationTreeDefinitionUI = {

View file

@ -19,7 +19,6 @@ import { EuiThemeProvider } from '@elastic/eui';
import { NavigationProvider } from '../src/services';
import { Navigation } from '../src/ui/navigation';
import type { PanelContentProvider } from '../src/ui';
import { NavigationServices } from '../src/types';
import { EventTracker } from '../src/analytics';
@ -47,16 +46,14 @@ const services = getServicesMock();
export const renderNavigation = ({
navTreeDef,
services: overrideServices = {},
panelContentProvider,
}: {
navTreeDef: Observable<NavigationTreeDefinitionUI>;
services?: Partial<NavigationServices>;
panelContentProvider?: PanelContentProvider;
}): RenderResult => {
const renderResult = render(
<EuiThemeProvider>
<NavigationProvider {...services} {...overrideServices}>
<Navigation navigationTree$={navTreeDef} panelContentProvider={panelContentProvider} />
<Navigation navigationTree$={navTreeDef} />
</NavigationProvider>
</EuiThemeProvider>
);

View file

@ -14,6 +14,6 @@ export { EventType, FieldType } from './src/analytics';
export type { NavigationProps } from './src/ui';
export type { PanelComponentProps, PanelContent, PanelContentProvider } from './src/ui';
export type { PanelComponentProps, PanelContent } from './src/ui';
export type { NavigationServices, NavigationKibanaDependencies } from './src/types';

View file

@ -15,8 +15,4 @@ export type { Props as RecentlyAccessedProps } from './recently_accessed';
export { FeedbackBtn } from './feedback_btn';
export type {
PanelContent,
PanelComponentProps,
ContentProvider as PanelContentProvider,
} from './panel';
export type { PanelContent, PanelComponentProps } from './panel';

View file

@ -138,9 +138,17 @@ const serializeNavNode = (
const isEuiCollapsibleNavItemProps = (
props: EuiCollapsibleNavItemProps | EuiCollapsibleNavSubItemProps
): props is EuiCollapsibleNavItemProps => {
return (
props.title !== undefined && (props as EuiCollapsibleNavSubItemProps).renderItem === undefined
);
// collapsible nav item should not have renderItem
if ('renderItem' in props) {
return false;
}
if (!(props.title || props.items?.length)) {
// title is not needed if nav item has sub-items
return false;
}
return true;
};
const renderBlockTitle: (
@ -486,7 +494,6 @@ export const NavigationSectionUI: FC<Props> = React.memo(({ navNode: _navNode })
basePath,
});
}, [navNode, navigateToUrl, closePanel, getIsCollapsed, activeNodes, eventTracker, basePath]);
const { items: topLevelItems } = props;
// Serializer to add recursively the accordionProps to each of the items
@ -549,9 +556,7 @@ export const NavigationSectionUI: FC<Props> = React.memo(({ navNode: _navNode })
}, [topLevelItems, serializeAccordionItems]);
if (!isEuiCollapsibleNavItemProps(props)) {
throw new Error(
`Invalid EuiCollapsibleNavItem props for node ${(props as EuiCollapsibleNavSubItemProps).id}`
);
throw new Error(`Invalid EuiCollapsibleNavItem props for node ${_navNode.id}`);
}
if (!isVisible) {

View file

@ -14,14 +14,12 @@ import React, {
useContext,
useMemo,
useState,
ReactNode,
useEffect,
useRef,
} from 'react';
import type { ChromeProjectNavigationNode, PanelSelectedNode } from '@kbn/core-chrome-browser';
import { DefaultContent } from './default_content';
import { ContentProvider } from './types';
export interface PanelContext {
isOpen: boolean;
@ -39,7 +37,6 @@ export interface PanelContext {
const Context = React.createContext<PanelContext | null>(null);
interface Props {
contentProvider?: ContentProvider;
activeNodes: ChromeProjectNavigationNode[][];
selectedNode?: PanelSelectedNode | null;
setSelectedNode?: (node: PanelSelectedNode | null) => void;
@ -47,8 +44,6 @@ interface Props {
export const PanelProvider: FC<PropsWithChildren<Props>> = ({
children,
contentProvider,
activeNodes,
selectedNode: selectedNodeProp = null,
setSelectedNode,
}) => {
@ -99,20 +94,8 @@ export const PanelProvider: FC<PropsWithChildren<Props>> = ({
return null;
}
const provided = contentProvider?.(selectedNode.path);
if (!provided) {
return <DefaultContent selectedNode={selectedNode} />;
}
if (provided.content) {
const Component = provided.content;
return <Component closePanel={close} selectedNode={selectedNode} activeNodes={activeNodes} />;
}
const title: string | ReactNode = provided.title ?? selectedNode.title;
return <DefaultContent selectedNode={{ ...selectedNode, title }} />;
}, [selectedNode, contentProvider, close, activeNodes]);
return <DefaultContent selectedNode={selectedNode} />;
}, [selectedNode]);
const ctx: PanelContext = useMemo(
() => ({

View file

@ -41,7 +41,6 @@ function serializeChildren(node: PanelSelectedNode): ChromeProjectNavigationNode
return [
{
id: 'root',
title: '',
path: `${node.path}.root`,
children: [...node.children],
},

View file

@ -11,4 +11,4 @@ export { NavigationPanel } from './navigation_panel';
export { PanelProvider, usePanel } from './context';
export type { PanelContext } from './context';
export type { ContentProvider, PanelContent, PanelComponentProps } from './types';
export type { PanelContent, PanelComponentProps } from './types';

View file

@ -23,5 +23,3 @@ export interface PanelContent {
title?: ReactNode | string;
content?: ComponentType<PanelComponentProps>;
}
export type ContentProvider = (nodeId: string) => PanelContent | void;

View file

@ -12,4 +12,4 @@ export type { Props as NavigationProps } from './navigation';
export { RecentlyAccessed } from './components';
export type { PanelContent, PanelComponentProps, PanelContentProvider } from './components';
export type { PanelContent, PanelComponentProps } from './components';

View file

@ -97,7 +97,6 @@ const NavigationWrapper: FC<Props & Omit<Partial<EuiCollapsibleNavBetaProps>, 'c
const generalLayoutNavTree: NavigationTreeDefinitionUI = {
id: 'es',
body: [
// My custom project
{
id: 'example_project',
path: '',
@ -450,81 +449,95 @@ const generalLayoutNavTree: NavigationTreeDefinitionUI = {
],
footer: [
{
id: 'footer-section5',
title: 'Parent item, closed',
id: 'example_project_footer',
path: '',
renderAs: 'accordion',
icon: 'iInCircle',
children: [
{
id: 'item29',
path: '',
title: 'Item 29',
href: '/app/kibana',
icon: 'iInCircle',
},
{
id: 'item30',
path: '',
title: 'Item 30',
href: '/app/kibana',
icon: 'iInCircle',
},
{
id: 'item31',
path: '',
title: 'Item 31',
href: '/app/kibana',
icon: 'iInCircle',
},
{
id: 'sub-accordion',
icon: 'iInCircle',
title: 'Sub-Accordion',
id: 'footer-section5',
title: 'Parent item, closed',
path: '',
renderAs: 'accordion',
spaceBefore: null,
icon: 'iInCircle',
children: [
{
id: 'sub1',
id: 'item29',
path: '',
title: 'Item 32',
title: 'Item 29',
href: '/app/kibana',
icon: 'iInCircle',
},
{
id: 'item30',
path: '',
title: 'Item 30',
href: '/app/kibana',
icon: 'iInCircle',
},
{
id: 'item31',
path: '',
title: 'Item 31',
href: '/app/kibana',
icon: 'iInCircle',
},
{
id: 'sub-accordion',
icon: 'iInCircle',
title: 'Sub-Accordion',
path: '',
renderAs: 'accordion',
children: [
{
id: 'sub1',
path: '',
title: 'Item 32',
href: '/app/kibana',
icon: 'iInCircle',
},
],
},
],
},
],
},
{ id: 'item10', path: '', title: 'Item 10', icon: 'iInCircle', href: '/app/kibana' },
{
id: 'footer-section6',
title: 'Parent item, opened',
path: '',
renderAs: 'accordion',
icon: 'iInCircle',
defaultIsCollapsed: false,
children: [
{
id: 'item33',
id: 'item10',
path: '',
title: 'Item 33',
href: '/app/kibana',
title: 'Item 10',
icon: 'iInCircle',
href: '/app/kibana',
},
{
id: 'item34',
id: 'footer-section6',
title: 'Parent item, opened',
path: '',
title: 'Item 34',
href: '/app/kibana',
renderAs: 'accordion',
spaceBefore: null,
icon: 'iInCircle',
},
{
id: 'item35',
path: '',
title: 'Item 35',
href: '/app/kibana',
icon: 'iInCircle',
openInNewTab: true, // FIXME: show "popout" icon aligned to the right
defaultIsCollapsed: false,
children: [
{
id: 'item33',
path: '',
title: 'Item 33',
href: '/app/kibana',
icon: 'iInCircle',
},
{
id: 'item34',
path: '',
title: 'Item 34',
href: '/app/kibana',
icon: 'iInCircle',
},
{
id: 'item35',
path: '',
title: 'Item 35',
href: '/app/kibana',
icon: 'iInCircle',
openInNewTab: true,
},
],
},
],
},

View file

@ -17,13 +17,7 @@ import type {
} from '@kbn/core-chrome-browser';
import type { Observable } from 'rxjs';
import { EuiCollapsibleNavBeta, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import {
RecentlyAccessed,
NavigationPanel,
PanelProvider,
type PanelContentProvider,
FeedbackBtn,
} from './components';
import { RecentlyAccessed, NavigationPanel, PanelProvider, FeedbackBtn } from './components';
import { useNavigation as useNavigationService } from '../services';
import { NavigationSectionUI } from './components/navigation_section_ui';
@ -44,10 +38,9 @@ const NavigationContext = createContext<Context>({
export interface Props {
navigationTree$: Observable<NavigationTreeDefinitionUI>;
dataTestSubj?: string;
panelContentProvider?: PanelContentProvider;
}
const NavigationComp: FC<Props> = ({ navigationTree$, dataTestSubj, panelContentProvider }) => {
const NavigationComp: FC<Props> = ({ navigationTree$, dataTestSubj }) => {
const { activeNodes$, selectedPanelNode, setSelectedPanelNode, isFeedbackBtnVisible$ } =
useNavigationService();
@ -85,7 +78,6 @@ const NavigationComp: FC<Props> = ({ navigationTree$, dataTestSubj, panelContent
return (
<PanelProvider
activeNodes={activeNodes}
contentProvider={panelContentProvider}
selectedNode={selectedPanelNode}
setSelectedNode={setSelectedPanelNode}
>

View file

@ -264,69 +264,13 @@ The `ItemDefinition` interface represents a top level item in the side navigatio
### Panels
As seen in the API above, the `renderAs` property can be used to render a group as a panel opener. This is useful when you want to display a group of links in the side navigation and display its content in a panel on the right of the side navigation.
The content of the panel can be auto-generaged (based on the group's children) or manually provided.
The content of the panel is auto-generated based on the group's children.
#### Auto-generated panels
#### Secondary nav panel
When the panel content is auto-generated, the group's children will be rendered in the panel. Those `children` can be items or other groups (that render as `'block' (default) or `'accordion'`).
A group's children can be rendered in the secondary nav panel. Those `children` can be items or other groups (that render as `'block' (default) or `'accordion'`).
The panel will be opened when the user clicks on the group's icon button. The panel will be closed when the user clicks on the group's icon again or when the user clicks outside of the panel.
#### Manually provided panels
When the panel content is manually provided, the group's `children` are used for the navigation tree definition (and the breadcrumbs) but the actual UI content rendered inside the panel is provided through JSX.
```tsx
// 1. Define the PanelContentProvider
// -----------------------------------
const panelContentProvider: PanelContentProvider = (id: string) => {
// The full ID of the node icon button that was clicked is provided (e.g. "root.group1.itemA")
// You can use this ID to determine which panel content to render
if (id === 'foo1') {
// Return the JSX to render in the panel for this node.
return {
content: ({
/** Handler to close the panel */
closePanel,
/** ChromeNavigationNode - The node that has been clicked in the main nav */
selectedNode,
/** ChromeProjectNavigationNode[][] - Active nodes that match the current URL location */
activeNodes,
}) => {
return (
<div>
<EuiText>This is a custom component to render in the panel.</EuiText>
<EuiButton onClick={() => closePanel()}>Close panel</EuiButton>
</div>
);
},
};
}
if (id === 'foo2') {
// If need be you can only customize the "Title" of the panel and leave the content
// to be auto-generated.
return {
title: <div style={{ backgroundColor: 'yellow', fontWeight: 600 }}>Custom title</div>,
};
}
// All other nodes content ids that haven't match will be auto-generated
};
// 2a. Provide it when initiating the navigation (serverless)
// ----------------------------------------------------------
serverless.initNavigation(navigationTree$, { panelContentProvider });
// 2b. Provide it when initiating the navigation (stateful)
// --------------------------------------------------------
navigation.addSolutionNavigation({
...
navigationTree$,
panelContentProvider
});
```
### Important Concepts
#### Deep links

View file

@ -20,7 +20,6 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/
import type { Space } from '@kbn/spaces-plugin/public';
import type { SolutionId, SolutionNavigationDefinition } from '@kbn/core-chrome-browser';
import { InternalChromeStart } from '@kbn/core-chrome-browser-internal';
import type { PanelContentProvider } from '@kbn/shared-ux-chrome-navigation';
import type {
NavigationPublicSetup,
NavigationPublicStart,
@ -148,9 +147,7 @@ export class NavigationPublicPlugin
private getSideNavComponent({
dataTestSubj,
panelContentProvider,
}: {
panelContentProvider?: PanelContentProvider;
dataTestSubj?: string;
} = {}): SolutionNavigationDefinition['sideNavComponent'] {
if (!this.coreStart) throw new Error('coreStart is not available');
@ -163,7 +160,7 @@ export class NavigationPublicPlugin
return () => (
<SideNavComponent
navProps={{ navigationTree$: navigationTreeUi$, dataTestSubj, panelContentProvider }}
navProps={{ navigationTree$: navigationTreeUi$, dataTestSubj }}
deps={{ core, activeNodes$: activeNavigationNodes$ }}
/>
);
@ -171,8 +168,8 @@ export class NavigationPublicPlugin
private addSolutionNavigation(solutionNavigation: AddSolutionNavigationArg) {
if (!this.coreStart) throw new Error('coreStart is not available');
const { dataTestSubj, panelContentProvider, ...rest } = solutionNavigation;
const sideNavComponent = this.getSideNavComponent({ dataTestSubj, panelContentProvider });
const { dataTestSubj, ...rest } = solutionNavigation;
const sideNavComponent = this.getSideNavComponent({ dataTestSubj });
const { project } = this.coreStart.chrome as InternalChromeStart;
project.updateSolutionNavigations({
[solutionNavigation.id]: { ...rest, sideNavComponent },

View file

@ -14,7 +14,6 @@ import type { SolutionNavigationDefinition } from '@kbn/core-chrome-browser';
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
import type { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/public';
import { PanelContentProvider } from '@kbn/shared-ux-chrome-navigation';
import { TopNavMenuProps, TopNavMenuExtensionsRegistrySetup, createTopNav } from './top_nav_menu';
import type { RegisteredTopNavMenuData } from './top_nav_menu/top_nav_menu_data';
@ -26,8 +25,6 @@ export type SolutionNavigation = Omit<SolutionNavigationDefinition, 'sideNavComp
export type AddSolutionNavigationArg = Omit<SolutionNavigation, 'sideNavComponent'> & {
/** Data test subj for the side navigation */
dataTestSubj?: string;
/** Panel content provider for the side navigation */
panelContentProvider?: PanelContentProvider;
};
export interface NavigationPublicStart {

View file

@ -104,19 +104,12 @@ export class ServerlessPlugin
return {
setSideNavComponentDeprecated: (sideNavigationComponent) =>
project.setSideNavComponent(sideNavigationComponent),
initNavigation: (id, navigationTree$, { panelContentProvider, dataTestSubj } = {}) => {
initNavigation: (id, navigationTree$, { dataTestSubj } = {}) => {
project.initNavigation(id, navigationTree$);
project.setSideNavComponent(() => (
<SideNavComponent
navProps={{
navigationTree$: navigationTreeUi$,
dataTestSubj,
panelContentProvider,
}}
deps={{
core,
activeNodes$: activeNavigationNodes$,
}}
navProps={{ navigationTree$: navigationTreeUi$, dataTestSubj }}
deps={{ core, activeNodes$: activeNavigationNodes$ }}
/>
));
},

View file

@ -14,7 +14,6 @@ import type {
} from '@kbn/core-chrome-browser';
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
import type { Observable } from 'rxjs';
import type { PanelContentProvider } from '@kbn/shared-ux-chrome-navigation';
import { CardNavExtensionDefinition } from '@kbn/management-cards-navigation';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
@ -29,10 +28,7 @@ export interface ServerlessPluginStart {
initNavigation(
id: SolutionId,
navigationTree$: Observable<NavigationTreeDefinition>,
config?: {
dataTestSubj?: string;
panelContentProvider?: PanelContentProvider;
}
config?: { dataTestSubj?: string }
): void;
/**
* @deprecated Use {@link ServerlessPluginStart.initNavigation} instead.