mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PanelChrome: Implement hover header (#61774)
Closes #59078 Co-authored-by: polinaboneva <polina.boneva@grafana.com> Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com> Co-authored-by: Alexandra Vargas <alexa1866@gmail.com>
This commit is contained in:
parent
ba731f7865
commit
40ec4ef5b8
@ -1,6 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { FocusScope } from '@react-aria/focus';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { usePopperTooltip } from 'react-popper-tooltip';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
|
||||
@ -12,12 +12,19 @@ export interface Props {
|
||||
overlay: React.ReactElement | (() => React.ReactElement);
|
||||
placement?: TooltipPlacement;
|
||||
children: React.ReactElement | ((isOpen: boolean) => React.ReactElement);
|
||||
/** Amount in pixels to nudge the dropdown vertically and horizontally, respectively. */
|
||||
offset?: [number, number];
|
||||
onVisibleChange?: (state: boolean) => void;
|
||||
}
|
||||
|
||||
export const Dropdown = React.memo(({ children, overlay, placement }: Props) => {
|
||||
export const Dropdown = React.memo(({ children, overlay, placement, offset, onVisibleChange }: Props) => {
|
||||
const [show, setShow] = useState(false);
|
||||
const transitionRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
onVisibleChange?.(show);
|
||||
}, [onVisibleChange, show]);
|
||||
|
||||
const { getArrowProps, getTooltipProps, setTooltipRef, setTriggerRef, visible } = usePopperTooltip({
|
||||
visible: show,
|
||||
placement: placement,
|
||||
@ -25,7 +32,7 @@ export const Dropdown = React.memo(({ children, overlay, placement }: Props) =>
|
||||
interactive: true,
|
||||
delayHide: 0,
|
||||
delayShow: 0,
|
||||
offset: [0, 8],
|
||||
offset: offset ?? [0, 8],
|
||||
trigger: ['click'],
|
||||
});
|
||||
|
||||
|
108
packages/grafana-ui/src/components/PanelChrome/HoverWidget.tsx
Normal file
108
packages/grafana-ui/src/components/PanelChrome/HoverWidget.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import { css } from '@emotion/css';
|
||||
import classnames from 'classnames';
|
||||
import React, { ReactElement, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
import { PanelMenu } from './PanelMenu';
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
menu: ReactElement | (() => ReactElement);
|
||||
title?: string;
|
||||
offset?: number;
|
||||
dragClass?: string;
|
||||
}
|
||||
|
||||
export function HoverWidget({ menu, title, dragClass, children, offset = -32 }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const draggableRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Capture the pointer to keep the widget visible while dragging
|
||||
const onPointerDown = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
||||
draggableRef.current?.setPointerCapture(e.pointerId);
|
||||
}, []);
|
||||
|
||||
const onPointerUp = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
||||
draggableRef.current?.releasePointerCapture(e.pointerId);
|
||||
}, []);
|
||||
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
if (children === undefined || React.Children.count(children) === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(styles.container, { 'show-on-hover': !menuOpen })}
|
||||
style={{ top: `${offset}px` }}
|
||||
data-testid="hover-header-container"
|
||||
>
|
||||
<div
|
||||
className={classnames(styles.square, styles.draggable, dragClass)}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerUp={onPointerUp}
|
||||
ref={draggableRef}
|
||||
>
|
||||
<Icon name="draggabledots" />
|
||||
</div>
|
||||
{children}
|
||||
<div className={styles.square}>
|
||||
<PanelMenu
|
||||
menu={menu}
|
||||
title={title}
|
||||
placement="bottom"
|
||||
menuButtonClass={styles.menuButton}
|
||||
onVisibleChange={setMenuOpen}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
hidden: css({
|
||||
visibility: 'hidden',
|
||||
opacity: '0',
|
||||
}),
|
||||
container: css({
|
||||
label: 'hover-container-widget',
|
||||
transition: `all .1s linear`,
|
||||
display: 'flex',
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
boxSizing: 'border-box',
|
||||
alignItems: 'center',
|
||||
background: theme.colors.background.secondary,
|
||||
color: theme.colors.text.primary,
|
||||
border: `1px solid ${theme.colors.border.weak}`,
|
||||
borderRadius: '1px',
|
||||
height: theme.spacing(4),
|
||||
boxShadow: theme.shadows.z1,
|
||||
}),
|
||||
square: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: theme.spacing(4),
|
||||
height: '100%',
|
||||
}),
|
||||
draggable: css({
|
||||
cursor: 'move',
|
||||
}),
|
||||
menuButton: css({
|
||||
color: theme.colors.text.primary,
|
||||
'&:hover': {
|
||||
background: 'inherit',
|
||||
},
|
||||
}),
|
||||
title: css({
|
||||
padding: theme.spacing(0.75),
|
||||
}),
|
||||
};
|
||||
}
|
@ -60,22 +60,12 @@ it('renders panel with a header if prop leftItems', () => {
|
||||
expect(screen.getByTestId('header-container')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// todo implement when hoverHeader is implemented
|
||||
it.skip('renders panel without header if no title, no leftItems, and hoverHeader is undefined', () => {
|
||||
setup();
|
||||
|
||||
expect(screen.getByTestId('header-container')).toBeInTheDocument();
|
||||
it('renders panel with hover header if no title, no leftItems, hoverHeader is undefined but menu is present', () => {
|
||||
setup({ title: '', leftItems: undefined, hoverHeader: undefined, menu: <div>Menu</div> });
|
||||
expect(screen.getByTestId('hover-header-container')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// todo implement when hoverHeader is implemented
|
||||
it.skip('renders panel with a fixed header if prop hoverHeader is false', () => {
|
||||
setup({ hoverHeader: false });
|
||||
|
||||
expect(screen.getByTestId('header-container')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// todo implement when hoverHeader is implemented
|
||||
it.skip('renders panel with a hovering header if prop hoverHeader is true', () => {
|
||||
it('renders panel with a hovering header if prop hoverHeader is true', () => {
|
||||
setup({ title: 'Test Panel Header', hoverHeader: true });
|
||||
|
||||
expect(screen.queryByTestId('header-container')).not.toBeInTheDocument();
|
||||
@ -97,10 +87,10 @@ it('renders panel with a header with icons in place if prop titleItems', () => {
|
||||
expect(screen.getByTestId('title-items-container')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders panel with a header if prop menu', () => {
|
||||
setup({ menu: <div> Menu </div> });
|
||||
it('renders panel with a hover header if prop menu is present and hoverHeader is false', () => {
|
||||
setup({ menu: <div> Menu </div>, hoverHeader: false });
|
||||
|
||||
expect(screen.getByTestId('header-container')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('hover-header-container')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders panel with a show-on-hover menu icon if prop menu', () => {
|
||||
|
@ -5,13 +5,13 @@ import { GrafanaTheme2, LoadingState } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { useStyles2, useTheme2 } from '../../themes';
|
||||
import { Dropdown } from '../Dropdown/Dropdown';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { LoadingBar } from '../LoadingBar/LoadingBar';
|
||||
import { ToolbarButton } from '../ToolbarButton';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
|
||||
import { HoverWidget } from './HoverWidget';
|
||||
import { PanelDescription } from './PanelDescription';
|
||||
import { PanelMenu } from './PanelMenu';
|
||||
import { PanelStatus } from './PanelStatus';
|
||||
import { TitleItem } from './TitleItem';
|
||||
|
||||
@ -23,9 +23,10 @@ export interface PanelChromeProps {
|
||||
height: number;
|
||||
children: (innerWidth: number, innerHeight: number) => ReactNode;
|
||||
padding?: PanelPadding;
|
||||
hoverHeaderOffset?: number;
|
||||
title?: string;
|
||||
description?: string | (() => string);
|
||||
titleItems?: ReactNode[];
|
||||
titleItems?: ReactNode;
|
||||
menu?: ReactElement | (() => ReactElement);
|
||||
dragClass?: string;
|
||||
dragClassCancel?: string;
|
||||
@ -70,11 +71,12 @@ export function PanelChrome({
|
||||
title = '',
|
||||
description = '',
|
||||
displayMode = 'default',
|
||||
titleItems = [],
|
||||
titleItems,
|
||||
menu,
|
||||
dragClass,
|
||||
dragClassCancel,
|
||||
hoverHeader = false,
|
||||
hoverHeaderOffset,
|
||||
loadingState,
|
||||
statusMessage,
|
||||
statusMessageOnClick,
|
||||
@ -91,7 +93,7 @@ export function PanelChrome({
|
||||
const hasHeader =
|
||||
hoverHeader === false &&
|
||||
(title.length > 0 ||
|
||||
titleItems.length > 0 ||
|
||||
titleItems !== undefined ||
|
||||
description !== '' ||
|
||||
loadingState === LoadingState.Streaming ||
|
||||
(leftItems?.length ?? 0) > 0);
|
||||
@ -105,71 +107,83 @@ export function PanelChrome({
|
||||
};
|
||||
|
||||
const containerStyles: CSSProperties = { width, height };
|
||||
const ariaLabel = title ? selectors.components.Panels.Panel.containerByTitle(title) : 'Panel';
|
||||
|
||||
if (displayMode === 'transparent') {
|
||||
containerStyles.backgroundColor = 'transparent';
|
||||
containerStyles.border = 'none';
|
||||
}
|
||||
|
||||
const ariaLabel = title ? selectors.components.Panels.Panel.containerByTitle(title) : 'Panel';
|
||||
|
||||
const headerContent = (
|
||||
<>
|
||||
{title && (
|
||||
<h6 title={title} className={styles.title}>
|
||||
{title}
|
||||
</h6>
|
||||
)}
|
||||
|
||||
<PanelDescription description={description} className={dragClassCancel} />
|
||||
|
||||
{titleItems !== undefined && (
|
||||
<div className={cx(styles.titleItems, dragClassCancel)} data-testid="title-items-container">
|
||||
{titleItems}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadingState === LoadingState.Streaming && (
|
||||
<Tooltip content="Streaming">
|
||||
<TitleItem className={dragClassCancel} data-testid="panel-streaming">
|
||||
<Icon name="circle-mono" size="md" className={styles.streaming} />
|
||||
</TitleItem>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container} style={containerStyles} aria-label={ariaLabel}>
|
||||
<div
|
||||
className={cx(styles.container, { [styles.regularHeader]: hasHeader })}
|
||||
style={containerStyles}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<div className={styles.loadingBarContainer}>
|
||||
{loadingState === LoadingState.Loading ? (
|
||||
<LoadingBar width={'28%'} height={'2px'} ariaLabel="Panel loading bar" />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className={cx(styles.headerContainer, dragClass)} style={headerStyles} data-testid="header-container">
|
||||
{title && (
|
||||
<h6 title={title} className={styles.title}>
|
||||
{title}
|
||||
</h6>
|
||||
)}
|
||||
{(hoverHeader || !hasHeader) && menu && (
|
||||
<HoverWidget menu={menu} title={title} offset={hoverHeaderOffset} dragClass={dragClass}>
|
||||
{headerContent}
|
||||
</HoverWidget>
|
||||
)}
|
||||
|
||||
<PanelDescription description={description} className={dragClassCancel} />
|
||||
{hasHeader && (
|
||||
<div className={cx(styles.headerContainer, dragClass)} style={headerStyles} data-testid="header-container">
|
||||
{headerContent}
|
||||
|
||||
{titleItems.length > 0 && (
|
||||
<div className={cx(styles.titleItems, dragClassCancel)} data-testid="title-items-container">
|
||||
{titleItems.map((item) => item)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadingState === LoadingState.Streaming && (
|
||||
<Tooltip content="Streaming">
|
||||
<TitleItem className={dragClassCancel} data-testid="panel-streaming">
|
||||
<Icon name="circle-mono" size="md" className={styles.streaming} />
|
||||
</TitleItem>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<div className={styles.rightAligned}>
|
||||
{menu && (
|
||||
<Dropdown overlay={menu} placement="bottom">
|
||||
<ToolbarButton
|
||||
aria-label={`Menu for panel with ${title ? `title ${title}` : 'no title'}`}
|
||||
title="Menu"
|
||||
icon="ellipsis-v"
|
||||
iconSize="md"
|
||||
narrow
|
||||
data-testid="panel-menu-button"
|
||||
className={cx(styles.menuItem, dragClassCancel, 'menu-icon')}
|
||||
<div className={styles.rightAligned}>
|
||||
{menu && (
|
||||
<PanelMenu
|
||||
menu={menu}
|
||||
title={title}
|
||||
menuButtonClass={cx(styles.menuItem, dragClassCancel, 'show-on-hover')}
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
)}
|
||||
|
||||
{leftItems && <div className={styles.leftItems}>{itemsRenderer(leftItems, (item) => item)}</div>}
|
||||
{leftItems && <div className={styles.leftItems}>{itemsRenderer(leftItems, (item) => item)}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{statusMessage && (
|
||||
<PanelStatus
|
||||
className={cx(styles.errorContainer, dragClassCancel)}
|
||||
message={statusMessage}
|
||||
onClick={statusMessageOnClick}
|
||||
ariaLabel="Panel status"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{statusMessage && (
|
||||
<PanelStatus
|
||||
className={cx(styles.errorContainer, dragClassCancel)}
|
||||
message={statusMessage}
|
||||
onClick={statusMessageOnClick}
|
||||
ariaLabel="Panel status"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={styles.content} style={contentStyle}>
|
||||
{children(innerWidth, innerHeight)}
|
||||
@ -228,10 +242,15 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
flexDirection: 'column',
|
||||
flex: '1 1 0',
|
||||
|
||||
'&:focus-within, &:hover': {
|
||||
'.show-on-hover': {
|
||||
visibility: 'hidden',
|
||||
opacity: '0',
|
||||
},
|
||||
'&:focus-visible, &:hover': {
|
||||
// only show menu icon on hover or focused panel
|
||||
'.menu-icon': {
|
||||
'.show-on-hover': {
|
||||
visibility: 'visible',
|
||||
opacity: '1',
|
||||
},
|
||||
},
|
||||
|
||||
@ -239,6 +258,14 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
outline: `1px solid ${theme.colors.action.focus}`,
|
||||
},
|
||||
}),
|
||||
regularHeader: css({
|
||||
'&:focus-within': {
|
||||
'.show-on-hover': {
|
||||
visibility: 'visible',
|
||||
opacity: '1',
|
||||
},
|
||||
},
|
||||
}),
|
||||
loadingBarContainer: css({
|
||||
label: 'panel-loading-bar-container',
|
||||
position: 'absolute',
|
||||
@ -273,6 +300,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: theme.spacing(50),
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
fontWeight: theme.typography.h6.fontWeight,
|
||||
}),
|
||||
@ -294,6 +322,10 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: theme.zIndex.tooltip,
|
||||
}),
|
||||
leftItems: css({
|
||||
display: 'flex',
|
||||
@ -307,6 +339,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
}),
|
||||
titleItems: css({
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
|
||||
@ -12,7 +15,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function PanelDescription({ description, className }: Props) {
|
||||
const styles = getStyles();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const getDescriptionContent = (): JSX.Element => {
|
||||
// description
|
||||
@ -34,7 +37,7 @@ export function PanelDescription({ description, className }: Props) {
|
||||
) : null;
|
||||
}
|
||||
|
||||
const getStyles = () => {
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
description: css({
|
||||
code: {
|
||||
|
40
packages/grafana-ui/src/components/PanelChrome/PanelMenu.tsx
Normal file
40
packages/grafana-ui/src/components/PanelChrome/PanelMenu.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { cx } from '@emotion/css';
|
||||
import React, { ReactElement } from 'react';
|
||||
|
||||
import { Dropdown } from '../Dropdown/Dropdown';
|
||||
import { ToolbarButton } from '../ToolbarButton';
|
||||
import { TooltipPlacement } from '../Tooltip';
|
||||
|
||||
interface PanelMenuProps {
|
||||
menu: ReactElement | (() => ReactElement);
|
||||
menuButtonClass?: string;
|
||||
dragClassCancel?: string;
|
||||
title?: string;
|
||||
placement?: TooltipPlacement;
|
||||
offset?: [number, number];
|
||||
onVisibleChange?: (state: boolean) => void;
|
||||
}
|
||||
|
||||
export function PanelMenu({
|
||||
menu,
|
||||
title,
|
||||
placement = 'bottom',
|
||||
offset,
|
||||
dragClassCancel,
|
||||
menuButtonClass,
|
||||
onVisibleChange,
|
||||
}: PanelMenuProps) {
|
||||
return (
|
||||
<Dropdown overlay={menu} placement={placement} offset={offset} onVisibleChange={onVisibleChange}>
|
||||
<ToolbarButton
|
||||
aria-label={`Menu for panel with ${title ? `title ${title}` : 'no title'}`}
|
||||
title="Menu"
|
||||
icon="ellipsis-v"
|
||||
iconSize="md"
|
||||
narrow
|
||||
data-testid="panel-menu-button"
|
||||
className={cx(menuButtonClass, dragClassCancel)}
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import classnames from 'classnames';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { PanelMenuItem } from '@grafana/data';
|
||||
@ -7,13 +8,15 @@ import { PanelHeaderMenuItem } from './PanelHeaderMenuItem';
|
||||
export interface Props {
|
||||
items: PanelMenuItem[];
|
||||
style?: React.CSSProperties;
|
||||
itemsClassName?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export class PanelHeaderMenu extends PureComponent<Props> {
|
||||
renderItems = (menu: PanelMenuItem[], isSubMenu = false) => {
|
||||
return (
|
||||
<ul
|
||||
className="dropdown-menu dropdown-menu--menu panel-menu"
|
||||
className={classnames('dropdown-menu', 'dropdown-menu--menu', 'panel-menu', this.props.itemsClassName)}
|
||||
style={this.props.style}
|
||||
role={isSubMenu ? '' : 'menu'}
|
||||
>
|
||||
@ -36,6 +39,10 @@ export class PanelHeaderMenu extends PureComponent<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
return <div className="panel-menu-container dropdown open">{this.renderItems(this.props.items)}</div>;
|
||||
return (
|
||||
<div className={classnames('panel-menu-container', 'dropdown', 'open', this.props.className)}>
|
||||
{this.renderItems(this.props.items)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -13,14 +13,28 @@ interface Props {
|
||||
loadingState?: LoadingState;
|
||||
onClose: () => void;
|
||||
style?: React.CSSProperties;
|
||||
menuItemsClassName?: string;
|
||||
menuWrapperClassName?: string;
|
||||
}
|
||||
|
||||
export function PanelHeaderMenuWrapper({ style, panel, dashboard, loadingState }: Props) {
|
||||
export function PanelHeaderMenuWrapper({
|
||||
style,
|
||||
panel,
|
||||
dashboard,
|
||||
loadingState,
|
||||
menuItemsClassName,
|
||||
menuWrapperClassName,
|
||||
}: Props) {
|
||||
return (
|
||||
<PanelHeaderMenuProvider panel={panel} dashboard={dashboard} loadingState={loadingState}>
|
||||
{({ items }) => {
|
||||
return <PanelHeaderMenu style={style} items={items} />;
|
||||
}}
|
||||
{({ items }) => (
|
||||
<PanelHeaderMenu
|
||||
className={menuWrapperClassName}
|
||||
itemsClassName={menuItemsClassName}
|
||||
style={style}
|
||||
items={items}
|
||||
/>
|
||||
)}
|
||||
</PanelHeaderMenuProvider>
|
||||
);
|
||||
}
|
||||
|
@ -47,6 +47,8 @@ export function PanelLinks({ panelLinks, onShowPanelLinks }: Props) {
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
menuTrigger: css({
|
||||
height: '100%',
|
||||
background: 'inherit',
|
||||
border: 'none',
|
||||
borderRadius: `${theme.shape.borderRadius()}`,
|
||||
cursor: 'context-menu',
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { css } from '@emotion/css';
|
||||
import classNames from 'classnames';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Subscription } from 'rxjs';
|
||||
@ -636,9 +637,12 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
||||
const title = panel.getDisplayTitle();
|
||||
const padding: PanelPadding = plugin.noPadding ? 'none' : 'md';
|
||||
|
||||
const dragClass = !(isViewing || isEditing) ? 'grid-drag-handle' : '';
|
||||
|
||||
const titleItems = [
|
||||
const showTitleItems =
|
||||
(panel.links && panel.links.length > 0 && this.onShowPanelLinks) ||
|
||||
(data.series.length > 0 && data.series.some((v) => (v.meta?.notices?.length ?? 0) > 0)) ||
|
||||
(data.request && data.request.timeInfo) ||
|
||||
alertState;
|
||||
const titleItems = showTitleItems && (
|
||||
<PanelHeaderTitleItems
|
||||
key="title-items"
|
||||
alertState={alertState}
|
||||
@ -646,25 +650,60 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
||||
panelId={panel.id}
|
||||
panelLinks={panel.links}
|
||||
onShowPanelLinks={this.onShowPanelLinks}
|
||||
/>,
|
||||
];
|
||||
/>
|
||||
);
|
||||
|
||||
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={{ top: 0 }}
|
||||
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;
|
||||
|
||||
return (
|
||||
<PanelChrome
|
||||
width={width}
|
||||
@ -679,6 +718,8 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
||||
dragClass={dragClass}
|
||||
dragClassCancel="grid-drag-cancel"
|
||||
padding={padding}
|
||||
hoverHeaderOffset={hoverHeaderOffset}
|
||||
hoverHeader={title ? false : true}
|
||||
displayMode={transparent ? 'transparent' : 'default'}
|
||||
>
|
||||
{(innerWidth, innerHeight) => (
|
||||
|
@ -339,6 +339,9 @@ export class PanelModel implements DataConfigSource, IPanelModel {
|
||||
if (manuallyUpdated) {
|
||||
this.configRev++;
|
||||
}
|
||||
|
||||
// Maybe a bit heavy. Could add a "GridPosChanged" event instead?
|
||||
this.render();
|
||||
}
|
||||
|
||||
runAllPanelQueries({
|
||||
|
Loading…
Reference in New Issue
Block a user