Commit 40e87536 by kay delaney Committed by GitHub

Explore: Allows a user to cancel a running query (#22545)

parent 910f65d2
...@@ -13,6 +13,7 @@ import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; ...@@ -13,6 +13,7 @@ import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { StoreState } from 'app/types/store'; import { StoreState } from 'app/types/store';
import { import {
changeDatasource, changeDatasource,
cancelQueries,
clearQueries, clearQueries,
splitClose, splitClose,
runQueries, runQueries,
...@@ -72,6 +73,7 @@ interface StateProps { ...@@ -72,6 +73,7 @@ interface StateProps {
interface DispatchProps { interface DispatchProps {
changeDatasource: typeof changeDatasource; changeDatasource: typeof changeDatasource;
clearAll: typeof clearQueries; clearAll: typeof clearQueries;
cancelQueries: typeof cancelQueries;
runQueries: typeof runQueries; runQueries: typeof runQueries;
closeSplit: typeof splitClose; closeSplit: typeof splitClose;
split: typeof splitOpen; split: typeof splitOpen;
...@@ -93,8 +95,12 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> { ...@@ -93,8 +95,12 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
this.props.clearAll(this.props.exploreId); this.props.clearAll(this.props.exploreId);
}; };
onRunQuery = () => { onRunQuery = (loading = false) => {
return this.props.runQueries(this.props.exploreId); if (loading) {
return this.props.cancelQueries(this.props.exploreId);
} else {
return this.props.runQueries(this.props.exploreId);
}
}; };
onChangeRefreshInterval = (item: string) => { onChangeRefreshInterval = (item: string) => {
...@@ -388,6 +394,7 @@ const mapDispatchToProps: DispatchProps = { ...@@ -388,6 +394,7 @@ const mapDispatchToProps: DispatchProps = {
updateLocation, updateLocation,
changeRefreshInterval, changeRefreshInterval,
clearAll: clearQueries, clearAll: clearQueries,
cancelQueries,
runQueries, runQueries,
closeSplit: splitClose, closeSplit: splitClose,
split: splitOpen, split: splitOpen,
......
...@@ -20,7 +20,7 @@ const getStyles = memoizeOne(() => { ...@@ -20,7 +20,7 @@ const getStyles = memoizeOne(() => {
type Props = { type Props = {
splitted: boolean; splitted: boolean;
loading: boolean; loading: boolean;
onRun: () => void; onRun: (loading: boolean) => void;
refreshInterval?: string; refreshInterval?: string;
onChangeRefreshInterval: (interval: string) => void; onChangeRefreshInterval: (interval: string) => void;
showDropdown: boolean; showDropdown: boolean;
...@@ -29,12 +29,17 @@ type Props = { ...@@ -29,12 +29,17 @@ type Props = {
export function RunButton(props: Props) { export function RunButton(props: Props) {
const { splitted, loading, onRun, onChangeRefreshInterval, refreshInterval, showDropdown } = props; const { splitted, loading, onRun, onChangeRefreshInterval, refreshInterval, showDropdown } = props;
const styles = getStyles(); const styles = getStyles();
const runButton = ( const runButton = (
<ResponsiveButton <ResponsiveButton
splitted={splitted} splitted={splitted}
title="Run Query" title={loading ? 'Cancel' : 'Run Query'}
onClick={onRun} onClick={() => onRun(loading)}
buttonClassName={classNames('navbar-button--secondary', { 'btn--radius-right-0': showDropdown })} buttonClassName={classNames({
'navbar-button--secondary': !loading,
'navbar-button--danger': loading,
'btn--radius-right-0': showDropdown,
})}
iconClassName={loading ? 'fa fa-spinner fa-fw fa-spin run-icon' : 'fa fa-refresh fa-fw'} iconClassName={loading ? 'fa fa-spinner fa-fw fa-spin run-icon' : 'fa fa-refresh fa-fw'}
/> />
); );
...@@ -44,7 +49,9 @@ export function RunButton(props: Props) { ...@@ -44,7 +49,9 @@ export function RunButton(props: Props) {
<RefreshPicker <RefreshPicker
onIntervalChanged={onChangeRefreshInterval} onIntervalChanged={onChangeRefreshInterval}
value={refreshInterval} value={refreshInterval}
buttonSelectClassName={`navbar-button--secondary ${styles.selectButtonOverride}`} buttonSelectClassName={`${loading ? 'navbar-button--danger' : 'navbar-button--secondary'} ${
styles.selectButtonOverride
}`}
refreshButton={runButton} refreshButton={runButton}
/> />
); );
......
...@@ -218,6 +218,11 @@ export const changeRefreshIntervalAction = createAction<ChangeRefreshIntervalPay ...@@ -218,6 +218,11 @@ export const changeRefreshIntervalAction = createAction<ChangeRefreshIntervalPay
export const clearQueriesAction = createAction<ClearQueriesPayload>('explore/clearQueries'); export const clearQueriesAction = createAction<ClearQueriesPayload>('explore/clearQueries');
/** /**
* Cancel running queries.
*/
export const cancelQueriesAction = createAction<ClearQueriesPayload>('explore/cancelQueries');
/**
* Highlight expressions in the log results * Highlight expressions in the log results
*/ */
export const highlightLogsExpressionAction = createAction<HighlightLogsExpressionPayload>( export const highlightLogsExpressionAction = createAction<HighlightLogsExpressionPayload>(
......
...@@ -2,7 +2,7 @@ import { PayloadAction } from '@reduxjs/toolkit'; ...@@ -2,7 +2,7 @@ import { PayloadAction } from '@reduxjs/toolkit';
import { DataQuery, DefaultTimeZone, ExploreMode, LogsDedupStrategy, RawTimeRange, toUtc } from '@grafana/data'; import { DataQuery, DefaultTimeZone, ExploreMode, LogsDedupStrategy, RawTimeRange, toUtc } from '@grafana/data';
import * as Actions from './actions'; import * as Actions from './actions';
import { changeDatasource, loadDatasource, navigateToExplore, refreshExplore } from './actions'; import { changeDatasource, loadDatasource, navigateToExplore, refreshExplore, cancelQueries } from './actions';
import { ExploreId, ExploreUpdateState, ExploreUrlState } from 'app/types'; import { ExploreId, ExploreUpdateState, ExploreUrlState } from 'app/types';
import { thunkTester } from 'test/core/thunk/thunkTester'; import { thunkTester } from 'test/core/thunk/thunkTester';
import { import {
...@@ -13,6 +13,8 @@ import { ...@@ -13,6 +13,8 @@ import {
setQueriesAction, setQueriesAction,
updateDatasourceInstanceAction, updateDatasourceInstanceAction,
updateUIStateAction, updateUIStateAction,
cancelQueriesAction,
scanStopAction,
} from './actionTypes'; } from './actionTypes';
import { Emitter } from 'app/core/core'; import { Emitter } from 'app/core/core';
import { makeInitialUpdateState } from './reducers'; import { makeInitialUpdateState } from './reducers';
...@@ -20,6 +22,7 @@ import { PanelModel } from 'app/features/dashboard/state'; ...@@ -20,6 +22,7 @@ import { PanelModel } from 'app/features/dashboard/state';
import { updateLocation } from '../../../core/actions'; import { updateLocation } from '../../../core/actions';
import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv'; import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv';
import * as DatasourceSrv from 'app/features/plugins/datasource_srv'; import * as DatasourceSrv from 'app/features/plugins/datasource_srv';
import { interval } from 'rxjs';
jest.mock('app/features/plugins/datasource_srv'); jest.mock('app/features/plugins/datasource_srv');
const getDatasourceSrvMock = (DatasourceSrv.getDatasourceSrv as any) as jest.Mock<DatasourceSrv.DatasourceSrv>; const getDatasourceSrvMock = (DatasourceSrv.getDatasourceSrv as any) as jest.Mock<DatasourceSrv.DatasourceSrv>;
...@@ -174,6 +177,40 @@ describe('refreshExplore', () => { ...@@ -174,6 +177,40 @@ describe('refreshExplore', () => {
}); });
}); });
describe('running queries', () => {
it('should cancel running query when cancelQueries is dispatched', async () => {
const unsubscribable = interval(1000);
unsubscribable.subscribe();
const exploreId = ExploreId.left;
const initialState = {
explore: {
[exploreId]: {
datasourceInstance: 'test-datasource',
initialized: true,
loading: true,
querySubscription: unsubscribable,
queries: ['A'],
range: testRange,
},
},
user: {
orgId: 'A',
},
};
const dispatchedActions = await thunkTester(initialState)
.givenThunk(cancelQueries)
.whenThunkIsDispatched(exploreId);
expect(dispatchedActions).toEqual([
scanStopAction({ exploreId }),
cancelQueriesAction({ exploreId }),
expect.anything(),
]);
});
});
describe('changing datasource', () => { describe('changing datasource', () => {
it('should switch to logs mode when changing from prometheus to loki', async () => { it('should switch to logs mode when changing from prometheus to loki', async () => {
const lokiMock = { const lokiMock = {
......
...@@ -85,6 +85,8 @@ import { ...@@ -85,6 +85,8 @@ import {
ToggleTablePayload, ToggleTablePayload,
updateDatasourceInstanceAction, updateDatasourceInstanceAction,
updateUIStateAction, updateUIStateAction,
changeLoadingStateAction,
cancelQueriesAction,
} from './actionTypes'; } from './actionTypes';
import { getTimeZone } from 'app/features/profile/state/selectors'; import { getTimeZone } from 'app/features/profile/state/selectors';
import { getShiftedTimeRange } from 'app/core/utils/timePicker'; import { getShiftedTimeRange } from 'app/core/utils/timePicker';
...@@ -243,6 +245,17 @@ export function clearQueries(exploreId: ExploreId): ThunkResult<void> { ...@@ -243,6 +245,17 @@ export function clearQueries(exploreId: ExploreId): ThunkResult<void> {
} }
/** /**
* Cancel running queries
*/
export function cancelQueries(exploreId: ExploreId): ThunkResult<void> {
return dispatch => {
dispatch(scanStopAction({ exploreId }));
dispatch(cancelQueriesAction({ exploreId }));
dispatch(stateSave());
};
}
/**
* Loads all explore data sources and sets the chosen datasource. * Loads all explore data sources and sets the chosen datasource.
* If there are no datasources a missing datasource action is dispatched. * If there are no datasources a missing datasource action is dispatched.
*/ */
...@@ -460,6 +473,7 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => { ...@@ -460,6 +473,7 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
const transaction = buildQueryTransaction(queries, queryOptions, range, scanning); const transaction = buildQueryTransaction(queries, queryOptions, range, scanning);
let firstResponse = true; let firstResponse = true;
dispatch(changeLoadingStateAction({ exploreId, loadingState: LoadingState.Loading }));
const newQuerySub = runRequest(datasourceInstance, transaction.request) const newQuerySub = runRequest(datasourceInstance, transaction.request)
.pipe( .pipe(
......
...@@ -65,6 +65,7 @@ import { ...@@ -65,6 +65,7 @@ import {
toggleTableAction, toggleTableAction,
updateDatasourceInstanceAction, updateDatasourceInstanceAction,
updateUIStateAction, updateUIStateAction,
cancelQueriesAction,
} from './actionTypes'; } from './actionTypes';
import { ResultProcessor } from '../utils/ResultProcessor'; import { ResultProcessor } from '../utils/ResultProcessor';
import { updateLocation } from '../../../core/actions'; import { updateLocation } from '../../../core/actions';
...@@ -236,6 +237,14 @@ export const itemReducer = (state: ExploreItemState = makeExploreItemState(), ac ...@@ -236,6 +237,14 @@ export const itemReducer = (state: ExploreItemState = makeExploreItemState(), ac
}; };
} }
if (cancelQueriesAction.match(action)) {
stopQueryState(state.querySubscription);
return {
...state,
loading: false,
};
}
if (highlightLogsExpressionAction.match(action)) { if (highlightLogsExpressionAction.match(action)) {
const { expressions } = action.payload; const { expressions } = action.payload;
return { ...state, logsHighlighterExpressions: expressions }; return { ...state, logsHighlighterExpressions: expressions };
......
...@@ -155,6 +155,7 @@ i.navbar-page-btn__search { ...@@ -155,6 +155,7 @@ i.navbar-page-btn__search {
.gicon { .gicon {
filter: $navbar-btn-gicon-brightness; filter: $navbar-btn-gicon-brightness;
} }
&:hover { &:hover {
.gicon { .gicon {
filter: brightness(0.8); filter: brightness(0.8);
...@@ -180,6 +181,10 @@ i.navbar-page-btn__search { ...@@ -180,6 +181,10 @@ i.navbar-page-btn__search {
} }
} }
&--danger {
@include buttonBackground($red-base, $red-shade);
}
@include media-breakpoint-down(lg) { @include media-breakpoint-down(lg) {
.btn-title { .btn-title {
margin-left: $space-xs; margin-left: $space-xs;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment