From 18b237879d31542ccad272606b2bcb59a2fda838 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 3 Oct 2023 13:03:27 +0100 Subject: [PATCH] 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 --- .betterer.results | 3 - .../components/AppChrome/AppChromeMenu.tsx | 4 +- ...tureHighlight.tsx => FeatureHighlight.tsx} | 22 ++-- ...kedMegaMenu.test.tsx => MegaMenu.test.tsx} | 4 +- .../{DockedMegaMenu.tsx => MegaMenu.tsx} | 9 +- ...NavBarMenuSection.tsx => MegaMenuItem.tsx} | 90 +++++++++----- ...avBarItemIcon.tsx => MegaMenuItemIcon.tsx} | 2 +- ...avBarMenuItem.tsx => MegaMenuItemText.tsx} | 15 +-- .../DockedMegaMenu/NavBarMenuItemWrapper.tsx | 112 ------------------ public/app/core/components/Indent/Indent.tsx | 26 ++++ .../NestedFolderPicker/NestedFolderList.tsx | 6 +- .../components/DashboardsTree.tsx | 19 +-- .../browse-dashboards/components/Indent.tsx | 20 ---- .../browse-dashboards/components/NameCell.tsx | 19 ++- .../app/features/browse-dashboards/types.ts | 2 - 15 files changed, 132 insertions(+), 221 deletions(-) rename public/app/core/components/AppChrome/DockedMegaMenu/{NavFeatureHighlight.tsx => FeatureHighlight.tsx} (53%) rename public/app/core/components/AppChrome/DockedMegaMenu/{DockedMegaMenu.test.tsx => MegaMenu.test.tsx} (96%) rename public/app/core/components/AppChrome/DockedMegaMenu/{DockedMegaMenu.tsx => MegaMenu.tsx} (90%) rename public/app/core/components/AppChrome/DockedMegaMenu/{NavBarMenuSection.tsx => MegaMenuItem.tsx} (53%) rename public/app/core/components/AppChrome/DockedMegaMenu/{NavBarItemIcon.tsx => MegaMenuItemIcon.tsx} (93%) rename public/app/core/components/AppChrome/DockedMegaMenu/{NavBarMenuItem.tsx => MegaMenuItemText.tsx} (89%) delete mode 100644 public/app/core/components/AppChrome/DockedMegaMenu/NavBarMenuItemWrapper.tsx create mode 100644 public/app/core/components/Indent/Indent.tsx delete mode 100644 public/app/features/browse-dashboards/components/Indent.tsx diff --git a/.betterer.results b/.betterer.results index 247f135992f..251f4c90001 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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.", "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": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], diff --git a/public/app/core/components/AppChrome/AppChromeMenu.tsx b/public/app/core/components/AppChrome/AppChromeMenu.tsx index e8c0404f366..ab164b0639d 100644 --- a/public/app/core/components/AppChrome/AppChromeMenu.tsx +++ b/public/app/core/components/AppChrome/AppChromeMenu.tsx @@ -10,7 +10,7 @@ import { useStyles2, useTheme2 } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; 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 { TOP_BAR_LEVEL_HEIGHT } from './types'; @@ -58,7 +58,7 @@ export function AppChromeMenu({}: Props) { timeout={{ enter: animationSpeed, exit: 0 }} > - + { +export const FeatureHighlight = ({ children }: Props): JSX.Element => { const styles = useStyles2(getStyles); return (
@@ -20,15 +20,15 @@ export const NavFeatureHighlight = ({ children }: Props): JSX.Element => { const getStyles = (theme: GrafanaTheme2) => { return { - highlight: css` - background-color: ${theme.colors.success.main}; - border-radius: ${theme.shape.radius.circle}; - width: 6px; - height: 6px; - display: inline-block; - position: absolute; - top: 50%; - transform: translateY(-50%); - `, + highlight: css({ + backgroundColor: theme.colors.success.main, + borderRadius: theme.shape.radius.circle, + width: '6px', + height: '6px', + display: 'inline-block;', + position: 'absolute', + top: '50%', + transform: 'translateY(-50%)', + }), }; }; diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/DockedMegaMenu.test.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.test.tsx similarity index 96% rename from public/app/core/components/AppChrome/DockedMegaMenu/DockedMegaMenu.test.tsx rename to public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.test.tsx index 7edfb40b882..ad5dd4e8bb0 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/DockedMegaMenu.test.tsx +++ b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.test.tsx @@ -9,7 +9,7 @@ import { locationService } from '@grafana/runtime'; import { TestProvider } from '../../../../../test/helpers/TestProvider'; -import { DockedMegaMenu } from './DockedMegaMenu'; +import { MegaMenu } from './MegaMenu'; const setup = () => { const navBarTree: NavModelItem[] = [ @@ -40,7 +40,7 @@ const setup = () => { return render( - {}} /> + {}} /> ); diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/DockedMegaMenu.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx similarity index 90% rename from public/app/core/components/AppChrome/DockedMegaMenu/DockedMegaMenu.tsx rename to public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx index 30e6e30aa2e..6915a9a2270 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/DockedMegaMenu.tsx +++ b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx @@ -8,7 +8,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { CustomScrollbar, Icon, IconButton, useStyles2 } from '@grafana/ui'; import { useSelector } from 'app/types'; -import { NavBarMenuItemWrapper } from './NavBarMenuItemWrapper'; +import { MegaMenuItem } from './MegaMenuItem'; import { enrichWithInteractionTracking, getActiveItem } from './utils'; export const MENU_WIDTH = '350px'; @@ -17,7 +17,7 @@ export interface Props extends DOMAttributes { onClose: () => void; } -export const DockedMegaMenu = React.memo( +export const MegaMenu = React.memo( forwardRef(({ onClose, ...restProps }, ref) => { const navBarTree = useSelector((state) => state.navBarTree); const styles = useStyles2(getStyles); @@ -49,7 +49,7 @@ export const DockedMegaMenu = React.memo(
    {navItems.map((link) => ( - + ))}
@@ -59,7 +59,7 @@ export const DockedMegaMenu = React.memo( }) ); -DockedMegaMenu.displayName = 'DockedMegaMenu'; +MegaMenu.displayName = 'MegaMenu'; const getStyles = (theme: GrafanaTheme2) => ({ content: css({ @@ -82,6 +82,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: 'grid', gridAutoRows: `minmax(${theme.spacing(6)}, auto)`, gridTemplateColumns: `minmax(${MENU_WIDTH}, auto)`, + listStyleType: 'none', minWidth: MENU_WIDTH, }), }); diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/NavBarMenuSection.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItem.tsx similarity index 53% rename from public/app/core/components/AppChrome/DockedMegaMenu/NavBarMenuSection.tsx rename to public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItem.tsx index 978181f6b08..c99000e5431 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/NavBarMenuSection.tsx +++ b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItem.tsx @@ -3,38 +3,36 @@ import React from 'react'; import { useLocalStorage } from 'react-use'; 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 { NavBarMenuItem } from './NavBarMenuItem'; -import { NavFeatureHighlight } from './NavFeatureHighlight'; +import { Indent } from '../../Indent/Indent'; + +import { FeatureHighlight } from './FeatureHighlight'; +import { MegaMenuItemIcon } from './MegaMenuItemIcon'; +import { MegaMenuItemText } from './MegaMenuItemText'; import { hasChildMatch } from './utils'; -export function NavBarMenuSection({ - link, - activeItem, - children, - className, - onClose, -}: { +interface Props { link: NavModelItem; activeItem?: NavModelItem; - children: React.ReactNode; - className?: string; onClose?: () => void; -}) { + level?: number; +} + +export function MegaMenuItem({ link, activeItem, level = 0, onClose }: Props) { const styles = useStyles2(getStyles); - const FeatureHighlightWrapper = link.highlightText ? NavFeatureHighlight : React.Fragment; + const FeatureHighlightWrapper = link.highlightText ? FeatureHighlight : React.Fragment; const isActive = link === activeItem; const hasActiveChild = hasChildMatch(link, activeItem); const [sectionExpanded, setSectionExpanded] = useLocalStorage(`grafana.navigation.expanded[${link.text}]`, false) ?? Boolean(hasActiveChild); + const showExpandButton = linkHasChildren(link) || link.emptyMessage; return ( - <> -
- +
+ { link.onClick?.(); onClose?.(); @@ -49,12 +47,13 @@ export function NavBarMenuSection({ })} > - +
{level === 0 && }
- {link.text} + + {link.text}
-
- {children && ( + + {showExpandButton && (
- {sectionExpanded && children} - + {showExpandButton && sectionExpanded && ( +
    + {linkHasChildren(link) ? ( + link.children + .filter((childLink) => !childLink.isCreateAction) + .map((childLink) => ( + + )) + ) : ( +
    {link.emptyMessage}
    + )} +
+ )} + ); } const getStyles = (theme: GrafanaTheme2) => ({ + children: css({ + display: 'flex', + listStyleType: 'none', + flexDirection: 'column', + }), collapsibleSectionWrapper: css({ alignItems: 'center', display: 'flex', @@ -81,18 +103,22 @@ const getStyles = (theme: GrafanaTheme2) => ({ padding: theme.spacing(0, 0.5), marginRight: theme.spacing(1), }), - collapseWrapperActive: css({ - backgroundColor: theme.colors.action.disabledBackground, + emptyMessage: css({ + color: theme.colors.text.secondary, + fontStyle: 'italic', + padding: theme.spacing(1, 1.5, 1, 7), }), - collapseContent: css({ - padding: 0, + iconWrapper: css({ + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', }), labelWrapper: css({ display: 'grid', fontSize: theme.typography.pxToRem(14), gridAutoFlow: 'column', gridTemplateColumns: `${theme.spacing(7)} auto`, - placeItems: 'center', + alignItems: 'center', fontWeight: theme.typography.fontWeightMedium, }), isActive: css({ @@ -115,3 +141,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ color: theme.colors.text.primary, }), }); + +function linkHasChildren(link: NavModelItem): link is NavModelItem & { children: NavModelItem[] } { + return Boolean(link.children && link.children.length > 0); +} diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/NavBarItemIcon.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItemIcon.tsx similarity index 93% rename from public/app/core/components/AppChrome/DockedMegaMenu/NavBarItemIcon.tsx rename to public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItemIcon.tsx index b259eb97660..b33f616fa7b 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/NavBarItemIcon.tsx +++ b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItemIcon.tsx @@ -10,7 +10,7 @@ interface NavBarItemIconProps { link: NavModelItem; } -export function NavBarItemIcon({ link }: NavBarItemIconProps) { +export function MegaMenuItemIcon({ link }: NavBarItemIconProps) { const theme = useTheme2(); const styles = getStyles(theme); diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/NavBarMenuItem.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItemText.tsx similarity index 89% rename from public/app/core/components/AppChrome/DockedMegaMenu/NavBarMenuItem.tsx rename to public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItemText.tsx index 4829d36701a..ec0480a0d8c 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/NavBarMenuItem.tsx +++ b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItemText.tsx @@ -15,7 +15,7 @@ export interface Props { 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 styles = getStyles(theme, isActive, isChild); @@ -23,7 +23,7 @@ export function NavBarMenuItem({ children, icon, isActive, isChild, onClick, tar
{icon && } -
{children}
+ {children} {target === '_blank' && ( @@ -66,10 +66,10 @@ export function NavBarMenuItem({ children, icon, isActive, isChild, onClick, tar ); } - return
  • {element}
  • ; + return
    {element}
    ; } -NavBarMenuItem.displayName = 'NavBarMenuItem'; +MegaMenuItemText.displayName = 'MegaMenuItemText'; const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive'], isChild: Props['isActive']) => ({ button: css({ @@ -83,11 +83,6 @@ const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive'], isChild: P height: '100%', width: '100%', }), - linkText: css({ - textOverflow: 'ellipsis', - overflow: 'hidden', - whiteSpace: 'nowrap', - }), externalLinkIcon: css({ color: theme.colors.text.secondary, }), @@ -127,7 +122,7 @@ const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive'], isChild: P backgroundImage: theme.colors.gradients.brandVertical, }, }), - listItem: css({ + wrapper: css({ boxSizing: 'border-box', position: 'relative', display: 'flex', diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/NavBarMenuItemWrapper.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/NavBarMenuItemWrapper.tsx deleted file mode 100644 index 04e4e010576..00000000000 --- a/public/app/core/components/AppChrome/DockedMegaMenu/NavBarMenuItemWrapper.tsx +++ /dev/null @@ -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 ( - -
      -
      {link.emptyMessage}
      -
    -
    - ); - } - - return ( - - {linkHasChildren(link) && ( -
      - {link.children.map((childLink) => { - return linkHasChildren(childLink) ? ( - - ) : ( - !childLink.isCreateAction && ( - { - childLink.onClick?.(); - onClose(); - }} - target={childLink.target} - url={childLink.url} - > - {childLink.text} - - ) - ); - })} -
    - )} -
    - ); -} - -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); -} diff --git a/public/app/core/components/Indent/Indent.tsx b/public/app/core/components/Indent/Indent.tsx new file mode 100644 index 00000000000..3f4647a4df3 --- /dev/null +++ b/public/app/core/components/Indent/Indent.tsx @@ -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; +} + +export function Indent({ children, spacing, level }: IndentProps) { + const styles = useStyles2(getStyles, spacing, level); + + return {children}; +} + +const getStyles = (theme: GrafanaTheme2, spacing: IndentProps['spacing'], level: IndentProps['level']) => ({ + indentor: css( + getResponsiveStyle(theme, spacing, (val) => ({ + paddingLeft: theme.spacing(val * level), + })) + ), +}); diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx index 36c69fc02bf..f0874362c93 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx @@ -8,8 +8,8 @@ import { GrafanaTheme2 } from '@grafana/data'; import { IconButton, useStyles2 } from '@grafana/ui'; import { getSvgSize } from '@grafana/ui/src/components/Icon/utils'; import { Text } from '@grafana/ui/src/components/Text/Text'; +import { Indent } from 'app/core/components/Indent/Indent'; 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 { DashboardsTreeItem } from 'app/features/browse-dashboards/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') { return ( - + ); @@ -190,7 +190,7 @@ function Row({ index, style: virtualStyles, data }: RowProps) { id={getDOMId(idPrefix, item.uid)} >
    - + {foldersAreOpenable ? ( +
    {headerGroups.map((headerGroup) => { const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps({ style: { width }, @@ -213,15 +207,6 @@ function VirtualListRow({ index, style, data }: VirtualListRowProps) { const getStyles = (theme: GrafanaTheme2) => { 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 row: css({ diff --git a/public/app/features/browse-dashboards/components/Indent.tsx b/public/app/features/browse-dashboards/components/Indent.tsx deleted file mode 100644 index d8edb362b6f..00000000000 --- a/public/app/features/browse-dashboards/components/Indent.tsx +++ /dev/null @@ -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 {children}; -} diff --git a/public/app/features/browse-dashboards/components/NameCell.tsx b/public/app/features/browse-dashboards/components/NameCell.tsx index 59d50312645..69ee47be2f2 100644 --- a/public/app/features/browse-dashboards/components/NameCell.tsx +++ b/public/app/features/browse-dashboards/components/NameCell.tsx @@ -9,11 +9,10 @@ import { Icon, IconButton, Link, Spinner, useStyles2, Text } from '@grafana/ui'; import { getSvgSize } from '@grafana/ui/src/components/Icon/utils'; import { getIconForKind } from 'app/features/search/service/utils'; +import { Indent } from '../../../core/components/Indent/Indent'; import { useChildrenByParentUIDState } from '../state'; import { DashboardsTreeItem } from '../types'; -import { Indent } from './Indent'; - const CHEVRON_SIZE = 'md'; const ICON_SIZE = 'sm'; @@ -31,7 +30,13 @@ export function NameCell({ row: { original: data }, onFolderClick }: NameCellPro if (item.kind === 'ui') { return ( <> - + {item.uiKind === 'empty-folder' ? ( @@ -48,7 +53,13 @@ export function NameCell({ row: { original: data }, onFolderClick }: NameCellPro return ( <> - + {item.kind === 'folder' ? (