From 36e474d109ea6f9576a7b787c39e3a5367018d90 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Fri, 24 Feb 2023 04:23:56 +0000 Subject: [PATCH] PanelHeaderMenu: Use UI/Menu component (#63040) --- packages/grafana-data/src/types/panel.ts | 3 +- .../src/components/Menu/MenuItem.tsx | 2 +- .../src/components/Menu/SubMenu.tsx | 23 ++++++-- .../src/components/Menu/utils.test.ts | 12 ++-- .../grafana-ui/src/components/Menu/utils.ts | 18 ++---- .../components/PanelChrome/PanelChrome.tsx | 1 + .../containers/PublicDashboardPage.tsx | 2 +- .../dashboard/dashgrid/DashboardGrid.tsx | 2 + .../dashboard/dashgrid/DashboardPanel.tsx | 4 +- .../dashgrid/PanelHeader/PanelHeader.tsx | 4 +- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 23 ++++++++ .../PanelHeader/PanelHeaderMenuWrapper.tsx | 13 ++++- .../dashboard/dashgrid/PanelStateWrapper.tsx | 58 +++---------------- .../timeseries/plugins/ContextMenuPlugin.tsx | 7 +-- 14 files changed, 84 insertions(+), 88 deletions(-) diff --git a/packages/grafana-data/src/types/panel.ts b/packages/grafana-data/src/types/panel.ts index 9061fdf9e3e..6ff01c3fcee 100644 --- a/packages/grafana-data/src/types/panel.ts +++ b/packages/grafana-data/src/types/panel.ts @@ -12,6 +12,7 @@ import { LoadingState, PreferredVisualisationType } from './data'; import { DataFrame, FieldType } from './dataFrame'; import { DataQueryError, DataQueryRequest, DataQueryTimings } from './datasource'; import { FieldConfigSource } from './fieldOverrides'; +import { IconName } from './icon'; import { OptionEditorConfig } from './options'; import { PluginMeta } from './plugin'; import { AbsoluteTimeRange, TimeRange, TimeZone } from './time'; @@ -156,7 +157,7 @@ export interface PanelOptionsEditorConfig) => void; shortcut?: string; href?: string; diff --git a/packages/grafana-ui/src/components/Menu/MenuItem.tsx b/packages/grafana-ui/src/components/Menu/MenuItem.tsx index c511d402b2a..8a7f59b100e 100644 --- a/packages/grafana-ui/src/components/Menu/MenuItem.tsx +++ b/packages/grafana-ui/src/components/Menu/MenuItem.tsx @@ -30,7 +30,7 @@ export interface MenuItemProps { /** Url of the menu item */ url?: string; /** Handler for the click behaviour */ - onClick?: (event?: React.MouseEvent, payload?: T) => void; + onClick?: (event: React.MouseEvent, payload?: T) => void; /** Custom MenuItem styles*/ className?: string; /** Active */ diff --git a/packages/grafana-ui/src/components/Menu/SubMenu.tsx b/packages/grafana-ui/src/components/Menu/SubMenu.tsx index 5de7e13a1f6..56c8e3b3ad1 100644 --- a/packages/grafana-ui/src/components/Menu/SubMenu.tsx +++ b/packages/grafana-ui/src/components/Menu/SubMenu.tsx @@ -1,5 +1,5 @@ -import { css } from '@emotion/css'; -import React, { CSSProperties, ReactElement, useRef } from 'react'; +import { css, cx } from '@emotion/css'; +import React, { CSSProperties, ReactElement, useEffect, useRef, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -9,7 +9,7 @@ import { Icon } from '../Icon/Icon'; import { MenuItemProps } from './MenuItem'; import { useMenuFocus } from './hooks'; -import { getPosition } from './utils'; +import { isElementOverflowing } from './utils'; /** @internal */ export interface SubMenuProps { @@ -40,6 +40,13 @@ export const SubMenu: React.FC = React.memo( close, }); + const [pushLeft, setPushLeft] = useState(false); + useEffect(() => { + if (isOpen && localRef.current) { + setPushLeft(isElementOverflowing(localRef.current)); + } + }, [isOpen]); + return ( <>
@@ -48,7 +55,7 @@ export const SubMenu: React.FC = React.memo( {isOpen && (
@@ -83,11 +90,15 @@ const getStyles = (theme: GrafanaTheme2) => { display: inline-block; border-radius: ${theme.shape.borderRadius()}; `, - subMenu: (element: HTMLElement | null) => css` + pushLeft: css` + right: 100%; + left: unset; + `, + subMenu: css` position: absolute; top: 0; + left: 100%; z-index: ${theme.zIndex.dropdown}; - ${getPosition(element)}: 100%; `, }; }; diff --git a/packages/grafana-ui/src/components/Menu/utils.test.ts b/packages/grafana-ui/src/components/Menu/utils.test.ts index e71b99c9489..11ab59f1d6c 100644 --- a/packages/grafana-ui/src/components/Menu/utils.test.ts +++ b/packages/grafana-ui/src/components/Menu/utils.test.ts @@ -1,7 +1,7 @@ -import { getPosition } from './utils'; +import { isElementOverflowing } from './utils'; describe('utils', () => { - it('getPosition', () => { + it('isElementOverflowing', () => { const getElement = (right: number, width: number) => ({ parentElement: { @@ -12,9 +12,9 @@ describe('utils', () => { Object.defineProperty(window, 'innerWidth', { value: 1000 }); - expect(getPosition(null)).toBe('left'); - expect(getPosition(getElement(900, 100))).toBe('right'); - expect(getPosition(getElement(800, 100))).toBe('left'); - expect(getPosition(getElement(1200, 0))).toBe('left'); + expect(isElementOverflowing(null)).toBe(false); + expect(isElementOverflowing(getElement(900, 100))).toBe(true); + expect(isElementOverflowing(getElement(800, 100))).toBe(false); + expect(isElementOverflowing(getElement(1200, 0))).toBe(false); }); }); diff --git a/packages/grafana-ui/src/components/Menu/utils.ts b/packages/grafana-ui/src/components/Menu/utils.ts index 4b76779266b..212fe732342 100644 --- a/packages/grafana-ui/src/components/Menu/utils.ts +++ b/packages/grafana-ui/src/components/Menu/utils.ts @@ -1,23 +1,15 @@ /** - * Returns where the subMenu should be positioned (left or right) + * Returns whether the provided element overflows the viewport bounds * - * @param element HTMLElement for the subMenu wrapper + * @param element The element we want to know about */ -export const getPosition = (element: HTMLElement | null) => { +export const isElementOverflowing = (element: HTMLElement | null) => { if (!element) { - return 'left'; + return false; } const wrapperPos = element.parentElement!.getBoundingClientRect(); const pos = element.getBoundingClientRect(); - if (pos.width === 0) { - return 'left'; - } - - if (wrapperPos.right + pos.width + 10 > window.innerWidth) { - return 'right'; - } else { - return 'left'; - } + return pos.width !== 0 && wrapperPos.right + pos.width + 10 > window.innerWidth; }; diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index cbd0d387d6e..398cb586fc6 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -165,6 +165,7 @@ export function PanelChrome({ )} diff --git a/public/app/features/dashboard/containers/PublicDashboardPage.tsx b/public/app/features/dashboard/containers/PublicDashboardPage.tsx index 9c0bc278815..52ba5bc257a 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPage.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPage.tsx @@ -99,7 +99,7 @@ const PublicDashboardPage = (props: Props) => { > {dashboardState.initError && }
- +
diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 37646ea43a9..f75c2d6c27e 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -20,6 +20,7 @@ export interface Props { isEditable: boolean; editPanel: PanelModel | null; viewPanel: PanelModel | null; + hidePanelMenus?: boolean; } export interface State { @@ -196,6 +197,7 @@ export class DashboardGrid extends PureComponent { isViewing={panel.isViewing} width={width} height={height} + hideMenu={this.props.hidePanelMenus} /> ); } diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 4f76e07a792..d9731a1fb49 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -21,6 +21,7 @@ export interface OwnProps { height: number; lazy?: boolean; timezone?: string; + hideMenu?: boolean; } const mapStateToProps = (state: StoreState, props: OwnProps) => { @@ -71,7 +72,7 @@ export class DashboardPanelUnconnected extends PureComponent { }; renderPanel = (isInView: boolean) => { - const { dashboard, panel, isViewing, isEditing, width, height, plugin, timezone } = this.props; + const { dashboard, panel, isViewing, isEditing, width, height, plugin, timezone, hideMenu } = this.props; if (!plugin) { return null; @@ -104,6 +105,7 @@ export class DashboardPanelUnconnected extends PureComponent { height={height} onInstanceStateChange={this.onInstanceStateChange} timezone={timezone} + hideMenu={hideMenu} /> ); }; diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index 1d2967d6509..1b65818c5fa 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -63,9 +63,7 @@ export function PanelHeader({ panel, error, isViewing, isEditing, data, alertSta {!dashboard.meta.publicDashboardAccessToken && (
- {panelMenuOpen ? ( - - ) : null} + {panelMenuOpen ? : null}
)} {data.request && data.request.timeInfo && ( diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 9fbea527b5b..e476c638412 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -2,6 +2,7 @@ import classnames from 'classnames'; import React, { PureComponent } from 'react'; import { PanelMenuItem } from '@grafana/data'; +import { Menu } from '@grafana/ui'; import { PanelHeaderMenuItem } from './PanelHeaderMenuItem'; @@ -46,3 +47,25 @@ export class PanelHeaderMenu extends PureComponent { ); } } + +export function PanelHeaderMenuNew({ items }: Props) { + const renderItems = (items: PanelMenuItem[]) => { + return items.map((item) => + item.type === 'divider' ? ( + + ) : ( + + ) + ); + }; + + return {renderItems(items)}; +} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuWrapper.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuWrapper.tsx index f6ed09562ce..bdda1600431 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuWrapper.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuWrapper.tsx @@ -4,24 +4,23 @@ import { LoadingState } from '@grafana/data'; import { DashboardModel, PanelModel } from '../../state'; -import { PanelHeaderMenu } from './PanelHeaderMenu'; +import { PanelHeaderMenu, PanelHeaderMenuNew } from './PanelHeaderMenu'; import { PanelHeaderMenuProvider } from './PanelHeaderMenuProvider'; interface Props { panel: PanelModel; dashboard: DashboardModel; loadingState?: LoadingState; - onClose: () => void; style?: React.CSSProperties; menuItemsClassName?: string; menuWrapperClassName?: string; } export function PanelHeaderMenuWrapper({ - style, panel, dashboard, loadingState, + style, menuItemsClassName, menuWrapperClassName, }: Props) { @@ -38,3 +37,11 @@ export function PanelHeaderMenuWrapper({ ); } + +export function PanelHeaderMenuWrapperNew({ style, panel, dashboard, loadingState }: Props) { + return ( + + {({ items }) => } + + ); +} diff --git a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx index e49911328ac..e6d50b68379 100644 --- a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx +++ b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx @@ -1,4 +1,3 @@ -import { css } from '@emotion/css'; import classNames from 'classnames'; import React, { PureComponent } from 'react'; import { Subscription } from 'rxjs'; @@ -55,7 +54,7 @@ import { DashboardModel, PanelModel } from '../state'; import { loadSnapshotData } from '../utils/loadSnapshotData'; import { PanelHeader } from './PanelHeader/PanelHeader'; -import { PanelHeaderMenuWrapper } from './PanelHeader/PanelHeaderMenuWrapper'; +import { PanelHeaderMenuWrapperNew } from './PanelHeader/PanelHeaderMenuWrapper'; import { PanelHeaderTitleItems } from './PanelHeader/PanelHeaderTitleItems'; import { seriesVisibilityConfigFactory } from './SeriesVisibilityConfigFactory'; import { liveTimer } from './liveTimer'; @@ -73,6 +72,7 @@ export interface Props { height: number; onInstanceStateChange: (value: any) => void; timezone?: string; + hideMenu?: boolean; } export interface State { @@ -653,57 +653,17 @@ export class PanelStateWrapper extends PureComponent { /> ); - const overrideStyles: { menuItemsClassName?: string; menuWrapperClassName?: string; pos?: React.CSSProperties } = { - menuItemsClassName: undefined, - menuWrapperClassName: undefined, - pos: { top: 0, left: '-156px' }, - }; - - if (config.featureToggles.newPanelChromeUI) { - // set override styles - overrideStyles.menuItemsClassName = css` - width: inherit; - top: inherit; - left: inherit; - position: inherit; - float: inherit; - `; - overrideStyles.menuWrapperClassName = css` - position: inherit; - width: inherit; - top: inherit; - left: inherit; - float: inherit; - .dropdown-submenu > .dropdown-menu { - position: absolute; - } - `; - overrideStyles.pos = undefined; - } - - // custom styles is neeeded to override legacy panel-menu styles and prevent menu from being cut off - let menu; - if (!dashboard.meta.publicDashboardAccessToken) { - menu = ( -
- {}} - menuItemsClassName={overrideStyles.menuItemsClassName} - menuWrapperClassName={overrideStyles.menuWrapperClassName} - /> -
- ); - } - const dragClass = !(isViewing || isEditing) ? 'grid-drag-handle' : ''; if (config.featureToggles.newPanelChromeUI) { // Shift the hover menu down if it's on the top row so it doesn't get clipped by topnav const hoverHeaderOffset = (panel.gridPos?.y ?? 0) === 0 ? -16 : undefined; + const menu = ( +
+ +
+ ); + return ( { statusMessageOnClick={this.onOpenErrorInspect} description={!!panel.description ? this.onShowPanelDescription : undefined} titleItems={titleItems} - menu={menu} + menu={this.props.hideMenu ? undefined : menu} dragClass={dragClass} dragClassCancel="grid-drag-cancel" padding={padding} diff --git a/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx index 31d44bd3ef5..76a99cbfce6 100644 --- a/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx @@ -151,13 +151,12 @@ export const ContextMenuPlugin: React.FC = ({ items: i.items.map((j) => { return { ...j, - onClick: (e?: React.MouseEvent) => { + onClick: (e: React.MouseEvent) => { if (!coords) { return; } - if (j.onClick) { - j.onClick(e, { coords }); - } + + j.onClick?.(e, { coords }); }, }; }),