Panel Header Fix: Implement new Panel Header on Angular Panels (#66826)

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
Alexa V
2023-04-25 18:16:46 +02:00
committed by GitHub
co-authored by Dominik Prokop
parent e6e741546f
commit 2a67b8ad32
3 changed files with 193 additions and 107 deletions
@@ -6,6 +6,7 @@ import { Subscription } from 'rxjs';
import { getDefaultTimeRange, LoadingState, PanelData, PanelPlugin } from '@grafana/data'; import { getDefaultTimeRange, LoadingState, PanelData, PanelPlugin } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { AngularComponent, getAngularLoader, locationService } from '@grafana/runtime'; import { AngularComponent, getAngularLoader, locationService } from '@grafana/runtime';
import { PanelChrome } from '@grafana/ui';
import config from 'app/core/config'; import config from 'app/core/config';
import { PANEL_BORDER } from 'app/core/constants'; import { PANEL_BORDER } from 'app/core/constants';
import { setPanelAngularComponent } from 'app/features/panel/state/reducers'; import { setPanelAngularComponent } from 'app/features/panel/state/reducers';
@@ -15,8 +16,10 @@ import { StoreState } from 'app/types';
import { isSoloRoute } from '../../../routes/utils'; import { isSoloRoute } from '../../../routes/utils';
import { getTimeSrv, TimeSrv } from '../services/TimeSrv'; import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
import { DashboardModel, PanelModel } from '../state'; import { DashboardModel, PanelModel } from '../state';
import { getPanelChromeProps } from '../utils/getPanelChromeProps';
import { PanelHeader } from './PanelHeader/PanelHeader'; import { PanelHeader } from './PanelHeader/PanelHeader';
import { PanelHeaderMenuWrapperNew } from './PanelHeader/PanelHeaderMenuWrapper';
interface OwnProps { interface OwnProps {
panel: PanelModel; panel: PanelModel;
@@ -27,6 +30,7 @@ interface OwnProps {
isInView: boolean; isInView: boolean;
width: number; width: number;
height: number; height: number;
hideMenu?: boolean;
} }
interface ConnectedProps { interface ConnectedProps {
@@ -58,7 +62,6 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
timeSrv: TimeSrv = getTimeSrv(); timeSrv: TimeSrv = getTimeSrv();
scopeProps?: AngularScopeProps; scopeProps?: AngularScopeProps;
subs = new Subscription(); subs = new Subscription();
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
@@ -179,9 +182,10 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
const { dashboard, panel, isViewing, isEditing, plugin } = this.props; const { dashboard, panel, isViewing, isEditing, plugin } = this.props;
const { errorMessage, data } = this.state; const { errorMessage, data } = this.state;
const { transparent } = panel; const { transparent } = panel;
const alertState = data.alertState?.state; const alertState = data.alertState?.state;
const panelChromeProps = getPanelChromeProps({ ...this.props, data });
const containerClassNames = classNames({ const containerClassNames = classNames({
'panel-container': true, 'panel-container': true,
'panel-container--absolute': isSoloRoute(locationService.getLocation().pathname), 'panel-container--absolute': isSoloRoute(locationService.getLocation().pathname),
@@ -196,29 +200,63 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
'panel-content--no-padding': plugin.noPadding, 'panel-content--no-padding': plugin.noPadding,
}); });
return ( if (config.featureToggles.newPanelChromeUI) {
<div // Shift the hover menu down if it's on the top row so it doesn't get clipped by topnav
className={containerClassNames} const hoverHeaderOffset = (panel.gridPos?.y ?? 0) === 0 ? -16 : undefined;
data-testid={selectors.components.Panels.Panel.title(panel.title)}
aria-label={selectors.components.Panels.Panel.containerByTitle(panel.title)} const menu = (
> <div data-testid="panel-dropdown">
<PanelHeader <PanelHeaderMenuWrapperNew panel={panel} dashboard={dashboard} loadingState={data.state} />
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>
</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}
>
{() => <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>
);
}
} }
} }
@@ -13,34 +13,29 @@ import {
FieldConfigSource, FieldConfigSource,
getDataSourceRef, getDataSourceRef,
getDefaultTimeRange, getDefaultTimeRange,
LinkModel,
LoadingState, LoadingState,
PanelData, PanelData,
PanelPlugin, PanelPlugin,
PanelPluginMeta, PanelPluginMeta,
PluginContextProvider, PluginContextProvider,
renderMarkdown,
TimeRange, TimeRange,
toDataFrameDTO, toDataFrameDTO,
toUtc, toUtc,
} from '@grafana/data'; } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { getTemplateSrv, config, locationService, RefreshEvent, reportInteraction } from '@grafana/runtime'; import { config, locationService, RefreshEvent } from '@grafana/runtime';
import { VizLegendOptions } from '@grafana/schema'; import { VizLegendOptions } from '@grafana/schema';
import { import {
ErrorBoundary, ErrorBoundary,
PanelChrome, PanelChrome,
PanelContext, PanelContext,
PanelContextProvider, PanelContextProvider,
PanelPadding,
SeriesVisibilityChangeMode, SeriesVisibilityChangeMode,
AdHocFilterItem, AdHocFilterItem,
} from '@grafana/ui'; } from '@grafana/ui';
import { PANEL_BORDER } from 'app/core/constants'; import { PANEL_BORDER } from 'app/core/constants';
import { profiler } from 'app/core/profiler'; import { profiler } from 'app/core/profiler';
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel'; import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
import { InspectTab } from 'app/features/inspector/types';
import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { applyFilterFromTable } from 'app/features/variables/adhoc/actions'; import { applyFilterFromTable } from 'app/features/variables/adhoc/actions';
import { onUpdatePanelSnapshotData } from 'app/plugins/datasource/grafana/utils'; import { onUpdatePanelSnapshotData } from 'app/plugins/datasource/grafana/utils';
@@ -53,11 +48,11 @@ import { deleteAnnotation, saveAnnotation, updateAnnotation } from '../../annota
import { getDashboardQueryRunner } from '../../query/state/DashboardQueryRunner/DashboardQueryRunner'; import { getDashboardQueryRunner } from '../../query/state/DashboardQueryRunner/DashboardQueryRunner';
import { getTimeSrv, TimeSrv } from '../services/TimeSrv'; import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
import { DashboardModel, PanelModel } from '../state'; import { DashboardModel, PanelModel } from '../state';
import { getPanelChromeProps } from '../utils/getPanelChromeProps';
import { loadSnapshotData } from '../utils/loadSnapshotData'; import { loadSnapshotData } from '../utils/loadSnapshotData';
import { PanelHeader } from './PanelHeader/PanelHeader'; import { PanelHeader } from './PanelHeader/PanelHeader';
import { PanelHeaderMenuWrapperNew } from './PanelHeader/PanelHeaderMenuWrapper'; import { PanelHeaderMenuWrapperNew } from './PanelHeader/PanelHeaderMenuWrapper';
import { PanelHeaderTitleItems } from './PanelHeader/PanelHeaderTitleItems';
import { seriesVisibilityConfigFactory } from './SeriesVisibilityConfigFactory'; import { seriesVisibilityConfigFactory } from './SeriesVisibilityConfigFactory';
import { liveTimer } from './liveTimer'; import { liveTimer } from './liveTimer';
@@ -91,7 +86,6 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
private readonly timeSrv: TimeSrv = getTimeSrv(); private readonly timeSrv: TimeSrv = getTimeSrv();
private subs = new Subscription(); private subs = new Subscription();
private eventFilter: EventFilterOptions = { onlyLocal: true }; private eventFilter: EventFilterOptions = { onlyLocal: true };
private descriptionInteractionReported = false;
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@@ -605,53 +599,6 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
return !panel.hasTitle(); return !panel.hasTitle();
} }
onShowPanelDescription = () => {
const { panel } = this.props;
const descriptionMarkdown = getTemplateSrv().replace(panel.description, panel.scopedVars);
const interpolatedDescription = renderMarkdown(descriptionMarkdown);
if (!this.descriptionInteractionReported) {
// Description rendering function can be called multiple times due to re-renders but we want to report the interaction once.
reportInteraction('dashboards_panelheader_description_displayed');
this.descriptionInteractionReported = true;
}
return interpolatedDescription;
};
onShowPanelLinks = (): LinkModel[] => {
const { panel } = this.props;
const linkSupplier = getPanelLinksSupplier(panel);
if (linkSupplier) {
const panelLinks = linkSupplier && linkSupplier.getLinks(panel.replaceVariables);
return panelLinks.map((panelLink) => ({
...panelLink,
onClick: (...args) => {
reportInteraction('dashboards_panelheader_datalink_clicked', { has_multiple_links: panelLinks.length > 1 });
panelLink.onClick?.(...args);
},
}));
}
return [];
};
onOpenInspector = (e: React.SyntheticEvent, tab: string) => {
e.stopPropagation();
locationService.partial({ inspect: this.props.panel.id, inspectTab: tab });
};
onOpenErrorInspect = (e: React.SyntheticEvent) => {
e.stopPropagation();
locationService.partial({ inspect: this.props.panel.id, inspectTab: InspectTab.Error });
reportInteraction('dashboards_panelheader_statusmessage_clicked');
};
onCancelQuery = () => {
this.props.panel.getQueryRunner().cancelQuery();
reportInteraction('dashboards_panelheader_cancelquery_clicked', { data_state: this.state.data.state });
};
render() { render() {
const { dashboard, panel, isViewing, isEditing, width, height, plugin } = this.props; const { dashboard, panel, isViewing, isEditing, width, height, plugin } = this.props;
const { errorMessage, data } = this.state; const { errorMessage, data } = this.state;
@@ -668,27 +615,8 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
[`panel-alert-state--${alertState}`]: alertState !== undefined, [`panel-alert-state--${alertState}`]: alertState !== undefined,
}); });
const title = panel.getDisplayTitle(); const panelChromeProps = getPanelChromeProps({ ...this.props, data });
const padding: PanelPadding = plugin.noPadding ? 'none' : 'md';
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}
data={data}
panelId={panel.id}
panelLinks={panel.links}
onShowPanelLinks={this.onShowPanelLinks}
/>
);
const dragClass = !(isViewing || isEditing) ? 'grid-drag-handle' : '';
if (config.featureToggles.newPanelChromeUI) { if (config.featureToggles.newPanelChromeUI) {
// Shift the hover menu down if it's on the top row so it doesn't get clipped by topnav // 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 hoverHeaderOffset = (panel.gridPos?.y ?? 0) === 0 ? -16 : undefined;
@@ -703,20 +631,20 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
<PanelChrome <PanelChrome
width={width} width={width}
height={height} height={height}
title={title} title={panelChromeProps.title}
loadingState={data.state} loadingState={data.state}
statusMessage={errorMessage} statusMessage={errorMessage}
statusMessageOnClick={this.onOpenErrorInspect} statusMessageOnClick={panelChromeProps.onOpenErrorInspect}
description={!!panel.description ? this.onShowPanelDescription : undefined} description={panelChromeProps.description}
titleItems={titleItems} titleItems={panelChromeProps.titleItems}
menu={this.props.hideMenu ? undefined : menu} menu={this.props.hideMenu ? undefined : menu}
dragClass={dragClass} dragClass={panelChromeProps.dragClass}
dragClassCancel="grid-drag-cancel" dragClassCancel="grid-drag-cancel"
padding={padding} padding={panelChromeProps.padding}
hoverHeaderOffset={hoverHeaderOffset} hoverHeaderOffset={hoverHeaderOffset}
hoverHeader={this.hasOverlayHeader()} hoverHeader={panelChromeProps.hasOverlayHeader()}
displayMode={transparent ? 'transparent' : 'default'} displayMode={transparent ? 'transparent' : 'default'}
onCancelQuery={this.onCancelQuery} onCancelQuery={panelChromeProps.onCancelQuery}
> >
{(innerWidth, innerHeight) => ( {(innerWidth, innerHeight) => (
<> <>
@@ -0,0 +1,120 @@
import React from 'react';
import { LinkModel, PanelData, PanelPlugin, renderMarkdown } from '@grafana/data';
import { getTemplateSrv, locationService, reportInteraction } from '@grafana/runtime';
import { PanelPadding } from '@grafana/ui';
import { InspectTab } from 'app/features/inspector/types';
import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
import { PanelHeaderTitleItems } from '../dashgrid/PanelHeader/PanelHeaderTitleItems';
import { DashboardModel, PanelModel } from '../state';
interface CommonProps {
panel: PanelModel;
data: PanelData;
dashboard: DashboardModel;
plugin: PanelPlugin;
isViewing: boolean;
isEditing: boolean;
isInView: boolean;
width: number;
height: number;
hideMenu?: boolean;
}
export function getPanelChromeProps(props: CommonProps) {
let descriptionInteractionReported = false;
function hasOverlayHeader() {
// always show normal header if we have time override
if (props.data.request && props.data.request.timeInfo) {
return false;
}
return !props.panel.hasTitle();
}
const onShowPanelDescription = () => {
const descriptionMarkdown = getTemplateSrv().replace(props.panel.description, props.panel.scopedVars);
const interpolatedDescription = renderMarkdown(descriptionMarkdown);
if (!descriptionInteractionReported) {
// Description rendering function can be called multiple times due to re-renders but we want to report the interaction once.
reportInteraction('dashboards_panelheader_description_displayed');
descriptionInteractionReported = true;
}
return interpolatedDescription;
};
const onShowPanelLinks = (): LinkModel[] => {
const linkSupplier = getPanelLinksSupplier(props.panel);
if (!linkSupplier) {
return [];
}
const panelLinks = linkSupplier && linkSupplier.getLinks(props.panel.replaceVariables);
return panelLinks.map((panelLink) => ({
...panelLink,
onClick: (...args) => {
reportInteraction('dashboards_panelheader_datalink_clicked', { has_multiple_links: panelLinks.length > 1 });
panelLink.onClick?.(...args);
},
}));
};
const onOpenInspector = (e: React.SyntheticEvent, tab: string) => {
e.stopPropagation();
locationService.partial({ inspect: props.panel.id, inspectTab: tab });
};
const onOpenErrorInspect = (e: React.SyntheticEvent) => {
e.stopPropagation();
locationService.partial({ inspect: props.panel.id, inspectTab: InspectTab.Error });
reportInteraction('dashboards_panelheader_statusmessage_clicked');
};
const onCancelQuery = () => {
props.panel.getQueryRunner().cancelQuery();
reportInteraction('dashboards_panelheader_cancelquery_clicked', { data_state: props.data.state });
};
const padding: PanelPadding = props.plugin.noPadding ? 'none' : 'md';
const alertState = props.data.alertState?.state;
const showTitleItems =
(props.panel.links && props.panel.links.length > 0 && onShowPanelLinks) ||
(props.data.series.length > 0 && props.data.series.some((v) => (v.meta?.notices?.length ?? 0) > 0)) ||
(props.data.request && props.data.request.timeInfo) ||
alertState;
const titleItems = showTitleItems && (
<PanelHeaderTitleItems
alertState={alertState}
data={props.data}
panelId={props.panel.id}
panelLinks={props.panel.links}
onShowPanelLinks={onShowPanelLinks}
/>
);
const description = props.panel.description ? onShowPanelDescription() : undefined;
const dragClass = !(props.isViewing || props.isEditing) ? 'grid-drag-handle' : '';
const title = props.panel.getDisplayTitle();
return {
hasOverlayHeader,
onShowPanelDescription,
onShowPanelLinks,
onOpenInspector,
onOpenErrorInspect,
onCancelQuery,
padding,
description,
dragClass,
title,
titleItems,
};
}