mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
05ce7e5789
commit
b14263af45
@ -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"]
|
||||
|
@ -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 |
|
||||
|
@ -46,7 +46,6 @@ export interface FeatureToggles {
|
||||
cloudWatchCrossAccountQuerying?: boolean;
|
||||
redshiftAsyncQueryDataSupport?: boolean;
|
||||
athenaAsyncQueryDataSupport?: boolean;
|
||||
newPanelChromeUI?: boolean;
|
||||
showDashboardValidationWarnings?: boolean;
|
||||
mysqlAnsiQuotes?: boolean;
|
||||
accessControlOnCall?: boolean;
|
||||
|
@ -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',
|
||||
|
@ -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')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
@ -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';
|
||||
|
@ -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 {
|
||||
|
@ -134,7 +134,6 @@ const dashboardBase = {
|
||||
describe('PublicDashboardPage', () => {
|
||||
beforeEach(() => {
|
||||
config.featureToggles.publicDashboards = true;
|
||||
config.featureToggles.newPanelChromeUI = true;
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
`,
|
||||
};
|
||||
};
|
@ -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;
|
||||
`,
|
||||
};
|
||||
}
|
@ -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) {
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user