Panel Header: Add CancelQuery option to panel header (#64796)

Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com>
This commit is contained in:
Alexa V
2023-03-16 13:56:58 +01:00
committed by GitHub
parent 10db808ea1
commit fef0ee913c
9 changed files with 67 additions and 63 deletions

View File

@@ -195,6 +195,7 @@ export const availableIconsIndex = {
'step-backward': true, 'step-backward': true,
'stopwatch-slash': true, 'stopwatch-slash': true,
sync: true, sync: true,
'sync-slash': true,
table: true, table: true,
'tag-alt': true, 'tag-alt': true,
'telegram-alt': true, 'telegram-alt': true,

View File

@@ -6,6 +6,7 @@ import { GrafanaTheme2, LoadingState } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { useStyles2, useTheme2 } from '../../themes'; import { useStyles2, useTheme2 } from '../../themes';
import { DelayRender } from '../../utils/DelayRender';
import { Icon } from '../Icon/Icon'; import { Icon } from '../Icon/Icon';
import { LoadingBar } from '../LoadingBar/LoadingBar'; import { LoadingBar } from '../LoadingBar/LoadingBar';
import { Tooltip } from '../Tooltip'; import { Tooltip } from '../Tooltip';
@@ -54,6 +55,7 @@ export interface PanelChromeProps {
*/ */
leftItems?: ReactNode[]; leftItems?: ReactNode[];
displayMode?: 'default' | 'transparent'; displayMode?: 'default' | 'transparent';
onCancelQuery?: () => void;
} }
/** /**
@@ -82,6 +84,7 @@ export function PanelChrome({
statusMessage, statusMessage,
statusMessageOnClick, statusMessageOnClick,
leftItems, leftItems,
onCancelQuery,
}: PanelChromeProps) { }: PanelChromeProps) {
const theme = useTheme2(); const theme = useTheme2();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
@@ -124,12 +127,21 @@ export function PanelChrome({
</div> </div>
{loadingState === LoadingState.Streaming && ( {loadingState === LoadingState.Streaming && (
<Tooltip content="Streaming"> <Tooltip content={onCancelQuery ? 'Stop streaming' : 'Streaming'}>
<TitleItem className={dragClassCancel} data-testid="panel-streaming"> <TitleItem className={dragClassCancel} data-testid="panel-streaming" onClick={onCancelQuery}>
<Icon name="circle-mono" size="md" className={styles.streaming} /> <Icon name="circle-mono" size="md" className={styles.streaming} />
</TitleItem> </TitleItem>
</Tooltip> </Tooltip>
)} )}
{loadingState === LoadingState.Loading && onCancelQuery && (
<DelayRender delay={2000}>
<Tooltip content="Cancel query">
<TitleItem className={dragClassCancel} data-testid="panel-cancel-query" onClick={onCancelQuery}>
<Icon name="sync-slash" size="md" />
</TitleItem>
</Tooltip>
</DelayRender>
)}
</> </>
); );

View File

@@ -5,6 +5,7 @@ import { GrafanaTheme2, LinkModel, LinkTarget } from '@grafana/data';
import { useStyles2 } from '../../themes'; import { useStyles2 } from '../../themes';
import { getFocusStyles, getMouseFocusStyles } from '../../themes/mixins'; import { getFocusStyles, getMouseFocusStyles } from '../../themes/mixins';
import { Button } from '../Button';
type TitleItemProps = { type TitleItemProps = {
className?: string; className?: string;
@@ -15,7 +16,9 @@ type TitleItemProps = {
title?: string; title?: string;
}; };
export const TitleItem = forwardRef<HTMLAnchorElement, TitleItemProps>( type TitleItemElement = HTMLAnchorElement & HTMLButtonElement;
export const TitleItem = forwardRef<TitleItemElement, TitleItemProps>(
({ className, children, href, onClick, target, title, ...rest }, ref) => { ({ className, children, href, onClick, target, title, ...rest }, ref) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
@@ -33,6 +36,12 @@ export const TitleItem = forwardRef<HTMLAnchorElement, TitleItemProps>(
{children} {children}
</a> </a>
); );
} else if (onClick) {
return (
<Button ref={ref} className={cx(styles.item, className)} variant="secondary" fill="text" onClick={onClick}>
{children}
</Button>
);
} else { } else {
return ( return (
<span ref={ref} className={cx(styles.item, className)} {...rest}> <span ref={ref} className={cx(styles.item, className)} {...rest}>

View File

@@ -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}</>;
}

View File

@@ -23,7 +23,7 @@ export function PanelHeaderMenuProvider({ panel, dashboard, loadingState, childr
const angularComponent = useSelector((state) => getPanelStateForModel(state, panel)?.angularComponent); const angularComponent = useSelector((state) => getPanelStateForModel(state, panel)?.angularComponent);
useEffect(() => { useEffect(() => {
setItems(getPanelMenu(dashboard, panel, loadingState, angularComponent)); setItems(getPanelMenu(dashboard, panel, angularComponent));
}, [dashboard, panel, angularComponent, loadingState, setItems]); }, [dashboard, panel, angularComponent, loadingState, setItems]);
return children({ items }); return children({ items });

View File

@@ -641,6 +641,11 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
reportInteraction('dashboards_panelheader_statusmessage_clicked'); 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;
@@ -705,6 +710,7 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
hoverHeaderOffset={hoverHeaderOffset} hoverHeaderOffset={hoverHeaderOffset}
hoverHeader={this.hasOverlayHeader()} hoverHeader={this.hasOverlayHeader()}
displayMode={transparent ? 'transparent' : 'default'} displayMode={transparent ? 'transparent' : 'default'}
onCancelQuery={this.onCancelQuery}
> >
{(innerWidth, innerHeight) => ( {(innerWidth, innerHeight) => (
<> <>

View File

@@ -10,7 +10,6 @@ import {
PluginExtensionRegistryItem, PluginExtensionRegistryItem,
setPluginsExtensionRegistry, setPluginsExtensionRegistry,
} from '@grafana/runtime'; } from '@grafana/runtime';
import { LoadingState } from '@grafana/schema';
import config from 'app/core/config'; import config from 'app/core/config';
import * as actions from 'app/features/explore/state/main'; import * as actions from 'app/features/explore/state/main';
import { setStore } from 'app/store/store'; 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', () => { describe('when extending panel menu from plugins', () => {
it('should contain menu item from link extension', () => { it('should contain menu item from link extension', () => {
setPluginsExtensionRegistry({ setPluginsExtensionRegistry({
@@ -156,7 +125,7 @@ describe('getPanelMenu()', () => {
const panel = new PanelModel({}); const panel = new PanelModel({});
const dashboard = createDashboardModelFixture({}); const dashboard = createDashboardModelFixture({});
const menuItems = getPanelMenu(dashboard, panel, LoadingState.Loading); const menuItems = getPanelMenu(dashboard, panel);
const moreSubMenu = menuItems.find((i) => i.text === 'More...')?.subMenu; const moreSubMenu = menuItems.find((i) => i.text === 'More...')?.subMenu;
expect(moreSubMenu).toEqual( expect(moreSubMenu).toEqual(
@@ -184,7 +153,7 @@ describe('getPanelMenu()', () => {
const panel = new PanelModel({}); const panel = new PanelModel({});
const dashboard = createDashboardModelFixture({}); const dashboard = createDashboardModelFixture({});
const menuItems = getPanelMenu(dashboard, panel, LoadingState.Loading); const menuItems = getPanelMenu(dashboard, panel);
const moreSubMenu = menuItems.find((i) => i.text === 'More...')?.subMenu; const moreSubMenu = menuItems.find((i) => i.text === 'More...')?.subMenu;
expect(moreSubMenu).toEqual( expect(moreSubMenu).toEqual(
@@ -223,7 +192,7 @@ describe('getPanelMenu()', () => {
const panel = new PanelModel({}); const panel = new PanelModel({});
const dashboard = createDashboardModelFixture({}); const dashboard = createDashboardModelFixture({});
const menuItems = getPanelMenu(dashboard, panel, LoadingState.Loading); const menuItems = getPanelMenu(dashboard, panel);
const moreSubMenu = menuItems.find((i) => i.text === 'More...')?.subMenu; const moreSubMenu = menuItems.find((i) => i.text === 'More...')?.subMenu;
expect(moreSubMenu).toEqual( expect(moreSubMenu).toEqual(
@@ -254,7 +223,7 @@ describe('getPanelMenu()', () => {
const panel = new PanelModel({}); const panel = new PanelModel({});
const dashboard = createDashboardModelFixture({}); const dashboard = createDashboardModelFixture({});
const menuItems = getPanelMenu(dashboard, panel, LoadingState.Loading); const menuItems = getPanelMenu(dashboard, panel);
const moreSubMenu = menuItems.find((i) => i.text === 'More...')?.subMenu; const moreSubMenu = menuItems.find((i) => i.text === 'More...')?.subMenu;
expect(moreSubMenu).toEqual( expect(moreSubMenu).toEqual(
@@ -310,7 +279,7 @@ describe('getPanelMenu()', () => {
title: 'My dashboard', title: 'My dashboard',
}); });
getPanelMenu(dashboard, panel, LoadingState.Loading); getPanelMenu(dashboard, panel);
const context: PluginExtensionPanelContext = { const context: PluginExtensionPanelContext = {
pluginId: 'timeseries', pluginId: 'timeseries',
@@ -392,7 +361,7 @@ describe('getPanelMenu()', () => {
title: 'My dashboard', 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 panel = new PanelModel({ isViewing: true });
const dashboard = createDashboardModelFixture({}); const dashboard = createDashboardModelFixture({});
const menuItems = getPanelMenu(dashboard, panel, undefined, angularComponent); const menuItems = getPanelMenu(dashboard, panel, angularComponent);
expect(menuItems).toMatchInlineSnapshot(` expect(menuItems).toMatchInlineSnapshot(`
[ [
{ {

View File

@@ -12,7 +12,6 @@ import {
reportInteraction, reportInteraction,
PluginExtensionPanelContext, PluginExtensionPanelContext,
} from '@grafana/runtime'; } from '@grafana/runtime';
import { LoadingState } from '@grafana/schema';
import { PanelCtrl } from 'app/angular/panel/panel_ctrl'; import { PanelCtrl } from 'app/angular/panel/panel_ctrl';
import config from 'app/core/config'; import config from 'app/core/config';
import { t } from 'app/core/internationalization'; import { t } from 'app/core/internationalization';
@@ -39,7 +38,6 @@ import { getTimeSrv } from '../services/TimeSrv';
export function getPanelMenu( export function getPanelMenu(
dashboard: DashboardModel, dashboard: DashboardModel,
panel: PanelModel, panel: PanelModel,
loadingState?: LoadingState,
angularComponent?: AngularComponent | null angularComponent?: AngularComponent | null
): PanelMenuItem[] { ): PanelMenuItem[] {
const onViewPanel = (event: React.MouseEvent<any>) => { const onViewPanel = (event: React.MouseEvent<any>) => {
@@ -120,12 +118,6 @@ export function getPanelMenu(
reportInteraction('dashboards_panelheader_togglelegend_clicked'); reportInteraction('dashboards_panelheader_togglelegend_clicked');
}; };
const onCancelStreaming = (event: React.MouseEvent) => {
event.preventDefault();
panel.getQueryRunner().cancelQuery();
reportInteraction('dashboards_panelheader_cancelstreaming_clicked');
};
const menu: PanelMenuItem[] = []; const menu: PanelMenuItem[] = [];
if (!panel.isEditing) { 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({ menu.push({
text: t('panel.header-menu.share', `Share`), text: t('panel.header-menu.share', `Share`),
iconClassName: 'share-alt', iconClassName: 'share-alt',

View File

@@ -317,8 +317,11 @@ export class PanelQueryRunner {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
// If we have an old result with loading state, send it with done state // If we have an old result with loading or streaming state, send it with done state
if (this.lastResult && this.lastResult.state === LoadingState.Loading) { if (
this.lastResult &&
(this.lastResult.state === LoadingState.Loading || this.lastResult.state === LoadingState.Streaming)
) {
this.subject.next({ this.subject.next({
...this.lastResult, ...this.lastResult,
state: LoadingState.Done, state: LoadingState.Done,