From 40e875369b195cad41d0463f7daada72c89addb8 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Wed, 25 Mar 2020 10:38:14 +0000 Subject: [PATCH] Explore: Allows a user to cancel a running query (#22545) --- .../app/features/explore/ExploreToolbar.tsx | 11 +++++- public/app/features/explore/RunButton.tsx | 17 +++++--- .../app/features/explore/state/actionTypes.ts | 5 +++ .../features/explore/state/actions.test.ts | 39 ++++++++++++++++++- public/app/features/explore/state/actions.ts | 14 +++++++ public/app/features/explore/state/reducers.ts | 9 +++++ public/sass/components/_navbar.scss | 5 +++ 7 files changed, 92 insertions(+), 8 deletions(-) diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 7004ae09464..6fa197b5f08 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -13,6 +13,7 @@ import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { StoreState } from 'app/types/store'; import { changeDatasource, + cancelQueries, clearQueries, splitClose, runQueries, @@ -72,6 +73,7 @@ interface StateProps { interface DispatchProps { changeDatasource: typeof changeDatasource; clearAll: typeof clearQueries; + cancelQueries: typeof cancelQueries; runQueries: typeof runQueries; closeSplit: typeof splitClose; split: typeof splitOpen; @@ -93,8 +95,12 @@ export class UnConnectedExploreToolbar extends PureComponent { this.props.clearAll(this.props.exploreId); }; - onRunQuery = () => { - return this.props.runQueries(this.props.exploreId); + onRunQuery = (loading = false) => { + if (loading) { + return this.props.cancelQueries(this.props.exploreId); + } else { + return this.props.runQueries(this.props.exploreId); + } }; onChangeRefreshInterval = (item: string) => { @@ -388,6 +394,7 @@ const mapDispatchToProps: DispatchProps = { updateLocation, changeRefreshInterval, clearAll: clearQueries, + cancelQueries, runQueries, closeSplit: splitClose, split: splitOpen, diff --git a/public/app/features/explore/RunButton.tsx b/public/app/features/explore/RunButton.tsx index e701150cdc9..681f9735fc4 100644 --- a/public/app/features/explore/RunButton.tsx +++ b/public/app/features/explore/RunButton.tsx @@ -20,7 +20,7 @@ const getStyles = memoizeOne(() => { type Props = { splitted: boolean; loading: boolean; - onRun: () => void; + onRun: (loading: boolean) => void; refreshInterval?: string; onChangeRefreshInterval: (interval: string) => void; showDropdown: boolean; @@ -29,12 +29,17 @@ type Props = { export function RunButton(props: Props) { const { splitted, loading, onRun, onChangeRefreshInterval, refreshInterval, showDropdown } = props; const styles = getStyles(); + const runButton = ( onRun(loading)} + 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'} /> ); @@ -44,7 +49,9 @@ export function RunButton(props: Props) { ); diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index 4d7b57c58d5..a9dbdf36670 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/public/app/features/explore/state/actionTypes.ts @@ -217,6 +217,11 @@ export const changeRefreshIntervalAction = createAction('explore/clearQueries'); +/** + * Cancel running queries. + */ +export const cancelQueriesAction = createAction('explore/cancelQueries'); + /** * Highlight expressions in the log results */ diff --git a/public/app/features/explore/state/actions.test.ts b/public/app/features/explore/state/actions.test.ts index a0c2d412e93..87a25dad99a 100644 --- a/public/app/features/explore/state/actions.test.ts +++ b/public/app/features/explore/state/actions.test.ts @@ -2,7 +2,7 @@ import { PayloadAction } from '@reduxjs/toolkit'; import { DataQuery, DefaultTimeZone, ExploreMode, LogsDedupStrategy, RawTimeRange, toUtc } from '@grafana/data'; 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 { thunkTester } from 'test/core/thunk/thunkTester'; import { @@ -13,6 +13,8 @@ import { setQueriesAction, updateDatasourceInstanceAction, updateUIStateAction, + cancelQueriesAction, + scanStopAction, } from './actionTypes'; import { Emitter } from 'app/core/core'; import { makeInitialUpdateState } from './reducers'; @@ -20,6 +22,7 @@ import { PanelModel } from 'app/features/dashboard/state'; import { updateLocation } from '../../../core/actions'; import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv'; import * as DatasourceSrv from 'app/features/plugins/datasource_srv'; +import { interval } from 'rxjs'; jest.mock('app/features/plugins/datasource_srv'); const getDatasourceSrvMock = (DatasourceSrv.getDatasourceSrv as any) as jest.Mock; @@ -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', () => { it('should switch to logs mode when changing from prometheus to loki', async () => { const lokiMock = { diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index dea738b9685..a60714ed2cd 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -85,6 +85,8 @@ import { ToggleTablePayload, updateDatasourceInstanceAction, updateUIStateAction, + changeLoadingStateAction, + cancelQueriesAction, } from './actionTypes'; import { getTimeZone } from 'app/features/profile/state/selectors'; import { getShiftedTimeRange } from 'app/core/utils/timePicker'; @@ -242,6 +244,17 @@ export function clearQueries(exploreId: ExploreId): ThunkResult { }; } +/** + * Cancel running queries + */ +export function cancelQueries(exploreId: ExploreId): ThunkResult { + return dispatch => { + dispatch(scanStopAction({ exploreId })); + dispatch(cancelQueriesAction({ exploreId })); + dispatch(stateSave()); + }; +} + /** * Loads all explore data sources and sets the chosen datasource. * If there are no datasources a missing datasource action is dispatched. @@ -460,6 +473,7 @@ export const runQueries = (exploreId: ExploreId): ThunkResult => { const transaction = buildQueryTransaction(queries, queryOptions, range, scanning); let firstResponse = true; + dispatch(changeLoadingStateAction({ exploreId, loadingState: LoadingState.Loading })); const newQuerySub = runRequest(datasourceInstance, transaction.request) .pipe( diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index bda74b8869f..7def8fc3780 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -65,6 +65,7 @@ import { toggleTableAction, updateDatasourceInstanceAction, updateUIStateAction, + cancelQueriesAction, } from './actionTypes'; import { ResultProcessor } from '../utils/ResultProcessor'; import { updateLocation } from '../../../core/actions'; @@ -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)) { const { expressions } = action.payload; return { ...state, logsHighlighterExpressions: expressions }; diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index 67c9a06d442..6402328b8a5 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -155,6 +155,7 @@ i.navbar-page-btn__search { .gicon { filter: $navbar-btn-gicon-brightness; } + &:hover { .gicon { filter: brightness(0.8); @@ -180,6 +181,10 @@ i.navbar-page-btn__search { } } + &--danger { + @include buttonBackground($red-base, $red-shade); + } + @include media-breakpoint-down(lg) { .btn-title { margin-left: $space-xs;