[Solution Side Nav] Misc UI fixes (#216109)

Part of https://github.com/elastic/kibana-team/issues/1439
Pulled from https://github.com/elastic/kibana/pull/210893
https://github.com/elastic/kibana/pull/215969

## Summary

1. Allow item in the secondary panel to use the `renderItem` field
2. Fix handling of `defaultIsCollapsed` for items in the secondary panel
3. Allow secondary panel to contain a mix of ungrouped items as well as
sub-groups of items


![alksdjnfgklsdfhglskdhkds](https://github.com/user-attachments/assets/11d316d6-6c9a-4743-897f-93c40efa9013)

4. Fix the flagging of the "active" parent in the main nav panel, based
on the current URL


![jhgkdfgkjfhkhn](https://github.com/user-attachments/assets/b5f6efe3-e8f5-494b-bc12-abbd51acc12a)

### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Tim Sullivan 2025-03-28 09:12:12 -07:00 committed by GitHub
parent 369a43b2c2
commit 05a8703d48
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 186 additions and 43 deletions

View file

@ -500,5 +500,120 @@ describe('Panel', () => {
expect(queryByTestId(/panelNavItem-id-item1/)).toBeVisible();
expect(queryByTestId(/panelNavItem-id-item3/)).toBeVisible();
});
test('allows panel to contain a mix of ungrouped items and grouped items', () => {
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: 'item0',
title: 'Item 0',
href: '/app/item0',
path: 'root.group1.foo.item0',
},
{
id: 'foo',
title: 'Group 1',
path: 'root.group1.foo',
children: [
{
id: 'item1',
href: '/app/item1',
path: 'root.group1.foo.item1',
title: 'Item 1',
},
{
id: 'item2',
href: '/app/item2',
path: 'root.group1.foo.item2',
title: 'Item 2',
},
],
},
],
},
],
},
],
};
const { queryByTestId } = renderNavigation({
navTreeDef: of(navTree),
});
queryByTestId(/panelOpener-root.group1/)?.click(); // open the panel
expect(queryByTestId(/panelGroupId-foo/)).toBeVisible(); // no crash
});
test('allows panel items to use custom rendering', () => {
const componentSpy = jest.fn();
const Custom: React.FC = () => {
componentSpy();
return <>Hello</>;
};
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: 'foo',
title: 'Group 1',
path: 'root.group1.foo',
children: [
{
id: 'item1',
title: 'Item 1',
path: 'root.group1.foo.item1',
renderItem: () => {
return <Custom />;
},
},
],
},
],
},
],
},
],
};
const { queryByTestId } = renderNavigation({
navTreeDef: of(navTree),
});
expect(componentSpy).not.toHaveBeenCalled();
queryByTestId(/panelOpener-root.group1/)?.click(); // open the panel
expect(componentSpy).toHaveBeenCalled();
});
});
});

View file

@ -81,7 +81,7 @@ export const NavigationItemOpenPanel: FC<Props> = ({ item, navigateToUrl, active
const isIconVisible = isNotMobile && !isSideNavCollapsed && !!children && children.length > 0;
const hasLandingPage = Boolean(href);
const isExpanded = selectedNode?.path === path;
const isActive = hasLandingPage ? isActiveFromUrl(item.path, activeNodes) : isExpanded;
const isActive = isActiveFromUrl(item.path, activeNodes) || isExpanded;
const itemClassNames = classNames(
'sideNavItem',

View file

@ -14,10 +14,6 @@ import React, { Fragment, type FC } from 'react';
import { PanelGroup } from './panel_group';
import { PanelNavItem } from './panel_nav_item';
function isGroupNode({ children }: Pick<ChromeProjectNavigationNode, 'children'>) {
return children !== undefined;
}
function isItemNode({ children }: Pick<ChromeProjectNavigationNode, 'children'>) {
return children === undefined;
}
@ -35,12 +31,12 @@ function isItemNode({ children }: Pick<ChromeProjectNavigationNode, 'children'>)
function serializeChildren(node: PanelSelectedNode): ChromeProjectNavigationNode[] | undefined {
if (!node.children) return undefined;
const allChildrenAreItems = node.children.every((_node) => {
const someChildrenAreItems = node.children.some((_node) => {
if (isItemNode(_node)) return true;
return _node.renderAs === 'item';
});
if (allChildrenAreItems) {
if (someChildrenAreItems) {
// Automatically wrap all the children into top level "root" group.
return [
{
@ -52,17 +48,6 @@ function serializeChildren(node: PanelSelectedNode): ChromeProjectNavigationNode
];
}
const allChildrenAreGroups = node.children.every((_node) => {
if (_node.renderAs === 'item') return false;
return isGroupNode(_node);
});
if (!allChildrenAreGroups) {
throw new Error(
`[Chrome navigation] Error in node [${node.id}]. Children must either all be "groups" or all "items" but not a mix of both.`
);
}
return node.children;
}

View file

@ -113,6 +113,7 @@ export const PanelGroup: FC<Props> = ({ navNode, isFirstInList, hasHorizontalRul
className={classNames.accordion}
buttonClassName={accordionButtonClassName}
data-test-subj={groupTestSubj}
initialIsOpen={navNode.defaultIsCollapsed === false}
buttonProps={{
'data-test-subj': `panelAccordionBtnId-${navNode.id}`,
}}

View file

@ -25,7 +25,7 @@ interface Props {
export const PanelNavItem: FC<Props> = ({ item, parentIsAccordion }) => {
const { navigateToUrl } = useServices();
const { close: closePanel } = usePanel();
const { id, icon, deepLink, openInNewTab } = item;
const { id, icon, deepLink, openInNewTab, renderItem } = item;
const href = deepLink?.url ?? item.href;
const { euiTheme } = useEuiTheme();
@ -40,7 +40,9 @@ export const PanelNavItem: FC<Props> = ({ item, parentIsAccordion }) => {
[closePanel, href, navigateToUrl]
);
return (
return renderItem ? (
renderItem()
) : (
<EuiListGroupItem
key={id}
label={parentIsAccordion ? <SubItemTitle item={item} /> : item.title}

View file

@ -13,11 +13,15 @@ import { of } from 'rxjs';
import {
EuiButton,
EuiCallOut,
EuiCollapsibleNavBeta,
EuiCollapsibleNavBetaProps,
EuiFlexGroup,
EuiFlexItem,
EuiHeader,
EuiHeaderSection,
EuiPageTemplate,
EuiSpacer,
} from '@elastic/eui';
import type {
@ -128,28 +132,38 @@ const generalLayoutNavTree: NavigationTreeDefinitionUI = {
icon: 'iInCircle',
renderAs: 'panelOpener',
children: [
// FIXME: group mixed with items causes crash
// {
// id: 'sub1',
// path: '',
// title: 'Item 11',
// href: '/app/kibana',
// icon: 'iInCircle',
// },
// {
// id: 'sub2',
// path: '',
// title: 'Item 12',
// href: '/app/kibana',
// icon: 'iInCircle',
// },
// {
// id: 'sub3',
// path: '',
// title: 'Item 13',
// href: '/app/kibana',
// icon: 'iInCircle',
// },
{
id: 'sub0',
path: '',
title: 'This text is not shown',
renderItem: () => (
<>
<p>This panel contains a mix of ungrouped items and grouped items</p>
<EuiSpacer />
</>
),
},
{
id: 'sub1',
path: '',
title: 'Item 11',
href: '/app/kibana',
icon: 'iInCircle',
},
{
id: 'sub2',
path: '',
title: 'Item 12',
href: '/app/kibana',
icon: 'iInCircle',
},
{
id: 'sub3',
path: '',
title: 'Item 13',
href: '/app/kibana',
icon: 'iInCircle',
},
{
id: 'child-section1',
path: '',
@ -240,6 +254,7 @@ const generalLayoutNavTree: NavigationTreeDefinitionUI = {
path: '',
icon: 'iInCircle',
renderAs: 'accordion',
defaultIsCollapsed: false,
children: [
{
id: 'sub1',
@ -272,7 +287,7 @@ const generalLayoutNavTree: NavigationTreeDefinitionUI = {
},
{
id: 'item05',
title: 'Item 05',
title: 'Item 05, with custom',
path: '',
icon: 'iInCircle',
renderAs: 'panelOpener',
@ -284,6 +299,31 @@ const generalLayoutNavTree: NavigationTreeDefinitionUI = {
href: '/app/kibana',
icon: 'iInCircle',
},
{
id: 'spacer1',
path: '',
title: 'This text is not shown.',
renderItem: () => {
return <EuiSpacer />;
},
},
{
id: 'callout1',
path: '',
title: 'This text is not shown.',
renderItem: () => {
return (
<EuiCallOut title="Check it out" iconType="cluster">
<EuiFlexGroup justifyContent="spaceAround" direction="column">
<EuiFlexItem>
<p>Choose an integration to start</p>
<EuiButton>Browse integrations</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiCallOut>
);
},
},
],
},
],