mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
523d1b46d4
commit
18b237879d
@ -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"]
|
||||||
],
|
],
|
||||||
|
@ -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
|
||||||
|
@ -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%)',
|
||||||
`,
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
@ -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>
|
||||||
);
|
);
|
@ -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,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
@ -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);
|
||||||
|
}
|
@ -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);
|
||||||
|
|
@ -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',
|
@ -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);
|
|
||||||
}
|
|
26
public/app/core/components/Indent/Indent.tsx
Normal file
26
public/app/core/components/Indent/Indent.tsx
Normal 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),
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
});
|
@ -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}
|
||||||
|
@ -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({
|
||||||
|
@ -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>;
|
|
||||||
}
|
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user