DockedMegaMenu: Refactor and rename to simplify (#75872)

* tidy up some styles

* remove NavBarMenuItemWrapper + consolidate components

* lots of renaming

* use object syntax in FeatureHighlight

* fix a couple of missing find+replace

* adjust li positioning

* fix text truncation

* bit more tidy up

* refactor indent into it's own component

* memoize styles in Indent
This commit is contained in:
Ashley Harrison 2023-10-03 13:03:27 +01:00 committed by GitHub
parent 523d1b46d4
commit 18b237879d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 132 additions and 221 deletions

View File

@ -1185,9 +1185,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "5"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"] [0, 0, 0, "Unexpected any. Specify a different type.", "6"]
], ],
"public/app/core/components/AppChrome/DockedMegaMenu/NavFeatureHighlight.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/core/components/AppChrome/MegaMenu/NavFeatureHighlight.tsx:5381": [ "public/app/core/components/AppChrome/MegaMenu/NavFeatureHighlight.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"] [0, 0, 0, "Styles should be written using objects.", "0"]
], ],

View File

@ -10,7 +10,7 @@ import { useStyles2, useTheme2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext'; import { useGrafana } from 'app/core/context/GrafanaContext';
import { KioskMode } from 'app/types'; import { KioskMode } from 'app/types';
import { DockedMegaMenu, MENU_WIDTH } from './DockedMegaMenu/DockedMegaMenu'; import { MegaMenu, MENU_WIDTH } from './DockedMegaMenu/MegaMenu';
import { TOGGLE_BUTTON_ID } from './NavToolbar/NavToolbar'; import { TOGGLE_BUTTON_ID } from './NavToolbar/NavToolbar';
import { TOP_BAR_LEVEL_HEIGHT } from './types'; import { TOP_BAR_LEVEL_HEIGHT } from './types';
@ -58,7 +58,7 @@ export function AppChromeMenu({}: Props) {
timeout={{ enter: animationSpeed, exit: 0 }} timeout={{ enter: animationSpeed, exit: 0 }}
> >
<FocusScope contain autoFocus> <FocusScope contain autoFocus>
<DockedMegaMenu className={styles.menu} onClose={onClose} ref={ref} {...overlayProps} {...dialogProps} /> <MegaMenu className={styles.menu} onClose={onClose} ref={ref} {...overlayProps} {...dialogProps} />
</FocusScope> </FocusScope>
</CSSTransition> </CSSTransition>
<CSSTransition <CSSTransition

View File

@ -8,7 +8,7 @@ export interface Props {
children: JSX.Element; children: JSX.Element;
} }
export const NavFeatureHighlight = ({ children }: Props): JSX.Element => { export const FeatureHighlight = ({ children }: Props): JSX.Element => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
return ( return (
<div> <div>
@ -20,15 +20,15 @@ export const NavFeatureHighlight = ({ children }: Props): JSX.Element => {
const getStyles = (theme: GrafanaTheme2) => { const getStyles = (theme: GrafanaTheme2) => {
return { return {
highlight: css` highlight: css({
background-color: ${theme.colors.success.main}; backgroundColor: theme.colors.success.main,
border-radius: ${theme.shape.radius.circle}; borderRadius: theme.shape.radius.circle,
width: 6px; width: '6px',
height: 6px; height: '6px',
display: inline-block; display: 'inline-block;',
position: absolute; position: 'absolute',
top: 50%; top: '50%',
transform: translateY(-50%); transform: 'translateY(-50%)',
`, }),
}; };
}; };

View File

@ -9,7 +9,7 @@ import { locationService } from '@grafana/runtime';
import { TestProvider } from '../../../../../test/helpers/TestProvider'; import { TestProvider } from '../../../../../test/helpers/TestProvider';
import { DockedMegaMenu } from './DockedMegaMenu'; import { MegaMenu } from './MegaMenu';
const setup = () => { const setup = () => {
const navBarTree: NavModelItem[] = [ const navBarTree: NavModelItem[] = [
@ -40,7 +40,7 @@ const setup = () => {
return render( return render(
<TestProvider storeState={{ navBarTree }} grafanaContext={grafanaContext}> <TestProvider storeState={{ navBarTree }} grafanaContext={grafanaContext}>
<Router history={locationService.getHistory()}> <Router history={locationService.getHistory()}>
<DockedMegaMenu onClose={() => {}} /> <MegaMenu onClose={() => {}} />
</Router> </Router>
</TestProvider> </TestProvider>
); );

View File

@ -8,7 +8,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { CustomScrollbar, Icon, IconButton, useStyles2 } from '@grafana/ui'; import { CustomScrollbar, Icon, IconButton, useStyles2 } from '@grafana/ui';
import { useSelector } from 'app/types'; import { useSelector } from 'app/types';
import { NavBarMenuItemWrapper } from './NavBarMenuItemWrapper'; import { MegaMenuItem } from './MegaMenuItem';
import { enrichWithInteractionTracking, getActiveItem } from './utils'; import { enrichWithInteractionTracking, getActiveItem } from './utils';
export const MENU_WIDTH = '350px'; export const MENU_WIDTH = '350px';
@ -17,7 +17,7 @@ export interface Props extends DOMAttributes {
onClose: () => void; onClose: () => void;
} }
export const DockedMegaMenu = React.memo( export const MegaMenu = React.memo(
forwardRef<HTMLDivElement, Props>(({ onClose, ...restProps }, ref) => { forwardRef<HTMLDivElement, Props>(({ onClose, ...restProps }, ref) => {
const navBarTree = useSelector((state) => state.navBarTree); const navBarTree = useSelector((state) => state.navBarTree);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
@ -49,7 +49,7 @@ export const DockedMegaMenu = React.memo(
<CustomScrollbar showScrollIndicators hideHorizontalTrack> <CustomScrollbar showScrollIndicators hideHorizontalTrack>
<ul className={styles.itemList}> <ul className={styles.itemList}>
{navItems.map((link) => ( {navItems.map((link) => (
<NavBarMenuItemWrapper link={link} onClose={onClose} activeItem={activeItem} key={link.text} /> <MegaMenuItem link={link} onClose={onClose} activeItem={activeItem} key={link.text} />
))} ))}
</ul> </ul>
</CustomScrollbar> </CustomScrollbar>
@ -59,7 +59,7 @@ export const DockedMegaMenu = React.memo(
}) })
); );
DockedMegaMenu.displayName = 'DockedMegaMenu'; MegaMenu.displayName = 'MegaMenu';
const getStyles = (theme: GrafanaTheme2) => ({ const getStyles = (theme: GrafanaTheme2) => ({
content: css({ content: css({
@ -82,6 +82,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
display: 'grid', display: 'grid',
gridAutoRows: `minmax(${theme.spacing(6)}, auto)`, gridAutoRows: `minmax(${theme.spacing(6)}, auto)`,
gridTemplateColumns: `minmax(${MENU_WIDTH}, auto)`, gridTemplateColumns: `minmax(${MENU_WIDTH}, auto)`,
listStyleType: 'none',
minWidth: MENU_WIDTH, minWidth: MENU_WIDTH,
}), }),
}); });

View File

@ -3,38 +3,36 @@ import React from 'react';
import { useLocalStorage } from 'react-use'; import { useLocalStorage } from 'react-use';
import { GrafanaTheme2, NavModelItem } from '@grafana/data'; import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { Button, Icon, useStyles2 } from '@grafana/ui'; import { Button, Icon, useStyles2, Text } from '@grafana/ui';
import { NavBarItemIcon } from './NavBarItemIcon'; import { Indent } from '../../Indent/Indent';
import { NavBarMenuItem } from './NavBarMenuItem';
import { NavFeatureHighlight } from './NavFeatureHighlight'; import { FeatureHighlight } from './FeatureHighlight';
import { MegaMenuItemIcon } from './MegaMenuItemIcon';
import { MegaMenuItemText } from './MegaMenuItemText';
import { hasChildMatch } from './utils'; import { hasChildMatch } from './utils';
export function NavBarMenuSection({ interface Props {
link,
activeItem,
children,
className,
onClose,
}: {
link: NavModelItem; link: NavModelItem;
activeItem?: NavModelItem; activeItem?: NavModelItem;
children: React.ReactNode;
className?: string;
onClose?: () => void; onClose?: () => void;
}) { level?: number;
}
export function MegaMenuItem({ link, activeItem, level = 0, onClose }: Props) {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const FeatureHighlightWrapper = link.highlightText ? NavFeatureHighlight : React.Fragment; const FeatureHighlightWrapper = link.highlightText ? FeatureHighlight : React.Fragment;
const isActive = link === activeItem; const isActive = link === activeItem;
const hasActiveChild = hasChildMatch(link, activeItem); const hasActiveChild = hasChildMatch(link, activeItem);
const [sectionExpanded, setSectionExpanded] = const [sectionExpanded, setSectionExpanded] =
useLocalStorage(`grafana.navigation.expanded[${link.text}]`, false) ?? Boolean(hasActiveChild); useLocalStorage(`grafana.navigation.expanded[${link.text}]`, false) ?? Boolean(hasActiveChild);
const showExpandButton = linkHasChildren(link) || link.emptyMessage;
return ( return (
<> <li>
<div className={cx(styles.collapsibleSectionWrapper, className)}> <div className={styles.collapsibleSectionWrapper}>
<NavBarMenuItem <MegaMenuItemText
isActive={link === activeItem} isActive={isActive}
onClick={() => { onClick={() => {
link.onClick?.(); link.onClick?.();
onClose?.(); onClose?.();
@ -49,12 +47,13 @@ export function NavBarMenuSection({
})} })}
> >
<FeatureHighlightWrapper> <FeatureHighlightWrapper>
<NavBarItemIcon link={link} /> <div className={styles.iconWrapper}>{level === 0 && <MegaMenuItemIcon link={link} />}</div>
</FeatureHighlightWrapper> </FeatureHighlightWrapper>
{link.text} <Indent level={Math.max(0, level - 1)} spacing={2} />
<Text truncate>{link.text}</Text>
</div> </div>
</NavBarMenuItem> </MegaMenuItemText>
{children && ( {showExpandButton && (
<Button <Button
aria-label={`${sectionExpanded ? 'Collapse' : 'Expand'} section ${link.text}`} aria-label={`${sectionExpanded ? 'Collapse' : 'Expand'} section ${link.text}`}
variant="secondary" variant="secondary"
@ -66,12 +65,35 @@ export function NavBarMenuSection({
</Button> </Button>
)} )}
</div> </div>
{sectionExpanded && children} {showExpandButton && sectionExpanded && (
</> <ul className={styles.children}>
{linkHasChildren(link) ? (
link.children
.filter((childLink) => !childLink.isCreateAction)
.map((childLink) => (
<MegaMenuItem
key={`${link.text}-${childLink.text}`}
link={childLink}
activeItem={activeItem}
onClose={onClose}
level={level + 1}
/>
))
) : (
<div className={styles.emptyMessage}>{link.emptyMessage}</div>
)}
</ul>
)}
</li>
); );
} }
const getStyles = (theme: GrafanaTheme2) => ({ const getStyles = (theme: GrafanaTheme2) => ({
children: css({
display: 'flex',
listStyleType: 'none',
flexDirection: 'column',
}),
collapsibleSectionWrapper: css({ collapsibleSectionWrapper: css({
alignItems: 'center', alignItems: 'center',
display: 'flex', display: 'flex',
@ -81,18 +103,22 @@ const getStyles = (theme: GrafanaTheme2) => ({
padding: theme.spacing(0, 0.5), padding: theme.spacing(0, 0.5),
marginRight: theme.spacing(1), marginRight: theme.spacing(1),
}), }),
collapseWrapperActive: css({ emptyMessage: css({
backgroundColor: theme.colors.action.disabledBackground, color: theme.colors.text.secondary,
fontStyle: 'italic',
padding: theme.spacing(1, 1.5, 1, 7),
}), }),
collapseContent: css({ iconWrapper: css({
padding: 0, display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
}), }),
labelWrapper: css({ labelWrapper: css({
display: 'grid', display: 'grid',
fontSize: theme.typography.pxToRem(14), fontSize: theme.typography.pxToRem(14),
gridAutoFlow: 'column', gridAutoFlow: 'column',
gridTemplateColumns: `${theme.spacing(7)} auto`, gridTemplateColumns: `${theme.spacing(7)} auto`,
placeItems: 'center', alignItems: 'center',
fontWeight: theme.typography.fontWeightMedium, fontWeight: theme.typography.fontWeightMedium,
}), }),
isActive: css({ isActive: css({
@ -115,3 +141,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
color: theme.colors.text.primary, color: theme.colors.text.primary,
}), }),
}); });
function linkHasChildren(link: NavModelItem): link is NavModelItem & { children: NavModelItem[] } {
return Boolean(link.children && link.children.length > 0);
}

View File

@ -10,7 +10,7 @@ interface NavBarItemIconProps {
link: NavModelItem; link: NavModelItem;
} }
export function NavBarItemIcon({ link }: NavBarItemIconProps) { export function MegaMenuItemIcon({ link }: NavBarItemIconProps) {
const theme = useTheme2(); const theme = useTheme2();
const styles = getStyles(theme); const styles = getStyles(theme);

View File

@ -15,7 +15,7 @@ export interface Props {
url?: string; url?: string;
} }
export function NavBarMenuItem({ children, icon, isActive, isChild, onClick, target, url }: Props) { export function MegaMenuItemText({ children, icon, isActive, isChild, onClick, target, url }: Props) {
const theme = useTheme2(); const theme = useTheme2();
const styles = getStyles(theme, isActive, isChild); const styles = getStyles(theme, isActive, isChild);
@ -23,7 +23,7 @@ export function NavBarMenuItem({ children, icon, isActive, isChild, onClick, tar
<div className={styles.linkContent}> <div className={styles.linkContent}>
{icon && <Icon data-testid="dropdown-child-icon" name={icon} />} {icon && <Icon data-testid="dropdown-child-icon" name={icon} />}
<div className={styles.linkText}>{children}</div> {children}
{target === '_blank' && ( {target === '_blank' && (
<Icon data-testid="external-link-icon" name="external-link-alt" className={styles.externalLinkIcon} /> <Icon data-testid="external-link-icon" name="external-link-alt" className={styles.externalLinkIcon} />
@ -66,10 +66,10 @@ export function NavBarMenuItem({ children, icon, isActive, isChild, onClick, tar
); );
} }
return <li className={styles.listItem}>{element}</li>; return <div className={styles.wrapper}>{element}</div>;
} }
NavBarMenuItem.displayName = 'NavBarMenuItem'; MegaMenuItemText.displayName = 'MegaMenuItemText';
const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive'], isChild: Props['isActive']) => ({ const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive'], isChild: Props['isActive']) => ({
button: css({ button: css({
@ -83,11 +83,6 @@ const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive'], isChild: P
height: '100%', height: '100%',
width: '100%', width: '100%',
}), }),
linkText: css({
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
}),
externalLinkIcon: css({ externalLinkIcon: css({
color: theme.colors.text.secondary, color: theme.colors.text.secondary,
}), }),
@ -127,7 +122,7 @@ const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive'], isChild: P
backgroundImage: theme.colors.gradients.brandVertical, backgroundImage: theme.colors.gradients.brandVertical,
}, },
}), }),
listItem: css({ wrapper: css({
boxSizing: 'border-box', boxSizing: 'border-box',
position: 'relative', position: 'relative',
display: 'flex', display: 'flex',

View File

@ -1,112 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { NavBarMenuItem } from './NavBarMenuItem';
import { NavBarMenuSection } from './NavBarMenuSection';
import { isMatchOrChildMatch } from './utils';
export function NavBarMenuItemWrapper({
link,
activeItem,
onClose,
}: {
link: NavModelItem;
activeItem?: NavModelItem;
onClose: () => void;
}) {
const styles = useStyles2(getStyles);
if (link.emptyMessage && !linkHasChildren(link)) {
return (
<NavBarMenuSection onClose={onClose} link={link} activeItem={activeItem}>
<ul className={styles.children}>
<div className={styles.emptyMessage}>{link.emptyMessage}</div>
</ul>
</NavBarMenuSection>
);
}
return (
<NavBarMenuSection onClose={onClose} link={link} activeItem={activeItem}>
{linkHasChildren(link) && (
<ul className={styles.children}>
{link.children.map((childLink) => {
return linkHasChildren(childLink) ? (
<NavBarMenuItemWrapper
key={`${link.text}-${childLink.text}`}
link={childLink}
activeItem={activeItem}
onClose={onClose}
/>
) : (
!childLink.isCreateAction && (
<NavBarMenuItem
key={`${link.text}-${childLink.text}`}
isActive={isMatchOrChildMatch(childLink, activeItem)}
isChild
onClick={() => {
childLink.onClick?.();
onClose();
}}
target={childLink.target}
url={childLink.url}
>
{childLink.text}
</NavBarMenuItem>
)
);
})}
</ul>
)}
</NavBarMenuSection>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
children: css({
display: 'flex',
flexDirection: 'column',
}),
flex: css({
display: 'flex',
}),
itemWithoutMenu: css({
position: 'relative',
placeItems: 'inherit',
justifyContent: 'start',
display: 'flex',
flexGrow: 1,
alignItems: 'center',
}),
fullWidth: css({
height: '100%',
width: '100%',
}),
iconContainer: css({
display: 'flex',
placeContent: 'center',
}),
itemWithoutMenuContent: css({
display: 'grid',
gridAutoFlow: 'column',
gridTemplateColumns: `${theme.spacing(7)} auto`,
alignItems: 'center',
height: '100%',
}),
linkText: css({
fontSize: theme.typography.pxToRem(14),
justifySelf: 'start',
}),
emptyMessage: css({
color: theme.colors.text.secondary,
fontStyle: 'italic',
padding: theme.spacing(1, 1.5, 1, 7),
}),
});
function linkHasChildren(link: NavModelItem): link is NavModelItem & { children: NavModelItem[] } {
return Boolean(link.children && link.children.length > 0);
}

View File

@ -0,0 +1,26 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2, ThemeSpacingTokens } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { getResponsiveStyle, ResponsiveProp } from '@grafana/ui/src/components/Layout/utils/responsiveness';
interface IndentProps {
children?: React.ReactNode;
level: number;
spacing: ResponsiveProp<ThemeSpacingTokens>;
}
export function Indent({ children, spacing, level }: IndentProps) {
const styles = useStyles2(getStyles, spacing, level);
return <span className={css(styles.indentor)}>{children}</span>;
}
const getStyles = (theme: GrafanaTheme2, spacing: IndentProps['spacing'], level: IndentProps['level']) => ({
indentor: css(
getResponsiveStyle(theme, spacing, (val) => ({
paddingLeft: theme.spacing(val * level),
}))
),
});

View File

@ -8,8 +8,8 @@ import { GrafanaTheme2 } from '@grafana/data';
import { IconButton, useStyles2 } from '@grafana/ui'; import { IconButton, useStyles2 } from '@grafana/ui';
import { getSvgSize } from '@grafana/ui/src/components/Icon/utils'; import { getSvgSize } from '@grafana/ui/src/components/Icon/utils';
import { Text } from '@grafana/ui/src/components/Text/Text'; import { Text } from '@grafana/ui/src/components/Text/Text';
import { Indent } from 'app/core/components/Indent/Indent';
import { Trans } from 'app/core/internationalization'; import { Trans } from 'app/core/internationalization';
import { Indent } from 'app/features/browse-dashboards/components/Indent';
import { childrenByParentUIDSelector, rootItemsSelector } from 'app/features/browse-dashboards/state'; import { childrenByParentUIDSelector, rootItemsSelector } from 'app/features/browse-dashboards/state';
import { DashboardsTreeItem } from 'app/features/browse-dashboards/types'; import { DashboardsTreeItem } from 'app/features/browse-dashboards/types';
import { DashboardViewItem } from 'app/features/search/types'; import { DashboardViewItem } from 'app/features/search/types';
@ -153,7 +153,7 @@ function Row({ index, style: virtualStyles, data }: RowProps) {
if (item.kind === 'ui' && item.uiKind === 'pagination-placeholder') { if (item.kind === 'ui' && item.uiKind === 'pagination-placeholder') {
return ( return (
<span style={virtualStyles} className={styles.row}> <span style={virtualStyles} className={styles.row}>
<Indent level={level} /> <Indent level={level} spacing={2} />
<Skeleton width={SKELETON_WIDTHS[index % SKELETON_WIDTHS.length]} /> <Skeleton width={SKELETON_WIDTHS[index % SKELETON_WIDTHS.length]} />
</span> </span>
); );
@ -190,7 +190,7 @@ function Row({ index, style: virtualStyles, data }: RowProps) {
id={getDOMId(idPrefix, item.uid)} id={getDOMId(idPrefix, item.uid)}
> >
<div className={styles.rowBody}> <div className={styles.rowBody}>
<Indent level={level} /> <Indent level={level} spacing={2} />
{foldersAreOpenable ? ( {foldersAreOpenable ? (
<IconButton <IconButton
size={CHEVRON_SIZE} size={CHEVRON_SIZE}

View File

@ -10,13 +10,7 @@ import { useStyles2 } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization'; import { t, Trans } from 'app/core/internationalization';
import { DashboardViewItem } from 'app/features/search/types'; import { DashboardViewItem } from 'app/features/search/types';
import { import { DashboardsTreeCellProps, DashboardsTreeColumn, DashboardsTreeItem, SelectionState } from '../types';
DashboardsTreeCellProps,
DashboardsTreeColumn,
DashboardsTreeItem,
INDENT_AMOUNT_CSS_VAR,
SelectionState,
} from '../types';
import CheckboxCell from './CheckboxCell'; import CheckboxCell from './CheckboxCell';
import CheckboxHeaderCell from './CheckboxHeaderCell'; import CheckboxHeaderCell from './CheckboxHeaderCell';
@ -126,7 +120,7 @@ export function DashboardsTree({
); );
return ( return (
<div {...getTableProps()} className={styles.tableRoot} role="table"> <div {...getTableProps()} role="table">
{headerGroups.map((headerGroup) => { {headerGroups.map((headerGroup) => {
const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps({ const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps({
style: { width }, style: { width },
@ -213,15 +207,6 @@ function VirtualListRow({ index, style, data }: VirtualListRowProps) {
const getStyles = (theme: GrafanaTheme2) => { const getStyles = (theme: GrafanaTheme2) => {
return { return {
tableRoot: css({
// Responsively
[INDENT_AMOUNT_CSS_VAR]: theme.spacing(1),
[theme.breakpoints.up('md')]: {
[INDENT_AMOUNT_CSS_VAR]: theme.spacing(3),
},
}),
// Column flex properties (cell sizing) are set by customFlexTableLayout.ts // Column flex properties (cell sizing) are set by customFlexTableLayout.ts
row: css({ row: css({

View File

@ -1,20 +0,0 @@
import React from 'react';
import { useTheme2 } from '@grafana/ui';
import { INDENT_AMOUNT_CSS_VAR } from '../types';
interface IndentProps {
children?: React.ReactNode;
level: number;
}
export function Indent({ children, level }: IndentProps) {
const theme = useTheme2();
// DashboardsTree responsively sets the value of INDENT_AMOUNT_CSS_VAR
// but we also have a fallback just in case it's not set for some reason...
const space = `var(${INDENT_AMOUNT_CSS_VAR}, ${theme.spacing(2)})`;
return <span style={{ paddingLeft: `calc(${space} * ${level})` }}>{children}</span>;
}

View File

@ -9,11 +9,10 @@ import { Icon, IconButton, Link, Spinner, useStyles2, Text } from '@grafana/ui';
import { getSvgSize } from '@grafana/ui/src/components/Icon/utils'; import { getSvgSize } from '@grafana/ui/src/components/Icon/utils';
import { getIconForKind } from 'app/features/search/service/utils'; import { getIconForKind } from 'app/features/search/service/utils';
import { Indent } from '../../../core/components/Indent/Indent';
import { useChildrenByParentUIDState } from '../state'; import { useChildrenByParentUIDState } from '../state';
import { DashboardsTreeItem } from '../types'; import { DashboardsTreeItem } from '../types';
import { Indent } from './Indent';
const CHEVRON_SIZE = 'md'; const CHEVRON_SIZE = 'md';
const ICON_SIZE = 'sm'; const ICON_SIZE = 'sm';
@ -31,7 +30,13 @@ export function NameCell({ row: { original: data }, onFolderClick }: NameCellPro
if (item.kind === 'ui') { if (item.kind === 'ui') {
return ( return (
<> <>
<Indent level={level} /> <Indent
level={level}
spacing={{
xs: 1,
md: 3,
}}
/>
<span className={styles.folderButtonSpacer} /> <span className={styles.folderButtonSpacer} />
{item.uiKind === 'empty-folder' ? ( {item.uiKind === 'empty-folder' ? (
<em className={styles.emptyText}> <em className={styles.emptyText}>
@ -48,7 +53,13 @@ export function NameCell({ row: { original: data }, onFolderClick }: NameCellPro
return ( return (
<> <>
<Indent level={level} /> <Indent
level={level}
spacing={{
xs: 1,
md: 3,
}}
/>
{item.kind === 'folder' ? ( {item.kind === 'folder' ? (
<IconButton <IconButton

View File

@ -42,8 +42,6 @@ export interface DashboardsTreeItem<T extends DashboardViewItemWithUIItems = Das
parentUID?: string; parentUID?: string;
} }
export const INDENT_AMOUNT_CSS_VAR = '--dashboards-tree-indentation';
interface RendererUserProps { interface RendererUserProps {
// Note: userProps for cell renderers (e.g. second argument in `cell.render('Cell', foo)` ) // Note: userProps for cell renderers (e.g. second argument in `cell.render('Cell', foo)` )
// aren't typed, so we must be careful when accessing this // aren't typed, so we must be careful when accessing this