mirror of
https://github.com/grafana/grafana.git
synced 2025-02-12 08:35:43 -06:00
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:
parent
e6e741546f
commit
2a67b8ad32
@ -6,6 +6,7 @@ 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 { PanelChrome } from '@grafana/ui';
|
||||
import config from 'app/core/config';
|
||||
import { PANEL_BORDER } from 'app/core/constants';
|
||||
import { setPanelAngularComponent } from 'app/features/panel/state/reducers';
|
||||
@ -15,8 +16,10 @@ 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';
|
||||
|
||||
interface OwnProps {
|
||||
panel: PanelModel;
|
||||
@ -27,6 +30,7 @@ interface OwnProps {
|
||||
isInView: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
hideMenu?: boolean;
|
||||
}
|
||||
|
||||
interface ConnectedProps {
|
||||
@ -58,7 +62,6 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
|
||||
timeSrv: TimeSrv = getTimeSrv();
|
||||
scopeProps?: AngularScopeProps;
|
||||
subs = new Subscription();
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@ -179,9 +182,10 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
|
||||
const { dashboard, panel, isViewing, isEditing, plugin } = 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),
|
||||
@ -196,29 +200,63 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
|
||||
'panel-content--no-padding': plugin.noPadding,
|
||||
});
|
||||
|
||||
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" />
|
||||
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>
|
||||
</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,
|
||||
getDataSourceRef,
|
||||
getDefaultTimeRange,
|
||||
LinkModel,
|
||||
LoadingState,
|
||||
PanelData,
|
||||
PanelPlugin,
|
||||
PanelPluginMeta,
|
||||
PluginContextProvider,
|
||||
renderMarkdown,
|
||||
TimeRange,
|
||||
toDataFrameDTO,
|
||||
toUtc,
|
||||
} from '@grafana/data';
|
||||
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 {
|
||||
ErrorBoundary,
|
||||
PanelChrome,
|
||||
PanelContext,
|
||||
PanelContextProvider,
|
||||
PanelPadding,
|
||||
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 { InspectTab } from 'app/features/inspector/types';
|
||||
import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { applyFilterFromTable } from 'app/features/variables/adhoc/actions';
|
||||
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 { getTimeSrv, TimeSrv } from '../services/TimeSrv';
|
||||
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 { PanelHeaderTitleItems } from './PanelHeader/PanelHeaderTitleItems';
|
||||
import { seriesVisibilityConfigFactory } from './SeriesVisibilityConfigFactory';
|
||||
import { liveTimer } from './liveTimer';
|
||||
|
||||
@ -91,7 +86,6 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
||||
private readonly timeSrv: TimeSrv = getTimeSrv();
|
||||
private subs = new Subscription();
|
||||
private eventFilter: EventFilterOptions = { onlyLocal: true };
|
||||
private descriptionInteractionReported = false;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@ -605,53 +599,6 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
||||
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() {
|
||||
const { dashboard, panel, isViewing, isEditing, width, height, plugin } = this.props;
|
||||
const { errorMessage, data } = this.state;
|
||||
@ -668,27 +615,8 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
||||
[`panel-alert-state--${alertState}`]: alertState !== undefined,
|
||||
});
|
||||
|
||||
const title = panel.getDisplayTitle();
|
||||
const padding: PanelPadding = plugin.noPadding ? 'none' : 'md';
|
||||
const panelChromeProps = getPanelChromeProps({ ...this.props, data });
|
||||
|
||||
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) {
|
||||
// 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;
|
||||
@ -703,20 +631,20 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
||||
<PanelChrome
|
||||
width={width}
|
||||
height={height}
|
||||
title={title}
|
||||
title={panelChromeProps.title}
|
||||
loadingState={data.state}
|
||||
statusMessage={errorMessage}
|
||||
statusMessageOnClick={this.onOpenErrorInspect}
|
||||
description={!!panel.description ? this.onShowPanelDescription : undefined}
|
||||
titleItems={titleItems}
|
||||
statusMessageOnClick={panelChromeProps.onOpenErrorInspect}
|
||||
description={panelChromeProps.description}
|
||||
titleItems={panelChromeProps.titleItems}
|
||||
menu={this.props.hideMenu ? undefined : menu}
|
||||
dragClass={dragClass}
|
||||
dragClass={panelChromeProps.dragClass}
|
||||
dragClassCancel="grid-drag-cancel"
|
||||
padding={padding}
|
||||
padding={panelChromeProps.padding}
|
||||
hoverHeaderOffset={hoverHeaderOffset}
|
||||
hoverHeader={this.hasOverlayHeader()}
|
||||
hoverHeader={panelChromeProps.hasOverlayHeader()}
|
||||
displayMode={transparent ? 'transparent' : 'default'}
|
||||
onCancelQuery={this.onCancelQuery}
|
||||
onCancelQuery={panelChromeProps.onCancelQuery}
|
||||
>
|
||||
{(innerWidth, innerHeight) => (
|
||||
<>
|
||||
|
120
public/app/features/dashboard/utils/getPanelChromeProps.tsx
Normal file
120
public/app/features/dashboard/utils/getPanelChromeProps.tsx
Normal file
@ -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,
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user