[SolutionSideNav] Add badge to all items except section header (#217301)

## Summary
This PR adds the ability add badge to all side nav items, except section
headers. Follow-up on https://github.com/elastic/kibana/pull/214854

![Screenshot 2025-04-07 at 12 24
29](https://github.com/user-attachments/assets/9ae2a610-1e56-4853-8214-ecb417bd4855)
This commit is contained in:
Krzysztof Kowalczyk 2025-04-07 21:37:39 +02:00 committed by GitHub
parent 9342cff262
commit 0d84936259
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 51 additions and 35 deletions

View file

@ -24,6 +24,7 @@ import {
EuiButton,
} from '@elastic/eui';
import type { ChromeProjectNavigationNode } from '@kbn/core-chrome-browser';
import { SubItemTitle } from './subitem_title';
import { useNavigation as useServices } from '../../services';
import { isActiveFromUrl } from '../../utils';
import type { NavigateToUrlFn } from '../../types';
@ -46,7 +47,11 @@ const getStyles = (euiTheme: EuiThemeComputed<{}>) => css`
}
`;
const getButtonStyles = (euiTheme: EuiThemeComputed<{}>, isActive: boolean) => css`
const getButtonStyles = (
euiTheme: EuiThemeComputed<{}>,
isActive: boolean,
withBadge?: boolean
) => css`
background-color: ${isActive ? transparentize(euiTheme.colors.lightShade, 0.5) : 'transparent'};
transform: none !important; /* don't translateY 1px */
color: inherit;
@ -56,14 +61,21 @@ const getButtonStyles = (euiTheme: EuiThemeComputed<{}>, isActive: boolean) => c
justify-content: flex-start;
position: relative;
}
& .euiIcon {
position: absolute;
right: 0;
top: 0;
transform: translateY(50%);
}
${!withBadge
? `
& .euiIcon {
position: absolute;
right: 0;
top: 0;
transform: translateY(50%);
}
`
: `
& .euiBetaBadge {
margin-left: -${euiTheme.size.m};
}
`}
`;
interface Props {
item: ChromeProjectNavigationNode;
navigateToUrl: NavigateToUrlFn;
@ -74,7 +86,7 @@ export const NavigationItemOpenPanel: FC<Props> = ({ item, navigateToUrl, active
const { euiTheme } = useEuiTheme();
const { open: openPanel, close: closePanel, selectedNode } = usePanel();
const { isSideNavCollapsed } = useServices();
const { title, deepLink, children } = item;
const { title, deepLink, children, withBadge } = item;
const { id, path } = item;
const href = deepLink?.url ?? item.href;
const isNotMobile = useIsWithinMinBreakpoint('s');
@ -89,7 +101,10 @@ export const NavigationItemOpenPanel: FC<Props> = ({ item, navigateToUrl, active
getStyles(euiTheme)
);
const buttonClassNames = classNames('sideNavItem', getButtonStyles(euiTheme, isActive));
const buttonClassNames = classNames(
'sideNavItem',
getButtonStyles(euiTheme, isActive, withBadge)
);
const dataTestSubj = classNames(`nav-item`, `nav-item-${path}`, {
[`nav-item-deepLinkId-${deepLink?.id}`]: !!deepLink,
@ -144,17 +159,17 @@ export const NavigationItemOpenPanel: FC<Props> = ({ item, navigateToUrl, active
className={buttonClassNames}
data-test-subj={dataTestSubj}
>
{title}
{withBadge ? <SubItemTitle item={item} /> : title}
</EuiButton>
);
}
return (
<EuiFlexGroup alignItems="center" gutterSize="xs">
<EuiFlexItem style={{ flexBasis: isIconVisible ? '80%' : '100%' }}>
<EuiFlexItem css={{ flexBasis: isIconVisible ? '80%' : '100%' }}>
<EuiListGroup gutterSize="none">
<EuiListGroupItem
label={title}
label={withBadge ? <SubItemTitle item={item} /> : title}
href={href}
wrapText
onClick={onLinkClick}
@ -166,7 +181,7 @@ export const NavigationItemOpenPanel: FC<Props> = ({ item, navigateToUrl, active
</EuiListGroup>
</EuiFlexItem>
{isIconVisible && (
<EuiFlexItem grow={0} style={{ flexBasis: '15%' }}>
<EuiFlexItem grow={0} css={{ flexBasis: '15%' }}>
<EuiButtonIcon
display={isExpanded ? 'base' : 'empty'}
size="s"

View file

@ -421,7 +421,7 @@ function nodeToEuiCollapsibleNavProps(
onClick,
icon: navNode.icon,
// @ts-expect-error title accepts JSX elements and they render correctly but the type definition expects a string
title: !subItems && navNode.withBadge ? <SubItemTitle item={navNode} /> : navNode.title,
title: navNode.withBadge ? <SubItemTitle item={navNode} /> : navNode.title,
['data-test-subj']: dataTestSubj,
iconProps: { size: deps.treeDepth === 0 ? 'm' : 's' },

View file

@ -22,6 +22,7 @@ import {
import { css } from '@emotion/css';
import type { ChromeProjectNavigationNode } from '@kbn/core-chrome-browser';
import { SubItemTitle } from '../subitem_title';
import { PanelNavItem } from './panel_nav_item';
const accordionButtonClassName = 'sideNavPanelAccordion__button';
@ -64,7 +65,7 @@ interface Props {
export const PanelGroup: FC<Props> = ({ navNode, isFirstInList, hasHorizontalRuleBefore }) => {
const { euiTheme } = useEuiTheme();
const { id, title, appendHorizontalRule, spaceBefore: _spaceBefore } = navNode;
const { id, title, appendHorizontalRule, spaceBefore: _spaceBefore, withBadge } = navNode;
const filteredChildren = navNode.children?.filter((child) => child.sideNavStatus !== 'hidden');
const classNames = getClassnames(euiTheme);
const hasTitle = !!title && title !== '';
@ -83,21 +84,18 @@ export const PanelGroup: FC<Props> = ({ navNode, isFirstInList, hasHorizontalRul
}
}
const renderChildren = useCallback(
({ parentIsAccordion } = { parentIsAccordion: false }) => {
if (!filteredChildren) return null;
const renderChildren = useCallback(() => {
if (!filteredChildren) return null;
return filteredChildren.map((item, i) => {
const isItem = item.renderAs === 'item' || !item.children;
return isItem ? (
<PanelNavItem key={item.id} item={item} parentIsAccordion={parentIsAccordion} />
) : (
<PanelGroup navNode={item} key={item.id} />
);
});
},
[filteredChildren]
);
return filteredChildren.map((item, i) => {
const isItem = item.renderAs === 'item' || !item.children;
return isItem ? (
<PanelNavItem key={item.id} item={item} />
) : (
<PanelGroup navNode={item} key={item.id} />
);
});
}, [filteredChildren]);
if (!filteredChildren?.length || !someChildIsVisible(filteredChildren)) {
return null;
@ -109,7 +107,7 @@ export const PanelGroup: FC<Props> = ({ navNode, isFirstInList, hasHorizontalRul
{spaceBefore !== null && <EuiSpacer size={spaceBefore} />}
<EuiAccordion
id={id}
buttonContent={title}
buttonContent={withBadge ? <SubItemTitle item={navNode} /> : title}
className={classNames.accordion}
buttonClassName={accordionButtonClassName}
data-test-subj={groupTestSubj}
@ -120,7 +118,7 @@ export const PanelGroup: FC<Props> = ({ navNode, isFirstInList, hasHorizontalRul
>
<>
{!firstChildIsGroup && <EuiSpacer size="s" />}
{renderChildren({ parentIsAccordion: true })}
{renderChildren()}
</>
</EuiAccordion>
{appendHorizontalRule && <EuiHorizontalRule margin="xs" />}

View file

@ -19,10 +19,9 @@ import { usePanel } from './context';
interface Props {
item: ChromeProjectNavigationNode;
parentIsAccordion?: boolean;
}
export const PanelNavItem: FC<Props> = ({ item, parentIsAccordion }) => {
export const PanelNavItem: FC<Props> = ({ item }) => {
const { navigateToUrl } = useServices();
const { close: closePanel } = usePanel();
const { id, icon, deepLink, openInNewTab, isExternalLink, renderItem } = item;
@ -46,7 +45,7 @@ export const PanelNavItem: FC<Props> = ({ item, parentIsAccordion }) => {
) : (
<EuiListGroupItem
key={id}
label={parentIsAccordion ? <SubItemTitle item={item} /> : item.title}
label={<SubItemTitle item={item} />}
wrapText
className={classNames(
'sideNavPanelLink',

View file

@ -113,6 +113,7 @@ const generalLayoutNavTree: NavigationTreeDefinitionUI = {
href: '/app/kibana',
icon: 'iInCircle',
isExternalLink: true,
withBadge: true,
},
{
id: 'item02',
@ -229,6 +230,7 @@ const generalLayoutNavTree: NavigationTreeDefinitionUI = {
title: 'Item 19',
icon: 'iInCircle',
renderAs: 'accordion',
withBadge: true,
children: [
{
id: 'sub1',
@ -294,6 +296,7 @@ const generalLayoutNavTree: NavigationTreeDefinitionUI = {
path: '',
icon: 'iInCircle',
renderAs: 'panelOpener',
withBadge: true,
children: [
{
id: 'sub1',
@ -411,6 +414,7 @@ const generalLayoutNavTree: NavigationTreeDefinitionUI = {
path: '',
renderAs: 'accordion',
icon: 'iInCircle',
withBadge: true,
children: [
{
id: 'item-beta',