PanelHeaderMenu: Use UI/Menu component (#63040)

This commit is contained in:
kay delaney 2023-02-24 04:23:56 +00:00 committed by GitHub
parent e77621649d
commit 36e474d109
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 84 additions and 88 deletions

View File

@ -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<TOptions, TSettings = any, TValue = an
export interface PanelMenuItem {
type?: 'submenu' | 'divider';
text: string;
iconClassName?: string;
iconClassName?: IconName;
onClick?: (event: React.MouseEvent<any>) => void;
shortcut?: string;
href?: string;

View File

@ -30,7 +30,7 @@ export interface MenuItemProps<T = any> {
/** Url of the menu item */
url?: string;
/** Handler for the click behaviour */
onClick?: (event?: React.MouseEvent<HTMLElement>, payload?: T) => void;
onClick?: (event: React.MouseEvent<HTMLElement>, payload?: T) => void;
/** Custom MenuItem styles*/
className?: string;
/** Active */

View File

@ -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<SubMenuProps> = React.memo(
close,
});
const [pushLeft, setPushLeft] = useState(false);
useEffect(() => {
if (isOpen && localRef.current) {
setPushLeft(isElementOverflowing(localRef.current));
}
}, [isOpen]);
return (
<>
<div className={styles.iconWrapper} aria-label={selectors.components.Menu.SubMenu.icon}>
@ -48,7 +55,7 @@ export const SubMenu: React.FC<SubMenuProps> = React.memo(
{isOpen && (
<div
ref={localRef}
className={styles.subMenu(localRef.current)}
className={cx(styles.subMenu, { [styles.pushLeft]: pushLeft })}
aria-label={selectors.components.Menu.SubMenu.container}
style={customStyle}
>
@ -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%;
`,
};
};

View File

@ -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);
});
});

View File

@ -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;
};

View File

@ -165,6 +165,7 @@ export function PanelChrome({
<PanelMenu
menu={menu}
title={title}
placement="bottom-end"
menuButtonClass={cx(styles.menuItem, dragClassCancel, 'show-on-hover')}
/>
)}

View File

@ -99,7 +99,7 @@ const PublicDashboardPage = (props: Props) => {
>
{dashboardState.initError && <DashboardFailed initError={dashboardState.initError} />}
<div className={styles.gridContainer}>
<DashboardGrid dashboard={dashboard} isEditable={false} viewPanel={null} editPanel={null} />
<DashboardGrid dashboard={dashboard} isEditable={false} viewPanel={null} editPanel={null} hidePanelMenus />
</div>
<PublicDashboardFooter />
</Page>

View File

@ -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<Props, State> {
isViewing={panel.isViewing}
width={width}
height={height}
hideMenu={this.props.hidePanelMenus}
/>
);
}

View File

@ -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<Props> {
};
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<Props> {
height={height}
onInstanceStateChange={this.onInstanceStateChange}
timezone={timezone}
hideMenu={hideMenu}
/>
);
};

View File

@ -63,9 +63,7 @@ export function PanelHeader({ panel, error, isViewing, isEditing, data, alertSta
{!dashboard.meta.publicDashboardAccessToken && (
<div data-testid="panel-dropdown">
<Icon name="angle-down" className="panel-menu-toggle" />
{panelMenuOpen ? (
<PanelHeaderMenuWrapper panel={panel} dashboard={dashboard} onClose={closeMenu} />
) : null}
{panelMenuOpen ? <PanelHeaderMenuWrapper panel={panel} dashboard={dashboard} /> : null}
</div>
)}
{data.request && data.request.timeInfo && (

View File

@ -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<Props> {
);
}
}
export function PanelHeaderMenuNew({ items }: Props) {
const renderItems = (items: PanelMenuItem[]) => {
return items.map((item) =>
item.type === 'divider' ? (
<Menu.Divider key={item.text} />
) : (
<Menu.Item
key={item.text}
label={item.text}
icon={item.iconClassName}
childItems={item.subMenu ? renderItems(item.subMenu) : undefined}
url={item.href}
onClick={item.onClick}
shortcut={item.shortcut}
/>
)
);
};
return <Menu>{renderItems(items)}</Menu>;
}

View File

@ -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({
</PanelHeaderMenuProvider>
);
}
export function PanelHeaderMenuWrapperNew({ style, panel, dashboard, loadingState }: Props) {
return (
<PanelHeaderMenuProvider panel={panel} dashboard={dashboard} loadingState={loadingState}>
{({ items }) => <PanelHeaderMenuNew style={style} items={items} />}
</PanelHeaderMenuProvider>
);
}

View File

@ -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<Props, State> {
/>
);
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 = (
<div data-testid="panel-dropdown">
<PanelHeaderMenuWrapper
style={overrideStyles.pos}
panel={panel}
dashboard={dashboard}
loadingState={data.state}
onClose={() => {}}
menuItemsClassName={overrideStyles.menuItemsClassName}
menuWrapperClassName={overrideStyles.menuWrapperClassName}
/>
</div>
);
}
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 = (
<div data-testid="panel-dropdown">
<PanelHeaderMenuWrapperNew panel={panel} dashboard={dashboard} loadingState={data.state} />
</div>
);
return (
<PanelChrome
width={width}
@ -714,7 +674,7 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
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}

View File

@ -151,13 +151,12 @@ export const ContextMenuPlugin: React.FC<ContextMenuPluginProps> = ({
items: i.items.map((j) => {
return {
...j,
onClick: (e?: React.MouseEvent<HTMLElement>) => {
onClick: (e: React.MouseEvent<HTMLElement>) => {
if (!coords) {
return;
}
if (j.onClick) {
j.onClick(e, { coords });
}
j.onClick?.(e, { coords });
},
};
}),