diff --git a/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx b/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx index fd1ceed7030..47d9f581ef9 100644 --- a/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx @@ -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 { timeSrv: TimeSrv = getTimeSrv(); scopeProps?: AngularScopeProps; subs = new Subscription(); - constructor(props: Props) { super(props); this.state = { @@ -179,9 +182,10 @@ export class PanelChromeAngularUnconnected extends PureComponent { 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 { 'panel-content--no-padding': plugin.noPadding, }); - return ( -
- -
-
(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 = ( +
+
-
- ); + ); + + return ( + + {() =>
(this.element = element)} className="panel-height-helper" />} + + ); + } else { + return ( +
+ +
+
(this.element = element)} className="panel-height-helper" /> +
+
+ ); + } } } diff --git a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx index 9d5f2f13b2d..a05518aacde 100644 --- a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx +++ b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx @@ -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 { 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 { 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 { [`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 && ( - - ); - - 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 { {(innerWidth, innerHeight) => ( <> diff --git a/public/app/features/dashboard/utils/getPanelChromeProps.tsx b/public/app/features/dashboard/utils/getPanelChromeProps.tsx new file mode 100644 index 00000000000..fb4a68d5c99 --- /dev/null +++ b/public/app/features/dashboard/utils/getPanelChromeProps.tsx @@ -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 && ( + + ); + + 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, + }; +}