From f42bb84cbf6d22727df4972170b5f0cec9499bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 9 Feb 2021 06:05:34 +0100 Subject: [PATCH] TextPanel: Fixes so panel title is updated when variables change (#30884) * TextPanel: Fixes so panel title is updated when variables change * Tests: fixes tests * Chore: updates after PR comments --- packages/grafana-data/src/types/geometry.ts | 7 + packages/grafana-data/src/types/index.ts | 1 + .../components/uPlot/plugins/ClickPlugin.tsx | 14 +- .../dashboard/dashgrid/DashboardPanel.tsx | 8 +- .../dashboard/dashgrid/PanelChrome.test.tsx | 22 +- .../dashboard/dashgrid/PanelChrome.tsx | 5 +- .../dashboard/dashgrid/PanelChromeAngular.tsx | 8 +- .../dashgrid/PanelHeader/PanelHeader.tsx | 266 ++++-------------- .../PanelHeaderLoadingIndicator.tsx | 48 ++++ .../PanelHeader/PanelHeaderMenuProvider.tsx | 29 ++ .../PanelHeader/PanelHeaderMenuTrigger.tsx | 51 ++++ .../PanelHeader/PanelHeaderMenuWrapper.tsx | 28 ++ .../PanelHeader/PanelHeaderNotice.tsx | 27 ++ .../PanelHeader/PanelHeaderNotices.tsx | 47 ++++ 14 files changed, 318 insertions(+), 243 deletions(-) create mode 100644 packages/grafana-data/src/types/geometry.ts create mode 100644 public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderLoadingIndicator.tsx create mode 100644 public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuProvider.tsx create mode 100644 public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuTrigger.tsx create mode 100644 public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuWrapper.tsx create mode 100644 public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderNotice.tsx create mode 100644 public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderNotices.tsx diff --git a/packages/grafana-data/src/types/geometry.ts b/packages/grafana-data/src/types/geometry.ts new file mode 100644 index 00000000000..a4a4558af8e --- /dev/null +++ b/packages/grafana-data/src/types/geometry.ts @@ -0,0 +1,7 @@ +/** + * A coordinate on a two dimensional plane. + */ +export interface CartesianCoords2D { + x: number; + y: number; +} diff --git a/packages/grafana-data/src/types/index.ts b/packages/grafana-data/src/types/index.ts index 8d31ee8b3cc..3e88ca59bf5 100644 --- a/packages/grafana-data/src/types/index.ts +++ b/packages/grafana-data/src/types/index.ts @@ -29,5 +29,6 @@ export * from './explore'; export * from './legacyEvents'; export * from './live'; export * from './variables'; +export * from './geometry'; export { GrafanaConfig, BuildInfo, FeatureToggles, LicenseInfo } from './config'; diff --git a/packages/grafana-ui/src/components/uPlot/plugins/ClickPlugin.tsx b/packages/grafana-ui/src/components/uPlot/plugins/ClickPlugin.tsx index 580b2372f92..85aa77fc773 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/ClickPlugin.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/ClickPlugin.tsx @@ -1,5 +1,6 @@ -import React, { useState, useCallback, useEffect } from 'react'; -import { Global, css as cssCore } from '@emotion/core'; +import React, { useCallback, useEffect, useState } from 'react'; +import { css as cssCore, Global } from '@emotion/core'; +import { CartesianCoords2D } from '@grafana/data'; import { PlotPluginProps } from '../types'; import { usePlotPluginContext } from '../context'; @@ -10,9 +11,9 @@ interface ClickPluginAPI { point: { seriesIdx: number | null; dataIdx: number | null }; coords: { // coords relative to plot canvas, css px - plotCanvas: Coords; + plotCanvas: CartesianCoords2D; // coords relative to viewport , css px - viewport: Coords; + viewport: CartesianCoords2D; }; // coords relative to plot canvas, css px clearSelection: () => void; @@ -26,11 +27,6 @@ interface ClickPluginProps extends PlotPluginProps { children: (api: ClickPluginAPI) => React.ReactElement | null; } -interface Coords { - x: number; - y: number; -} - // Exposes API for Graph click interactions export const ClickPlugin: React.FC = ({ id, onClick, children }) => { const pluginId = `ClickPlugin:${id}`; diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 4c4009135dc..f475124a456 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -10,10 +10,9 @@ import { PanelChromeAngular } from './PanelChromeAngular'; // Actions import { initDashboardPanel } from '../state/actions'; -import { updateLocation } from 'app/core/reducers/location'; // Types -import { PanelModel, DashboardModel } from '../state'; +import { DashboardModel, PanelModel } from '../state'; import { StoreState } from 'app/types'; import { PanelPlugin } from '@grafana/data'; @@ -40,7 +39,7 @@ const mapStateToProps = (state: StoreState, props: OwnProps) => { }; }; -const mapDispatchToProps = { initDashboardPanel, updateLocation }; +const mapDispatchToProps = { initDashboardPanel }; const connector = connect(mapStateToProps, mapDispatchToProps); @@ -76,7 +75,7 @@ export class DashboardPanelUnconnected extends PureComponent { }; renderPanel(plugin: PanelPlugin) { - const { dashboard, panel, isViewing, isInView, isEditing, updateLocation } = this.props; + const { dashboard, panel, isViewing, isInView, isEditing } = this.props; return ( @@ -110,7 +109,6 @@ export class DashboardPanelUnconnected extends PureComponent { isInView={isInView} width={width} height={height} - updateLocation={updateLocation} /> ); }} diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.test.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.test.tsx index 84685e6c1db..a2bbbe4aad1 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.test.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.test.tsx @@ -1,11 +1,12 @@ import React, { FC } from 'react'; import { ReplaySubject } from 'rxjs'; +import { Provider } from 'react-redux'; +import configureMockStore from 'redux-mock-store'; import { act, render, screen } from '@testing-library/react'; import { getDefaultTimeRange, LoadingState, PanelData, PanelPlugin, PanelProps } from '@grafana/data'; import { PanelChrome, Props } from './PanelChrome'; import { DashboardModel, PanelModel } from '../state'; -import { updateLocation } from '../../../core/actions'; import { PanelQueryRunner } from '../../query/state/PanelQueryRunner'; import { setTimeSrv, TimeSrv } from '../services/TimeSrv'; @@ -16,6 +17,8 @@ jest.mock('app/core/profiler', () => ({ })); function setupTestContext(options: Partial) { + const mockStore = configureMockStore(); + const store = mockStore({ dashboard: { panels: [] } }); const subject: ReplaySubject = new ReplaySubject(); const panelQueryRunner = ({ getData: () => subject, @@ -48,19 +51,22 @@ function setupTestContext(options: Partial) { isInView: false, width: 100, height: 100, - updateLocation: (jest.fn() as unknown) as typeof updateLocation, }; const props = { ...defaults, ...options }; - const { rerender } = render(); + const { rerender } = render( + + + + ); - return { rerender, props, subject }; + return { rerender, props, subject, store }; } describe('PanelChrome', () => { describe('when the user scrolls by a panel so fast that it starts loading data but scrolls out of view', () => { it('then it should load the panel successfully when scrolled into view again', () => { - const { rerender, props, subject } = setupTestContext({}); + const { rerender, props, subject, store } = setupTestContext({}); expect(screen.queryByText(/plugin panel to render/i)).not.toBeInTheDocument(); @@ -70,7 +76,11 @@ describe('PanelChrome', () => { }); const newProps = { ...props, isInView: true }; - rerender(); + rerender( + + + + ); expect(screen.getByText(/plugin panel to render/i)).toBeInTheDocument(); }); diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 684faa94faf..47bc5d63d22 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -10,7 +10,6 @@ import { getTimeSrv, TimeSrv } from '../services/TimeSrv'; import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel'; import { profiler } from 'app/core/profiler'; import config from 'app/core/config'; -import { updateLocation } from 'app/core/actions'; // Types import { DashboardModel, PanelModel } from '../state'; import { PANEL_BORDER } from 'app/core/constants'; @@ -40,7 +39,6 @@ export interface Props { isInView: boolean; width: number; height: number; - updateLocation: typeof updateLocation; } export interface State { @@ -324,7 +322,7 @@ export class PanelChrome extends Component { } render() { - const { dashboard, panel, isViewing, isEditing, width, height, updateLocation } = this.props; + const { dashboard, panel, isViewing, isEditing, width, height } = this.props; const { errorMessage, data } = this.state; const { transparent } = panel; @@ -347,7 +345,6 @@ export class PanelChrome extends Component { isEditing={isEditing} isViewing={isViewing} data={data} - updateLocation={updateLocation} /> {({ error }) => { diff --git a/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx b/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx index 02bc13f988f..b2ebd168efe 100644 --- a/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx @@ -14,7 +14,6 @@ import config from 'app/core/config'; import { DashboardModel, PanelModel } from '../state'; import { StoreState } from 'app/types'; import { getDefaultTimeRange, LoadingState, PanelData, PanelPlugin } from '@grafana/data'; -import { updateLocation } from 'app/core/actions'; import { PANEL_BORDER } from 'app/core/constants'; import { selectors } from '@grafana/e2e-selectors'; import { RenderEvent } from 'app/types/events'; @@ -36,7 +35,6 @@ interface ConnectedProps { interface DispatchProps { setPanelAngularComponent: typeof setPanelAngularComponent; - updateLocation: typeof updateLocation; } export type Props = OwnProps & ConnectedProps & DispatchProps; @@ -214,7 +212,7 @@ export class PanelChromeAngularUnconnected extends PureComponent { } render() { - const { dashboard, panel, isViewing, isEditing, plugin, angularComponent, updateLocation } = this.props; + const { dashboard, panel, isViewing, isEditing, plugin } = this.props; const { errorMessage, data, alertState } = this.state; const { transparent } = panel; @@ -239,13 +237,11 @@ export class PanelChromeAngularUnconnected extends PureComponent { dashboard={dashboard} title={panel.title} description={panel.description} - angularComponent={angularComponent} links={panel.links} error={errorMessage} isViewing={isViewing} isEditing={isEditing} data={data} - updateLocation={updateLocation} alertState={alertState} />
@@ -262,6 +258,6 @@ const mapStateToProps: MapStateToProps = ( }; }; -const mapDispatchToProps: MapDispatchToProps = { setPanelAngularComponent, updateLocation }; +const mapDispatchToProps: MapDispatchToProps = { setPanelAngularComponent }; export const PanelChromeAngular = connect(mapStateToProps, mapDispatchToProps)(PanelChromeAngularUnconnected); diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index 35046f13990..540f1fdeff2 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -1,234 +1,74 @@ -import React, { PureComponent } from 'react'; -import classNames from 'classnames'; -import { DataLink, LoadingState, PanelData, PanelMenuItem, QueryResultMetaNotice } from '@grafana/data'; -import { AngularComponent, config } from '@grafana/runtime'; -import { ClickOutsideWrapper, Icon, IconName, Tooltip, stylesFactory } from '@grafana/ui'; +import React, { FC } from 'react'; +import { cx } from 'emotion'; +import { DataLink, PanelData } from '@grafana/data'; +import { Icon } from '@grafana/ui'; import { selectors } from '@grafana/e2e-selectors'; import PanelHeaderCorner from './PanelHeaderCorner'; -import { PanelHeaderMenu } from './PanelHeaderMenu'; - 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 { getPanelMenu } from 'app/features/dashboard/utils/getPanelMenu'; -import { updateLocation } from 'app/core/actions'; -import { css } from 'emotion'; +import { PanelHeaderNotices } from './PanelHeaderNotices'; +import { PanelHeaderMenuTrigger } from './PanelHeaderMenuTrigger'; +import { PanelHeaderLoadingIndicator } from './PanelHeaderLoadingIndicator'; +import { PanelHeaderMenuWrapper } from './PanelHeaderMenuWrapper'; export interface Props { panel: PanelModel; dashboard: DashboardModel; title?: string; description?: string; - angularComponent?: AngularComponent | null; links?: DataLink[]; error?: string; alertState?: string; isViewing: boolean; isEditing: boolean; data: PanelData; - updateLocation: typeof updateLocation; } -interface ClickCoordinates { - x: number; - y: number; -} +export const PanelHeader: FC = ({ panel, error, isViewing, isEditing, data, alertState, dashboard }) => { + const onCancelQuery = () => panel.getQueryRunner().cancelQuery(); + const title = panel.replaceVariables(panel.title, {}, 'text'); + const className = cx('panel-header', !(isViewing || isEditing) ? 'grid-drag-handle' : ''); -interface State { - panelMenuOpen: boolean; - menuItems: PanelMenuItem[]; -} - -export class PanelHeader extends PureComponent { - clickCoordinates: ClickCoordinates = { x: 0, y: 0 }; - - state: State = { - panelMenuOpen: false, - menuItems: [], - }; - - eventToClickCoordinates = (event: React.MouseEvent) => { - return { - x: Math.floor(event.clientX), - y: Math.floor(event.clientY), - }; - }; - - onMouseDown = (event: React.MouseEvent) => { - this.clickCoordinates = this.eventToClickCoordinates(event); - }; - - isClick = (clickCoordinates: ClickCoordinates) => { - return clickCoordinates.x === this.clickCoordinates.x && clickCoordinates.y === this.clickCoordinates.y; - }; - - onMenuToggle = (event: React.MouseEvent) => { - if (!this.isClick(this.eventToClickCoordinates(event))) { - return; - } - - event.stopPropagation(); - - const { dashboard, panel, angularComponent } = this.props; - const menuItems = getPanelMenu(dashboard, panel, angularComponent); - - this.setState({ - panelMenuOpen: !this.state.panelMenuOpen, - menuItems, - }); - }; - - closeMenu = () => { - this.setState({ - panelMenuOpen: false, - }); - }; - - onCancelQuery = () => { - this.props.panel.getQueryRunner().cancelQuery(); - }; - - renderLoadingState(state: LoadingState): JSX.Element | null { - if (state === LoadingState.Loading) { - return ( -
- - - -
- ); - } - - if (state === LoadingState.Streaming) { - const styles = getStyles(); - - return ( -
-
-
- ); - } - - return null; - } - - openInspect = (e: React.SyntheticEvent, tab: string) => { - const { updateLocation, panel } = this.props; - - e.stopPropagation(); - - updateLocation({ - query: { inspect: panel.id, inspectTab: tab }, - partial: true, - }); - }; - - // This will show one icon for each severity - renderNotice = (notice: QueryResultMetaNotice) => { - let iconName: IconName = 'info-circle'; - if (notice.severity === 'error' || notice.severity === 'warning') { - iconName = 'exclamation-triangle'; - } - - return ( - - {notice.inspect ? ( -
this.openInspect(e, notice.inspect!)}> - -
- ) : ( - - - - )} -
- ); - }; - - render() { - const { panel, error, isViewing, isEditing, data, alertState } = this.props; - const { menuItems } = this.state; - const title = panel.replaceVariables(panel.title, {}, 'text'); - - const panelHeaderClass = classNames({ - 'panel-header': true, - 'grid-drag-handle': !(isViewing || isEditing), - }); - - // dedupe on severity - const notices: Record = {}; - - for (const series of data.series) { - if (series.meta && series.meta.notices) { - for (const notice of series.meta.notices) { - notices[notice.severity] = notice; - } - } - } - - return ( - <> - {this.renderLoadingState(data.state)} -
- -
-
- {Object.values(notices).map(this.renderNotice)} - {alertState && ( - - )} - {title} - - {this.state.panelMenuOpen && ( - - - - )} - {data.request && data.request.timeInfo && ( - - {data.request.timeInfo} - - )} -
-
-
- - ); - } -} - -/* - * Styles - */ -export const getStyles = stylesFactory(() => { - return { - streamIndicator: css` - width: 10px; - height: 10px; - background: ${config.theme.colors.textFaint}; - box-shadow: 0 0 2px ${config.theme.colors.textFaint}; - border-radius: 50%; - position: relative; - top: 6px; - right: 1px; - `, - }; -}); + return ( + <> + +
+ + + {({ closeMenu, panelMenuOpen }) => { + return ( +
+ + {alertState ? ( + + ) : null} + {title} + + + {data.request && data.request.timeInfo && ( + + {data.request.timeInfo} + + )} +
+ ); + }} +
+
+ + ); +}; diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderLoadingIndicator.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderLoadingIndicator.tsx new file mode 100644 index 00000000000..a32ea8fed4d --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderLoadingIndicator.tsx @@ -0,0 +1,48 @@ +import React, { FC } from 'react'; +import { css } from 'emotion'; +import { GrafanaTheme, LoadingState } from '@grafana/data'; +import { Icon, Tooltip, useStyles } from '@grafana/ui'; + +interface Props { + state: LoadingState; + onClick: () => void; +} + +export const PanelHeaderLoadingIndicator: FC = ({ state, onClick }) => { + const styles = useStyles(getStyles); + + if (state === LoadingState.Loading) { + return ( +
+ + + +
+ ); + } + + if (state === LoadingState.Streaming) { + return ( +
+
+
+ ); + } + + return null; +}; + +function getStyles(theme: GrafanaTheme) { + return { + streamIndicator: css` + width: 10px; + height: 10px; + background: ${theme.colors.textFaint}; + box-shadow: 0 0 2px ${theme.colors.textFaint}; + border-radius: 50%; + position: relative; + top: 6px; + right: 1px; + `, + }; +} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuProvider.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuProvider.tsx new file mode 100644 index 00000000000..4fd29502f92 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuProvider.tsx @@ -0,0 +1,29 @@ +import { FC, ReactElement, useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { PanelMenuItem } from '@grafana/data'; + +import { DashboardModel, PanelModel } from '../../state'; +import { StoreState } from '../../../../types'; +import { getPanelMenu } from '../../utils/getPanelMenu'; + +interface PanelHeaderMenuProviderApi { + items: PanelMenuItem[]; +} + +interface Props { + panel: PanelModel; + dashboard: DashboardModel; + children: (props: PanelHeaderMenuProviderApi) => ReactElement; +} + +export const PanelHeaderMenuProvider: FC = ({ panel, dashboard, children }) => { + const [items, setItems] = useState([]); + const angularComponent = useSelector( + (state: StoreState) => state.dashboard.panels[panel.id]?.angularComponent || null + ); + useEffect(() => { + setItems(getPanelMenu(dashboard, panel, angularComponent)); + }, [dashboard, panel, angularComponent, setItems]); + + return children({ items }); +}; diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuTrigger.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuTrigger.tsx new file mode 100644 index 00000000000..38741292e10 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuTrigger.tsx @@ -0,0 +1,51 @@ +import React, { FC, HTMLAttributes, MouseEvent, ReactElement, useCallback, useState } from 'react'; +import { CartesianCoords2D } from '@grafana/data'; + +interface PanelHeaderMenuTriggerApi { + panelMenuOpen: boolean; + closeMenu: () => void; +} + +interface Props extends HTMLAttributes { + children: (props: PanelHeaderMenuTriggerApi) => ReactElement; +} + +export const PanelHeaderMenuTrigger: FC = ({ children, ...divProps }) => { + const [clickCoordinates, setClickCoordinates] = useState({ x: 0, y: 0 }); + const [panelMenuOpen, setPanelMenuOpen] = useState(false); + const onMenuToggle = useCallback( + (event: MouseEvent) => { + if (!isClick(clickCoordinates, eventToClickCoordinates(event))) { + return; + } + + event.stopPropagation(); + + setPanelMenuOpen(!panelMenuOpen); + }, + [clickCoordinates, panelMenuOpen, setPanelMenuOpen] + ); + const onMouseDown = useCallback( + (event: MouseEvent) => { + setClickCoordinates(eventToClickCoordinates(event)); + }, + [setClickCoordinates] + ); + + return ( +
+ {children({ panelMenuOpen, closeMenu: () => setPanelMenuOpen(false) })} +
+ ); +}; + +function isClick(current: CartesianCoords2D, clicked: CartesianCoords2D): boolean { + return clicked.x === current.x && clicked.y === current.y; +} + +function eventToClickCoordinates(event: MouseEvent): CartesianCoords2D { + return { + x: Math.floor(event.clientX), + y: Math.floor(event.clientY), + }; +} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuWrapper.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuWrapper.tsx new file mode 100644 index 00000000000..596dc96fa65 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuWrapper.tsx @@ -0,0 +1,28 @@ +import React, { FC } from 'react'; +import { ClickOutsideWrapper } from '@grafana/ui'; +import { PanelHeaderMenuProvider } from './PanelHeaderMenuProvider'; +import { PanelHeaderMenu } from './PanelHeaderMenu'; +import { DashboardModel, PanelModel } from '../../state'; + +interface Props { + panel: PanelModel; + dashboard: DashboardModel; + show: boolean; + onClose: () => void; +} + +export const PanelHeaderMenuWrapper: FC = ({ show, onClose, panel, dashboard }) => { + if (!show) { + return null; + } + + return ( + + + {({ items }) => { + return ; + }} + + + ); +}; diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderNotice.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderNotice.tsx new file mode 100644 index 00000000000..16e9670523a --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderNotice.tsx @@ -0,0 +1,27 @@ +import React, { FC } from 'react'; +import { QueryResultMetaNotice } from '@grafana/data'; +import { Icon, Tooltip } from '@grafana/ui'; + +interface Props { + notice: QueryResultMetaNotice; + onClick: (e: React.SyntheticEvent, tab: string) => void; +} + +export const PanelHeaderNotice: FC = ({ notice, onClick }) => { + const iconName = + notice.severity === 'error' || notice.severity === 'warning' ? 'exclamation-triangle' : 'info-circle'; + + return ( + + {notice.inspect ? ( +
onClick(e, notice.inspect!)}> + +
+ ) : ( + + + + )} +
+ ); +}; diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderNotices.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderNotices.tsx new file mode 100644 index 00000000000..d4b888a9aa0 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderNotices.tsx @@ -0,0 +1,47 @@ +import React, { FC, useCallback } from 'react'; +import { DataFrame, QueryResultMetaNotice } from '@grafana/data'; +import { PanelHeaderNotice } from './PanelHeaderNotice'; +import { useDispatch } from 'react-redux'; +import { updateLocation } from '../../../../core/actions'; + +interface Props { + panelId: number; + frames: DataFrame[]; +} + +export const PanelHeaderNotices: FC = ({ frames, panelId }) => { + const dispatch = useDispatch(); + const openInspect = useCallback( + (e: React.SyntheticEvent, tab: string) => { + e.stopPropagation(); + + dispatch( + updateLocation({ + query: { inspect: panelId, inspectTab: tab }, + partial: true, + }) + ); + }, + [panelId] + ); + + // dedupe on severity + const notices: Record = {}; + for (const frame of frames) { + if (!frame.meta || !frame.meta.notices) { + continue; + } + + for (const notice of frame.meta.notices) { + notices[notice.severity] = notice; + } + } + + return ( + <> + {Object.values(notices).map((notice) => ( + + ))} + + ); +};