mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Allows a user to cancel a running query (#22545)
This commit is contained in:
@@ -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 = () => {
|
||||
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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -217,6 +217,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
|
||||
*/
|
||||
|
||||
@@ -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';
|
||||
@@ -242,6 +244,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;
|
||||
|
||||
Reference in New Issue
Block a user