mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
d35ae396a6
commit
a12abe91dd
22 changed files with 202 additions and 296 deletions
|
@ -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: [
|
||||
|
|
|
@ -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<
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -31,7 +31,7 @@ describe('Active node', () => {
|
|||
title: 'Item 1',
|
||||
path: 'group1.item1',
|
||||
},
|
||||
],
|
||||
] as ChromeProjectNavigationNode[],
|
||||
]);
|
||||
|
||||
return activeNodes$;
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
() => ({
|
||||
|
|
|
@ -41,7 +41,6 @@ function serializeChildren(node: PanelSelectedNode): ChromeProjectNavigationNode
|
|||
return [
|
||||
{
|
||||
id: 'root',
|
||||
title: '',
|
||||
path: `${node.path}.root`,
|
||||
children: [...node.children],
|
||||
},
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -23,5 +23,3 @@ export interface PanelContent {
|
|||
title?: ReactNode | string;
|
||||
content?: ComponentType<PanelComponentProps>;
|
||||
}
|
||||
|
||||
export type ContentProvider = (nodeId: string) => PanelContent | void;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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$ }}
|
||||
/>
|
||||
));
|
||||
},
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue