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';
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<Props> {
this.props.clearAll(this.props.exploreId);
};
onRunQuery = () => {
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,
......
......@@ -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 = (
<ResponsiveButton
splitted={splitted}
title="Run Query"
onClick={onRun}
buttonClassName={classNames('navbar-button--secondary', { 'btn--radius-right-0': showDropdown })}
title={loading ? 'Cancel' : 'Run Query'}
onClick={() => 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) {
<RefreshPicker
onIntervalChanged={onChangeRefreshInterval}
value={refreshInterval}
buttonSelectClassName={`navbar-button--secondary ${styles.selectButtonOverride}`}
buttonSelectClassName={`${loading ? 'navbar-button--danger' : 'navbar-button--secondary'} ${
styles.selectButtonOverride
}`}
refreshButton={runButton}
/>
);
......
......@@ -218,6 +218,11 @@ export const changeRefreshIntervalAction = createAction<ChangeRefreshIntervalPay
export const clearQueriesAction = createAction<ClearQueriesPayload>('explore/clearQueries');
/**
* Cancel running queries.
*/
export const cancelQueriesAction = createAction<ClearQueriesPayload>('explore/cancelQueries');
/**
* Highlight expressions in the log results
*/
export const highlightLogsExpressionAction = createAction<HighlightLogsExpressionPayload>(
......
......@@ -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<DatasourceSrv.DatasourceSrv>;
......@@ -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 = {
......
......@@ -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';
......@@ -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.
* If there are no datasources a missing datasource action is dispatched.
*/
......@@ -460,6 +473,7 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
const transaction = buildQueryTransaction(queries, queryOptions, range, scanning);
let firstResponse = true;
dispatch(changeLoadingStateAction({ exploreId, loadingState: LoadingState.Loading }));
const newQuerySub = runRequest(datasourceInstance, transaction.request)
.pipe(
......
......@@ -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 };
......
......@@ -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;
......
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