mirror of
https://github.com/grafana/grafana.git
synced 2024-11-26 02:40:26 -06:00
PanelHeaderMenu: Use UI/Menu component (#63040)
This commit is contained in:
parent
e77621649d
commit
36e474d109
@ -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;
|
||||
|
@ -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 */
|
||||
|
@ -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%;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -165,6 +165,7 @@ export function PanelChrome({
|
||||
<PanelMenu
|
||||
menu={menu}
|
||||
title={title}
|
||||
placement="bottom-end"
|
||||
menuButtonClass={cx(styles.menuItem, dragClassCancel, 'show-on-hover')}
|
||||
/>
|
||||
)}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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 && (
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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 });
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
Loading…
Reference in New Issue
Block a user