mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Panel Header: Add CancelQuery option to panel header (#64796)
Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com>
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
23
packages/grafana-ui/src/utils/DelayRender.tsx
Normal file
23
packages/grafana-ui/src/utils/DelayRender.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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) => (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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(`
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user