Dashboard: Remove old panel code and leave only new panel design (#74196)

* delete PanelHeader

* PanelHeaderMenuItem is only used in PageHeader

* PanelHeaderCorner only used by PanelEditorTableView

* PanelHeaderMenuTrigger not used anywhere

* PanelHeaderMenuWrapperNew is PanelHeaderMenuWrapper, old one is deleted

* remove newPanelChromeUI from test

* delete feature flag newPanelChromeUI

* PanelHeaderMenuWrapperNew is PanelHeaderMenuWrapper, old one is deleted
This commit is contained in:
Polina Boneva 2023-09-12 11:54:41 +03:00 committed by GitHub
parent 05ce7e5789
commit b14263af45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 97 additions and 639 deletions

View File

@ -1405,6 +1405,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "82"],
[0, 0, 0, "Unexpected any. Specify a different type.", "83"]
],
"public/app/core/components/PageHeader/PanelHeaderMenuItem.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],
"public/app/core/components/PasswordField/PasswordField.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],
@ -2091,17 +2094,10 @@ exports[`better eslint`] = {
"public/app/features/dashboard/dashgrid/DashboardPanel.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],
"public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],
"public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "3"]
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"public/app/features/dashboard/dashgrid/SeriesVisibilityConfigFactory.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]

View File

@ -27,7 +27,6 @@ Some features are enabled by default. You can disable these feature by setting t
| `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes |
| `redshiftAsyncQueryDataSupport` | Enable async query data support for Redshift | Yes |
| `athenaAsyncQueryDataSupport` | Enable async query data support for Athena | Yes |
| `newPanelChromeUI` | Show updated look and feel of grafana-ui PanelChrome: panel header, icons, and menu | Yes |
| `nestedFolderPicker` | Enables the new folder picker to work with nested folders. Requires the nestedFolders feature flag | Yes |
| `accessTokenExpirationCheck` | Enable OAuth access_token expiration check and token refresh using the refresh_token | |
| `emptyDashboardPage` | Enable the redesigned user interface of a dashboard page that includes no panels | Yes |

View File

@ -46,7 +46,6 @@ export interface FeatureToggles {
cloudWatchCrossAccountQuerying?: boolean;
redshiftAsyncQueryDataSupport?: boolean;
athenaAsyncQueryDataSupport?: boolean;
newPanelChromeUI?: boolean;
showDashboardValidationWarnings?: boolean;
mysqlAnsiQuotes?: boolean;
accessControlOnCall?: boolean;

View File

@ -72,11 +72,12 @@ export const Components = {
Panels: {
Panel: {
title: (title: string) => `data-testid Panel header ${title}`,
headerItems: (item: string) => `Panel header item ${item}`,
headerItems: (item: string) => `data-testid Panel header item ${item}`,
menuItems: (item: string) => `data-testid Panel menu item ${item}`,
menu: (title: string) => `data-testid Panel menu ${title}`,
containerByTitle: (title: string) => `${title} panel`,
headerCornerInfo: (mode: string) => `Panel header ${mode}`,
status: (status: string) => `data-testid Panel status ${status}`,
loadingBar: () => `Panel loading bar`,
HoverWidget: {
container: 'data-testid hover-header-container',

View File

@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { useStyles2 } from '../../themes';
import { ToolbarButton } from '../ToolbarButton/ToolbarButton';
@ -24,6 +25,7 @@ export function PanelStatus({ message, onClick, ariaLabel = 'status' }: Props) {
iconSize="md"
tooltip={message || ''}
aria-label={ariaLabel}
data-testid={selectors.components.Panels.Panel.status('error')}
/>
);
}

View File

@ -196,14 +196,6 @@ var (
FrontendOnly: true,
Owner: awsDatasourcesSquad,
},
{
Name: "newPanelChromeUI",
Description: "Show updated look and feel of grafana-ui PanelChrome: panel header, icons, and menu",
Stage: FeatureStageGeneralAvailability,
FrontendOnly: true,
Expression: "true", // enabled by default
Owner: grafanaDashboardsSquad,
},
{
Name: "showDashboardValidationWarnings",
Description: "Show warnings when dashboards do not validate against the schema",

View File

@ -27,7 +27,6 @@ entityStore,experimental,@grafana/grafana-app-platform-squad,true,false,false,fa
cloudWatchCrossAccountQuerying,GA,@grafana/aws-datasources,false,false,false,false
redshiftAsyncQueryDataSupport,GA,@grafana/aws-datasources,false,false,false,false
athenaAsyncQueryDataSupport,GA,@grafana/aws-datasources,false,false,false,true
newPanelChromeUI,GA,@grafana/dashboards-squad,false,false,false,true
showDashboardValidationWarnings,experimental,@grafana/dashboards-squad,false,false,false,false
mysqlAnsiQuotes,experimental,@grafana/backend-platform,false,false,false,false
accessControlOnCall,preview,@grafana/grafana-authnz-team,false,false,false,false

1 Name Stage Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
27 cloudWatchCrossAccountQuerying GA @grafana/aws-datasources false false false false
28 redshiftAsyncQueryDataSupport GA @grafana/aws-datasources false false false false
29 athenaAsyncQueryDataSupport GA @grafana/aws-datasources false false false true
newPanelChromeUI GA @grafana/dashboards-squad false false false true
30 showDashboardValidationWarnings experimental @grafana/dashboards-squad false false false false
31 mysqlAnsiQuotes experimental @grafana/backend-platform false false false false
32 accessControlOnCall preview @grafana/grafana-authnz-team false false false false

View File

@ -119,10 +119,6 @@ const (
// Enable async query data support for Athena
FlagAthenaAsyncQueryDataSupport = "athenaAsyncQueryDataSupport"
// FlagNewPanelChromeUI
// Show updated look and feel of grafana-ui PanelChrome: panel header, icons, and menu
FlagNewPanelChromeUI = "newPanelChromeUI"
// FlagShowDashboardValidationWarnings
// Show warnings when dashboards do not validate against the schema
FlagShowDashboardValidationWarnings = "showDashboardValidationWarnings"

View File

@ -3,7 +3,7 @@ import React from 'react';
import { NavModelItem, GrafanaTheme2 } from '@grafana/data';
import { Tab, TabsBar, Icon, useStyles2, toIconName } from '@grafana/ui';
import { PanelHeaderMenuItem } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem';
import { PanelHeaderMenuItem } from 'app/core/components/PageHeader/PanelHeaderMenuItem';
import { PageInfoItem } from '../Page/types';
import { PageInfo } from '../PageInfo/PageInfo';

View File

@ -6,10 +6,10 @@ import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
import { PanelRenderer } from 'app/features/panel/components/PanelRenderer';
import { Options } from 'app/plugins/panel/table/panelcfg.gen';
import PanelHeaderCorner from '../../dashgrid/PanelHeader/PanelHeaderCorner';
import { getTimeSrv } from '../../services/TimeSrv';
import { DashboardModel, PanelModel } from '../../state';
import PanelHeaderCorner from './PanelHeaderCorner';
import { usePanelLatestData } from './usePanelLatestData';
export interface Props {

View File

@ -134,7 +134,6 @@ const dashboardBase = {
describe('PublicDashboardPage', () => {
beforeEach(() => {
config.featureToggles.publicDashboards = true;
config.featureToggles.newPanelChromeUI = true;
jest.clearAllMocks();
});

View File

@ -1,11 +1,9 @@
import classNames from 'classnames';
import React, { PureComponent } from 'react';
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
import { Subscription } from 'rxjs';
import { getDefaultTimeRange, LoadingState, PanelData, PanelPlugin } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { AngularComponent, getAngularLoader, locationService } from '@grafana/runtime';
import { AngularComponent, getAngularLoader } from '@grafana/runtime';
import { PanelChrome } from '@grafana/ui';
import config from 'app/core/config';
import { PANEL_BORDER } from 'app/core/constants';
@ -13,13 +11,11 @@ import { setPanelAngularComponent } from 'app/features/panel/state/reducers';
import { getPanelStateForModel } from 'app/features/panel/state/selectors';
import { StoreState } from 'app/types';
import { isSoloRoute } from '../../../routes/utils';
import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
import { DashboardModel, PanelModel } from '../state';
import { getPanelChromeProps } from '../utils/getPanelChromeProps';
import { PanelHeader } from './PanelHeader/PanelHeader';
import { PanelHeaderMenuWrapperNew } from './PanelHeader/PanelHeaderMenuWrapper';
import { PanelHeaderMenuWrapper } from './PanelHeader/PanelHeaderMenuWrapper';
interface OwnProps {
panel: PanelModel;
@ -180,85 +176,44 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
}
render() {
const { dashboard, panel, isViewing, isEditing, plugin } = this.props;
const { dashboard, panel } = this.props;
const { errorMessage, data } = this.state;
const { transparent } = panel;
const alertState = data.alertState?.state;
const panelChromeProps = getPanelChromeProps({ ...this.props, data });
const containerClassNames = classNames({
'panel-container': true,
'panel-container--absolute': isSoloRoute(locationService.getLocation().pathname),
'panel-container--transparent': transparent,
'panel-container--no-title': this.hasOverlayHeader(),
'panel-has-alert': panel.alert !== undefined,
[`panel-alert-state--${alertState}`]: alertState !== undefined,
});
// 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 panelContentClassNames = classNames({
'panel-content': true,
'panel-content--no-padding': plugin.noPadding,
});
const menu = (
<div data-testid="panel-dropdown">
<PanelHeaderMenuWrapper panel={panel} dashboard={dashboard} loadingState={data.state} />
</div>
);
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={this.props.width}
height={this.props.height}
title={panelChromeProps.title}
loadingState={data.state}
statusMessage={errorMessage}
statusMessageOnClick={panelChromeProps.onOpenErrorInspect}
description={panelChromeProps.description}
titleItems={panelChromeProps.titleItems}
menu={this.props.hideMenu ? undefined : menu}
dragClass={panelChromeProps.dragClass}
dragClassCancel="grid-drag-cancel"
padding={panelChromeProps.padding}
hoverHeaderOffset={hoverHeaderOffset}
hoverHeader={panelChromeProps.hasOverlayHeader()}
displayMode={transparent ? 'transparent' : 'default'}
onCancelQuery={panelChromeProps.onCancelQuery}
onOpenMenu={panelChromeProps.onOpenMenu}
>
{() => <div ref={(element) => (this.element = element)} className="panel-height-helper" />}
</PanelChrome>
);
} else {
return (
<div
className={containerClassNames}
data-testid={selectors.components.Panels.Panel.title(panel.title)}
aria-label={selectors.components.Panels.Panel.containerByTitle(panel.title)}
>
<PanelHeader
panel={panel}
dashboard={dashboard}
title={panel.title}
description={panel.description}
links={panel.links}
error={errorMessage}
isViewing={isViewing}
isEditing={isEditing}
data={data}
alertState={alertState}
/>
<div className={panelContentClassNames}>
<div ref={(element) => (this.element = element)} className="panel-height-helper" />
</div>
</div>
);
}
return (
<PanelChrome
width={this.props.width}
height={this.props.height}
title={panelChromeProps.title}
loadingState={data.state}
statusMessage={errorMessage}
statusMessageOnClick={panelChromeProps.onOpenErrorInspect}
description={panelChromeProps.description}
titleItems={panelChromeProps.titleItems}
menu={this.props.hideMenu ? undefined : menu}
dragClass={panelChromeProps.dragClass}
dragClassCancel="grid-drag-cancel"
padding={panelChromeProps.padding}
hoverHeaderOffset={hoverHeaderOffset}
hoverHeader={panelChromeProps.hasOverlayHeader()}
displayMode={transparent ? 'transparent' : 'default'}
onCancelQuery={panelChromeProps.onCancelQuery}
onOpenMenu={panelChromeProps.onOpenMenu}
>
{() => <div ref={(element) => (this.element = element)} className="panel-height-helper" />}
</PanelChrome>
);
}
}

View File

@ -1,47 +0,0 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { config } from '@grafana/runtime';
import { createEmptyQueryResponse } from '../../../explore/state/utils';
import { PanelModel } from '../../state';
import { createDashboardModelFixture } from '../../state/__fixtures__/dashboardFixtures';
import { PanelHeader } from './PanelHeader';
let panelModel = new PanelModel({
id: 1,
gridPos: { x: 1, y: 1, w: 1, h: 1 },
type: 'type',
title: 'title',
});
let panelData = createEmptyQueryResponse();
describe('Panel Header', () => {
const dashboardModel = createDashboardModelFixture({}, {});
it('will render header title but not render dropdown icon when dashboard is being viewed publicly', () => {
window.history.pushState({}, 'Test Title', '/public-dashboards/abc123');
config.publicDashboardAccessToken = 'abc123';
render(
<PanelHeader panel={panelModel} dashboard={dashboardModel} isViewing={false} isEditing={false} data={panelData} />
);
expect(screen.getByText('title')).toBeDefined();
expect(screen.queryByTestId('panel-dropdown')).toBeNull();
});
it('will render header title and dropdown icon when dashboard is not being viewed publicly', () => {
const dashboardModel = createDashboardModelFixture({}, {});
window.history.pushState({}, 'Test Title', '/d/abc/123');
config.publicDashboardAccessToken = '';
render(
<PanelHeader panel={panelModel} dashboard={dashboardModel} isViewing={false} isEditing={false} data={panelData} />
);
expect(screen.getByText('title')).toBeDefined();
expect(screen.getByTestId('panel-dropdown')).toBeDefined();
});
});

View File

@ -1,109 +0,0 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { DataLink, GrafanaTheme2, PanelData } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config, reportInteraction } from '@grafana/runtime';
import { Icon, useStyles2, ClickOutsideWrapper } from '@grafana/ui';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
import PanelHeaderCorner from './PanelHeaderCorner';
import { PanelHeaderLoadingIndicator } from './PanelHeaderLoadingIndicator';
import { PanelHeaderMenuTrigger } from './PanelHeaderMenuTrigger';
import { PanelHeaderMenuWrapper } from './PanelHeaderMenuWrapper';
import { PanelHeaderNotices } from './PanelHeaderNotices';
export interface Props {
panel: PanelModel;
dashboard: DashboardModel;
title?: string;
description?: string;
links?: DataLink[];
error?: string;
alertState?: string;
isViewing: boolean;
isEditing: boolean;
data: PanelData;
}
export function PanelHeader({ panel, error, isViewing, isEditing, data, alertState, dashboard }: Props) {
const onCancelQuery = () => panel.getQueryRunner().cancelQuery();
const title = panel.getDisplayTitle();
const className = cx('panel-header', !(isViewing || isEditing) ? 'grid-drag-handle' : '');
const styles = useStyles2(panelStyles);
const onOpenMenu = () => {
reportInteraction('dashboards_panelheader_menu', { item: 'menu' });
};
return (
<>
<PanelHeaderLoadingIndicator state={data.state} onClick={onCancelQuery} />
<PanelHeaderCorner
panel={panel}
title={panel.title}
description={panel.description}
scopedVars={panel.scopedVars}
links={getPanelLinksSupplier(panel)}
error={error}
/>
<div className={className}>
<PanelHeaderMenuTrigger data-testid={selectors.components.Panels.Panel.title(title)} onOpenMenu={onOpenMenu}>
{({ closeMenu, panelMenuOpen }) => {
return (
<ClickOutsideWrapper onClick={closeMenu} parent={document}>
<div className="panel-title">
<PanelHeaderNotices frames={data.series} panelId={panel.id} />
{alertState ? (
<Icon
name={alertState === 'alerting' ? 'heart-break' : 'heart'}
className="icon-gf panel-alert-icon"
style={{ marginRight: '4px' }}
size="sm"
/>
) : null}
<h2 className={styles.titleText}>{title}</h2>
{!config.publicDashboardAccessToken && (
<div data-testid="panel-dropdown">
<Icon name="angle-down" className="panel-menu-toggle" />
{panelMenuOpen ? <PanelHeaderMenuWrapper panel={panel} dashboard={dashboard} /> : null}
</div>
)}
{data.request && data.request.timeInfo && (
<span className="panel-time-info">
<Icon name="clock-nine" size="sm" /> {data.request.timeInfo}
</span>
)}
</div>
</ClickOutsideWrapper>
);
}}
</PanelHeaderMenuTrigger>
</div>
</>
);
}
const panelStyles = (theme: GrafanaTheme2) => {
return {
titleText: css`
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: calc(100% - 38px);
cursor: pointer;
font-weight: ${theme.typography.fontWeightMedium};
font-size: ${theme.typography.body.fontSize};
margin: 0;
&:hover {
color: ${theme.colors.text.primary};
}
.panel-has-alert & {
max-width: calc(100% - 54px);
}
`,
};
};

View File

@ -1,53 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2, LoadingState } from '@grafana/data';
import { Icon, Tooltip, useStyles2 } from '@grafana/ui';
interface Props {
state: LoadingState;
onClick: () => void;
}
export const PanelHeaderLoadingIndicator = ({ state, onClick }: Props) => {
const styles = useStyles2(getStyles);
if (state === LoadingState.Loading) {
return (
// TODO: fix keyboard a11y
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div className="panel-loading" onClick={onClick}>
<Tooltip content="Cancel query">
<Icon className="panel-loading__spinner spin-clockwise" name="sync" />
</Tooltip>
</div>
);
}
if (state === LoadingState.Streaming) {
return (
// TODO: fix keyboard a11y
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div className="panel-loading" onClick={onClick}>
<div title="Streaming (click to stop)" className={styles.streamIndicator} />
</div>
);
}
return null;
};
function getStyles(theme: GrafanaTheme2) {
return {
streamIndicator: css`
width: 10px;
height: 10px;
background: ${theme.colors.text.disabled};
box-shadow: 0 0 2px ${theme.colors.text.disabled};
border-radius: ${theme.shape.radius.circle};
position: relative;
top: 6px;
right: 1px;
`,
};
}

View File

@ -1,12 +1,9 @@
import classnames from 'classnames';
import React, { PureComponent } from 'react';
import React from 'react';
import { PanelMenuItem } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Menu } from '@grafana/ui';
import { PanelHeaderMenuItem } from './PanelHeaderMenuItem';
export interface Props {
items: PanelMenuItem[];
style?: React.CSSProperties;
@ -14,65 +11,7 @@ export interface Props {
className?: string;
}
export class PanelHeaderMenu extends PureComponent<Props> {
renderItems = (menu: PanelMenuItem[], isSubMenu = false) => {
return (
<ul
className={classnames('dropdown-menu', 'dropdown-menu--menu', 'panel-menu', this.props.itemsClassName)}
style={this.props.style}
role={isSubMenu ? '' : 'menu'}
>
{menu.map((menuItem, idx: number) => {
return (
<PanelHeaderMenuItem
key={`${menuItem.text}${idx}`}
type={menuItem.type}
text={menuItem.text}
iconClassName={menuItem.iconClassName}
onClick={menuItem.onClick}
shortcut={menuItem.shortcut}
href={menuItem.href}
>
{menuItem.subMenu && this.renderItems(menuItem.subMenu, true)}
</PanelHeaderMenuItem>
);
})}
</ul>
);
};
render() {
return (
<div className={classnames('panel-menu-container', 'dropdown', 'open', this.props.className)}>
{this.renderItems(flattenGroups(this.props.items))}
</div>
);
}
}
function flattenGroups(items: PanelMenuItem[]): PanelMenuItem[] {
return items.reduce((all: PanelMenuItem[], item) => {
if (Array.isArray(item.subMenu) && item.type === 'submenu') {
all.push({
...item,
subMenu: flattenGroups(item.subMenu),
});
return all;
}
if (Array.isArray(item.subMenu) && item.type === 'group') {
const { subMenu, ...rest } = item;
all.push(rest);
all.push.apply(all, flattenGroups(subMenu));
return all;
}
all.push(item);
return all;
}, []);
}
export function PanelHeaderMenuNew({ items }: Props) {
export function PanelHeaderMenu({ items }: Props) {
const renderItems = (items: PanelMenuItem[]) => {
return items.map((item) => {
switch (item.type) {

View File

@ -1,58 +0,0 @@
import React, { HTMLAttributes, MouseEvent, ReactElement, useCallback, useRef, useState } from 'react';
import { CartesianCoords2D } from '@grafana/data';
interface PanelHeaderMenuTriggerApi {
panelMenuOpen: boolean;
closeMenu: () => void;
}
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
children: (props: PanelHeaderMenuTriggerApi) => ReactElement;
onOpenMenu?: () => void;
}
export function PanelHeaderMenuTrigger({ children, onOpenMenu, ...divProps }: Props) {
const clickCoordinates = useRef<CartesianCoords2D>({ x: 0, y: 0 });
const [panelMenuOpen, setPanelMenuOpen] = useState<boolean>(false);
const onMenuToggle = useCallback(
(event: MouseEvent<HTMLDivElement>) => {
if (!isClick(clickCoordinates.current, eventToClickCoordinates(event))) {
return;
}
setPanelMenuOpen(!panelMenuOpen);
if (panelMenuOpen) {
onOpenMenu?.();
}
},
[panelMenuOpen, setPanelMenuOpen, onOpenMenu]
);
const onMouseDown = useCallback((event: MouseEvent<HTMLDivElement>) => {
clickCoordinates.current = eventToClickCoordinates(event);
}, []);
return (
// TODO: fix keyboard a11y
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<header {...divProps} className="panel-title-container" onClick={onMenuToggle} onMouseDown={onMouseDown}>
{children({ panelMenuOpen, closeMenu: () => setPanelMenuOpen(false) })}
</header>
);
}
function isClick(current: CartesianCoords2D, clicked: CartesianCoords2D, deadZone = 3.5): boolean {
// A "deadzone" radius is added so that if the cursor is moved within this radius
// between mousedown and mouseup, it's still considered a click and not a drag.
const clickDistance = Math.sqrt((current.x - clicked.x) ** 2 + (current.y - clicked.y) ** 2);
return clickDistance <= deadZone;
}
function eventToClickCoordinates(event: MouseEvent<HTMLDivElement>): CartesianCoords2D {
return {
x: event.clientX,
y: event.clientY,
};
}

View File

@ -4,7 +4,7 @@ import { LoadingState } from '@grafana/data';
import { DashboardModel, PanelModel } from '../../state';
import { PanelHeaderMenu, PanelHeaderMenuNew } from './PanelHeaderMenu';
import { PanelHeaderMenu } from './PanelHeaderMenu';
import { PanelHeaderMenuProvider } from './PanelHeaderMenuProvider';
interface Props {
@ -16,32 +16,10 @@ interface Props {
menuWrapperClassName?: string;
}
export function PanelHeaderMenuWrapper({
panel,
dashboard,
loadingState,
style,
menuItemsClassName,
menuWrapperClassName,
}: Props) {
export function PanelHeaderMenuWrapper({ style, panel, dashboard, loadingState }: Props) {
return (
<PanelHeaderMenuProvider panel={panel} dashboard={dashboard} loadingState={loadingState}>
{({ items }) => (
<PanelHeaderMenu
className={menuWrapperClassName}
itemsClassName={menuItemsClassName}
style={style}
items={items}
/>
)}
</PanelHeaderMenuProvider>
);
}
export function PanelHeaderMenuWrapperNew({ style, panel, dashboard, loadingState }: Props) {
return (
<PanelHeaderMenuProvider panel={panel} dashboard={dashboard} loadingState={loadingState}>
{({ items }) => <PanelHeaderMenuNew style={style} items={items} />}
{({ items }) => <PanelHeaderMenu style={style} items={items} />}
</PanelHeaderMenuProvider>
);
}

View File

@ -128,9 +128,7 @@ describe('PanelStateWrapper', () => {
</Provider>
);
const button = screen.getByRole('button', {
name: selectors.components.Panels.Panel.headerCornerInfo('error'),
});
const button = screen.getByTestId(selectors.components.Panels.Panel.status('error'));
expect(button).toBeInTheDocument();
await act(async () => {
fireEvent.focus(button);

View File

@ -1,4 +1,3 @@
import classNames from 'classnames';
import React, { PureComponent } from 'react';
import { Subscription } from 'rxjs';
@ -17,13 +16,11 @@ import {
PanelData,
PanelPlugin,
PanelPluginMeta,
PluginContextProvider,
TimeRange,
toDataFrameDTO,
toUtc,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config, locationService, RefreshEvent } from '@grafana/runtime';
import { RefreshEvent } from '@grafana/runtime';
import { VizLegendOptions } from '@grafana/schema';
import {
ErrorBoundary,
@ -33,7 +30,6 @@ import {
SeriesVisibilityChangeMode,
AdHocFilterItem,
} from '@grafana/ui';
import { PANEL_BORDER } from 'app/core/constants';
import { profiler } from 'app/core/profiler';
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
@ -43,7 +39,6 @@ import { changeSeriesColorConfigFactory } from 'app/plugins/panel/timeseries/ove
import { dispatch } from 'app/store/store';
import { RenderEvent } from 'app/types/events';
import { isSoloRoute } from '../../../routes/utils';
import { deleteAnnotation, saveAnnotation, updateAnnotation } from '../../annotations/api';
import { getDashboardQueryRunner } from '../../query/state/DashboardQueryRunner/DashboardQueryRunner';
import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
@ -51,8 +46,7 @@ import { DashboardModel, PanelModel } from '../state';
import { getPanelChromeProps } from '../utils/getPanelChromeProps';
import { loadSnapshotData } from '../utils/loadSnapshotData';
import { PanelHeader } from './PanelHeader/PanelHeader';
import { PanelHeaderMenuWrapperNew } from './PanelHeader/PanelHeaderMenuWrapper';
import { PanelHeaderMenuWrapper } from './PanelHeader/PanelHeaderMenuWrapper';
import { seriesVisibilityConfigFactory } from './SeriesVisibilityConfigFactory';
import { liveTimer } from './liveTimer';
@ -523,181 +517,59 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
);
}
renderPanel(width: number, height: number) {
const { panel, plugin, dashboard } = this.props;
const { renderCounter, data } = this.state;
const { theme } = config;
const { state: loadingState } = data;
// do not render component until we have first data
if (this.skipFirstRender(loadingState)) {
return null;
}
// This is only done to increase a counter that is used by backend
// image rendering to know when to capture image
if (this.shouldSignalRenderingCompleted(loadingState, plugin.meta)) {
profiler.renderingCompleted();
}
const PanelComponent = plugin.panel!;
const timeRange = this.state.liveTime ?? data.timeRange ?? this.timeSrv.timeRange();
const headerHeight = this.hasOverlayHeader() ? 0 : theme.panelHeaderHeight;
const chromePadding = plugin.noPadding ? 0 : theme.panelPadding;
const panelWidth = width - chromePadding * 2 - PANEL_BORDER;
const innerPanelHeight = height - headerHeight - chromePadding * 2 - PANEL_BORDER;
const panelContentClassNames = classNames({
'panel-content': true,
'panel-content--no-padding': plugin.noPadding,
});
const panelOptions = panel.getOptions();
// Update the event filter (dashboard settings may have changed)
// Yes this is called ever render for a function that is triggered on every mouse move
this.eventFilter.onlyLocal = dashboard.graphTooltip === 0;
const timeZone = this.props.timezone || this.props.dashboard.getTimezone();
return (
<>
<div className={panelContentClassNames}>
<PluginContextProvider meta={plugin.meta}>
<PanelContextProvider value={this.state.context}>
<PanelComponent
id={panel.id}
data={data}
title={panel.title}
timeRange={timeRange}
timeZone={timeZone}
options={panelOptions}
fieldConfig={panel.fieldConfig}
transparent={panel.transparent}
width={panelWidth}
height={innerPanelHeight}
renderCounter={renderCounter}
replaceVariables={panel.replaceVariables}
onOptionsChange={this.onOptionsChange}
onFieldConfigChange={this.onFieldConfigChange}
onChangeTimeRange={this.onChangeTimeRange}
eventBus={dashboard.events}
/>
</PanelContextProvider>
</PluginContextProvider>
</div>
</>
);
}
hasOverlayHeader() {
const { panel } = this.props;
const { data } = this.state;
// always show normal header if we have time override
if (data.request && data.request.timeInfo) {
return false;
}
return !panel.hasTitle();
}
render() {
const { dashboard, panel, isViewing, isEditing, width, height, plugin } = this.props;
const { dashboard, panel, width, height, plugin } = this.props;
const { errorMessage, data } = this.state;
const { transparent } = panel;
const alertState = data.alertState?.state;
const hasHoverHeader = this.hasOverlayHeader();
const containerClassNames = classNames({
'panel-container': true,
'panel-container--absolute': isSoloRoute(locationService.getLocation().pathname),
'panel-container--transparent': transparent,
'panel-container--no-title': hasHoverHeader,
[`panel-alert-state--${alertState}`]: alertState !== undefined,
});
const panelChromeProps = getPanelChromeProps({ ...this.props, data });
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;
// 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>
);
const menu = (
<div data-testid="panel-dropdown">
<PanelHeaderMenuWrapper panel={panel} dashboard={dashboard} loadingState={data.state} />
</div>
);
return (
<PanelChrome
width={width}
height={height}
title={panelChromeProps.title}
loadingState={data.state}
statusMessage={errorMessage}
statusMessageOnClick={panelChromeProps.onOpenErrorInspect}
description={panelChromeProps.description}
titleItems={panelChromeProps.titleItems}
menu={this.props.hideMenu ? undefined : menu}
dragClass={panelChromeProps.dragClass}
dragClassCancel="grid-drag-cancel"
padding={panelChromeProps.padding}
hoverHeaderOffset={hoverHeaderOffset}
hoverHeader={panelChromeProps.hasOverlayHeader()}
displayMode={transparent ? 'transparent' : 'default'}
onCancelQuery={panelChromeProps.onCancelQuery}
onOpenMenu={panelChromeProps.onOpenMenu}
>
{(innerWidth, innerHeight) => (
<>
<ErrorBoundary
dependencies={[data, plugin, panel.getOptions()]}
onError={this.onPanelError}
onRecover={this.onPanelErrorRecover}
>
{({ error }) => {
if (error) {
return null;
}
return this.renderPanelContent(innerWidth, innerHeight);
}}
</ErrorBoundary>
</>
)}
</PanelChrome>
);
} else {
return (
<section
className={containerClassNames}
aria-label={selectors.components.Panels.Panel.containerByTitle(panel.title)}
>
<PanelHeader
panel={panel}
dashboard={dashboard}
title={panel.title}
description={panel.description}
links={panel.links}
error={errorMessage}
isEditing={isEditing}
isViewing={isViewing}
alertState={alertState}
data={data}
/>
<ErrorBoundary
dependencies={[data, plugin, panel.getOptions()]}
onError={this.onPanelError}
onRecover={this.onPanelErrorRecover}
>
{({ error }) => {
if (error) {
return null;
}
return this.renderPanel(width, height);
}}
</ErrorBoundary>
</section>
);
}
return (
<PanelChrome
width={width}
height={height}
title={panelChromeProps.title}
loadingState={data.state}
statusMessage={errorMessage}
statusMessageOnClick={panelChromeProps.onOpenErrorInspect}
description={panelChromeProps.description}
titleItems={panelChromeProps.titleItems}
menu={this.props.hideMenu ? undefined : menu}
dragClass={panelChromeProps.dragClass}
dragClassCancel="grid-drag-cancel"
padding={panelChromeProps.padding}
hoverHeaderOffset={hoverHeaderOffset}
hoverHeader={panelChromeProps.hasOverlayHeader()}
displayMode={transparent ? 'transparent' : 'default'}
onCancelQuery={panelChromeProps.onCancelQuery}
onOpenMenu={panelChromeProps.onOpenMenu}
>
{(innerWidth, innerHeight) => (
<>
<ErrorBoundary
dependencies={[data, plugin, panel.getOptions()]}
onError={this.onPanelError}
onRecover={this.onPanelErrorRecover}
>
{({ error }) => {
if (error) {
return null;
}
return this.renderPanelContent(innerWidth, innerHeight);
}}
</ErrorBoundary>
</>
)}
</PanelChrome>
);
}
}