diff --git a/packages/grafana-data/src/types/icon.ts b/packages/grafana-data/src/types/icon.ts index bc0e5ff8b3d..9c978f586fd 100644 --- a/packages/grafana-data/src/types/icon.ts +++ b/packages/grafana-data/src/types/icon.ts @@ -195,6 +195,7 @@ export const availableIconsIndex = { 'step-backward': true, 'stopwatch-slash': true, sync: true, + 'sync-slash': true, table: true, 'tag-alt': true, 'telegram-alt': true, diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index e822dada9f5..626326f3cf9 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -6,6 +6,7 @@ import { GrafanaTheme2, LoadingState } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { useStyles2, useTheme2 } from '../../themes'; +import { DelayRender } from '../../utils/DelayRender'; import { Icon } from '../Icon/Icon'; import { LoadingBar } from '../LoadingBar/LoadingBar'; import { Tooltip } from '../Tooltip'; @@ -54,6 +55,7 @@ export interface PanelChromeProps { */ leftItems?: ReactNode[]; displayMode?: 'default' | 'transparent'; + onCancelQuery?: () => void; } /** @@ -82,6 +84,7 @@ export function PanelChrome({ statusMessage, statusMessageOnClick, leftItems, + onCancelQuery, }: PanelChromeProps) { const theme = useTheme2(); const styles = useStyles2(getStyles); @@ -124,12 +127,21 @@ export function PanelChrome({ {loadingState === LoadingState.Streaming && ( - - + + )} + {loadingState === LoadingState.Loading && onCancelQuery && ( + + + + + + + + )} ); diff --git a/packages/grafana-ui/src/components/PanelChrome/TitleItem.tsx b/packages/grafana-ui/src/components/PanelChrome/TitleItem.tsx index a062a64bd28..2bd0fa99e52 100644 --- a/packages/grafana-ui/src/components/PanelChrome/TitleItem.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/TitleItem.tsx @@ -5,6 +5,7 @@ import { GrafanaTheme2, LinkModel, LinkTarget } from '@grafana/data'; import { useStyles2 } from '../../themes'; import { getFocusStyles, getMouseFocusStyles } from '../../themes/mixins'; +import { Button } from '../Button'; type TitleItemProps = { className?: string; @@ -15,7 +16,9 @@ type TitleItemProps = { title?: string; }; -export const TitleItem = forwardRef( +type TitleItemElement = HTMLAnchorElement & HTMLButtonElement; + +export const TitleItem = forwardRef( ({ className, children, href, onClick, target, title, ...rest }, ref) => { const styles = useStyles2(getStyles); @@ -33,6 +36,12 @@ export const TitleItem = forwardRef( {children} ); + } else if (onClick) { + return ( + + ); } else { return ( diff --git a/packages/grafana-ui/src/utils/DelayRender.tsx b/packages/grafana-ui/src/utils/DelayRender.tsx new file mode 100644 index 00000000000..986837cded4 --- /dev/null +++ b/packages/grafana-ui/src/utils/DelayRender.tsx @@ -0,0 +1,23 @@ +import React, { useEffect, useState } from 'react'; + +interface Props { + children: React.ReactNode; + delay: number; +} + +/** + * Delay the rendering of the children by N amount of milliseconds + */ +export function DelayRender({ children, delay }: Props) { + const [shouldRender, setRender] = useState(false); + useEffect(() => { + const intervalId = setInterval(() => { + setRender(true); + }, delay); + return () => { + clearInterval(intervalId); + }; + }, [children, delay]); + + return <>{shouldRender ? children : null}; +} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuProvider.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuProvider.tsx index 3fc8414e177..1875dfdb1c4 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuProvider.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuProvider.tsx @@ -23,7 +23,7 @@ export function PanelHeaderMenuProvider({ panel, dashboard, loadingState, childr const angularComponent = useSelector((state) => getPanelStateForModel(state, panel)?.angularComponent); useEffect(() => { - setItems(getPanelMenu(dashboard, panel, loadingState, angularComponent)); + setItems(getPanelMenu(dashboard, panel, angularComponent)); }, [dashboard, panel, angularComponent, loadingState, setItems]); return children({ items }); diff --git a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx index 309f0baaeb3..73d9929ba7b 100644 --- a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx +++ b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx @@ -641,6 +641,11 @@ export class PanelStateWrapper extends PureComponent { 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; @@ -705,6 +710,7 @@ export class PanelStateWrapper extends PureComponent { hoverHeaderOffset={hoverHeaderOffset} hoverHeader={this.hasOverlayHeader()} displayMode={transparent ? 'transparent' : 'default'} + onCancelQuery={this.onCancelQuery} > {(innerWidth, innerHeight) => ( <> diff --git a/public/app/features/dashboard/utils/getPanelMenu.test.ts b/public/app/features/dashboard/utils/getPanelMenu.test.ts index c548185062c..94fa6214058 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.test.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.test.ts @@ -10,7 +10,6 @@ import { PluginExtensionRegistryItem, setPluginsExtensionRegistry, } from '@grafana/runtime'; -import { LoadingState } from '@grafana/schema'; import config from 'app/core/config'; import * as actions from 'app/features/explore/state/main'; import { setStore } from 'app/store/store'; @@ -110,36 +109,6 @@ describe('getPanelMenu()', () => { `); }); - it('should return the correct panel menu items when data is streaming', () => { - const panel = new PanelModel({}); - const dashboard = createDashboardModelFixture({}); - - const menuItems = getPanelMenu(dashboard, panel, LoadingState.Streaming); - expect(menuItems).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - iconClassName: 'circle', - text: 'Stop query', - }), - ]) - ); - }); - - it('should return the correct panel menu items when data is loading', () => { - const panel = new PanelModel({}); - const dashboard = createDashboardModelFixture({}); - - const menuItems = getPanelMenu(dashboard, panel, LoadingState.Loading); - expect(menuItems).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - iconClassName: 'circle', - text: 'Stop query', - }), - ]) - ); - }); - describe('when extending panel menu from plugins', () => { it('should contain menu item from link extension', () => { setPluginsExtensionRegistry({ @@ -156,7 +125,7 @@ describe('getPanelMenu()', () => { const panel = new PanelModel({}); const dashboard = createDashboardModelFixture({}); - const menuItems = getPanelMenu(dashboard, panel, LoadingState.Loading); + const menuItems = getPanelMenu(dashboard, panel); const moreSubMenu = menuItems.find((i) => i.text === 'More...')?.subMenu; expect(moreSubMenu).toEqual( @@ -184,7 +153,7 @@ describe('getPanelMenu()', () => { const panel = new PanelModel({}); const dashboard = createDashboardModelFixture({}); - const menuItems = getPanelMenu(dashboard, panel, LoadingState.Loading); + const menuItems = getPanelMenu(dashboard, panel); const moreSubMenu = menuItems.find((i) => i.text === 'More...')?.subMenu; expect(moreSubMenu).toEqual( @@ -223,7 +192,7 @@ describe('getPanelMenu()', () => { const panel = new PanelModel({}); const dashboard = createDashboardModelFixture({}); - const menuItems = getPanelMenu(dashboard, panel, LoadingState.Loading); + const menuItems = getPanelMenu(dashboard, panel); const moreSubMenu = menuItems.find((i) => i.text === 'More...')?.subMenu; expect(moreSubMenu).toEqual( @@ -254,7 +223,7 @@ describe('getPanelMenu()', () => { const panel = new PanelModel({}); const dashboard = createDashboardModelFixture({}); - const menuItems = getPanelMenu(dashboard, panel, LoadingState.Loading); + const menuItems = getPanelMenu(dashboard, panel); const moreSubMenu = menuItems.find((i) => i.text === 'More...')?.subMenu; expect(moreSubMenu).toEqual( @@ -310,7 +279,7 @@ describe('getPanelMenu()', () => { title: 'My dashboard', }); - getPanelMenu(dashboard, panel, LoadingState.Loading); + getPanelMenu(dashboard, panel); const context: PluginExtensionPanelContext = { pluginId: 'timeseries', @@ -392,7 +361,7 @@ describe('getPanelMenu()', () => { title: 'My dashboard', }); - expect(() => getPanelMenu(dashboard, panel, LoadingState.Loading)).toThrowError(TypeError); + expect(() => getPanelMenu(dashboard, panel)).toThrowError(TypeError); }); }); @@ -405,7 +374,7 @@ describe('getPanelMenu()', () => { const panel = new PanelModel({ isViewing: true }); const dashboard = createDashboardModelFixture({}); - const menuItems = getPanelMenu(dashboard, panel, undefined, angularComponent); + const menuItems = getPanelMenu(dashboard, panel, angularComponent); expect(menuItems).toMatchInlineSnapshot(` [ { diff --git a/public/app/features/dashboard/utils/getPanelMenu.ts b/public/app/features/dashboard/utils/getPanelMenu.ts index a4195ce3eb3..4c1b9473fa8 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.ts @@ -12,7 +12,6 @@ import { reportInteraction, PluginExtensionPanelContext, } from '@grafana/runtime'; -import { LoadingState } from '@grafana/schema'; import { PanelCtrl } from 'app/angular/panel/panel_ctrl'; import config from 'app/core/config'; import { t } from 'app/core/internationalization'; @@ -39,7 +38,6 @@ import { getTimeSrv } from '../services/TimeSrv'; export function getPanelMenu( dashboard: DashboardModel, panel: PanelModel, - loadingState?: LoadingState, angularComponent?: AngularComponent | null ): PanelMenuItem[] { const onViewPanel = (event: React.MouseEvent) => { @@ -120,12 +118,6 @@ export function getPanelMenu( reportInteraction('dashboards_panelheader_togglelegend_clicked'); }; - const onCancelStreaming = (event: React.MouseEvent) => { - event.preventDefault(); - panel.getQueryRunner().cancelQuery(); - reportInteraction('dashboards_panelheader_cancelstreaming_clicked'); - }; - const menu: PanelMenuItem[] = []; if (!panel.isEditing) { @@ -146,17 +138,6 @@ export function getPanelMenu( }); } - if ( - dashboard.canEditPanel(panel) && - (loadingState === LoadingState.Streaming || loadingState === LoadingState.Loading) - ) { - menu.push({ - text: 'Stop query', - iconClassName: 'circle', - onClick: onCancelStreaming, - }); - } - menu.push({ text: t('panel.header-menu.share', `Share`), iconClassName: 'share-alt', diff --git a/public/app/features/query/state/PanelQueryRunner.ts b/public/app/features/query/state/PanelQueryRunner.ts index 0d6eb6c559e..fbd9b8efcde 100644 --- a/public/app/features/query/state/PanelQueryRunner.ts +++ b/public/app/features/query/state/PanelQueryRunner.ts @@ -317,8 +317,11 @@ export class PanelQueryRunner { this.subscription.unsubscribe(); - // If we have an old result with loading state, send it with done state - if (this.lastResult && this.lastResult.state === LoadingState.Loading) { + // If we have an old result with loading or streaming state, send it with done state + if ( + this.lastResult && + (this.lastResult.state === LoadingState.Loading || this.lastResult.state === LoadingState.Streaming) + ) { this.subject.next({ ...this.lastResult, state: LoadingState.Done,