diff --git a/public/app/features/explore/CorrelationEditorModeBar.tsx b/public/app/features/explore/CorrelationEditorModeBar.tsx index c714b951c07..5df1b7ef04c 100644 --- a/public/app/features/explore/CorrelationEditorModeBar.tsx +++ b/public/app/features/explore/CorrelationEditorModeBar.tsx @@ -57,7 +57,9 @@ export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, Expl ) { const { exploreId, changeDatasourceUid } = correlationDetails?.postConfirmAction; if (exploreId && changeDatasourceUid) { - dispatch(changeDatasource(exploreId, changeDatasourceUid, { importQueries: true })); + dispatch( + changeDatasource({ exploreId, datasource: changeDatasourceUid, options: { importQueries: true } }) + ); dispatch( changeCorrelationEditorDetails({ isExiting: false, @@ -143,7 +145,7 @@ export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, Expl const changeDatasourcePostAction = (exploreId: string, datasourceUid: string) => { setSaveMessage(undefined); - dispatch(changeDatasource(exploreId, datasourceUid, { importQueries: true })); + dispatch(changeDatasource({ exploreId, datasource: datasourceUid, options: { importQueries: true } })); }; const saveCorrelationPostAction = (skipPostConfirmAction: boolean) => { @@ -163,7 +165,7 @@ export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, Expl action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CHANGE_DATASOURCE && changeDatasourceUid !== undefined ) { - changeDatasource(exploreId, changeDatasourceUid); + changeDatasource({ exploreId, datasource: changeDatasourceUid }); resetEditor(); } } else { diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 0cbb25e78aa..360024a0e4c 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -100,7 +100,7 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle const onChangeDatasource = async (dsSettings: DataSourceInstanceSettings) => { if (!isCorrelationsEditorMode) { - dispatch(changeDatasource(exploreId, dsSettings.uid, { importQueries: true })); + dispatch(changeDatasource({ exploreId, datasource: dsSettings.uid, options: { importQueries: true } })); } else { if (correlationDetails?.correlationDirty || correlationDetails?.queryEditorDirty) { // prompt will handle datasource change if needed @@ -128,7 +128,7 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle }); } - dispatch(changeDatasource(exploreId, dsSettings.uid, { importQueries: true })); + dispatch(changeDatasource({ exploreId, datasource: dsSettings.uid, options: { importQueries: true } })); } } }; diff --git a/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx b/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx index 048444080e7..38650bb3f2c 100644 --- a/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx @@ -374,7 +374,7 @@ describe('RichHistoryCard', () => { await waitFor(() => { expect(setQueries).toHaveBeenCalledWith(expect.any(String), queries); - expect(changeDatasource).toHaveBeenCalledWith(expect.any(String), 'mixed'); + expect(changeDatasource).toHaveBeenCalledWith({ datasource: 'mixed', exploreId: 'left' }); }); }); }); diff --git a/public/app/features/explore/RichHistory/RichHistoryCard.tsx b/public/app/features/explore/RichHistory/RichHistoryCard.tsx index 43b70b5f22c..5823340f9d0 100644 --- a/public/app/features/explore/RichHistory/RichHistoryCard.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryCard.tsx @@ -188,7 +188,7 @@ export function RichHistoryCard(props: Props) { const queriesToRun = queryHistoryItem.queries; const differentDataSource = queryHistoryItem.datasourceUid !== datasourceInstance?.uid; if (differentDataSource) { - await changeDatasource(exploreId, queryHistoryItem.datasourceUid); + await changeDatasource({ exploreId, datasource: queryHistoryItem.datasourceUid }); } setQueries(exploreId, queriesToRun); diff --git a/public/app/features/explore/hooks/useStateSync/index.ts b/public/app/features/explore/hooks/useStateSync/index.ts index b2933697bcc..e40b57d55f5 100644 --- a/public/app/features/explore/hooks/useStateSync/index.ts +++ b/public/app/features/explore/hooks/useStateSync/index.ts @@ -13,7 +13,7 @@ import { addListener, ExploreItemState, ExploreQueryParams, useDispatch, useSele import { changeDatasource } from '../../state/datasource'; import { changePanelsStateAction, initializeExplore } from '../../state/explorePane'; import { clearPanes, splitClose, splitOpen, syncTimesAction } from '../../state/main'; -import { runQueries, setQueriesAction } from '../../state/query'; +import { cancelQueries, runQueries, setQueriesAction } from '../../state/query'; import { selectPanes } from '../../state/selectors'; import { changeRangeAction, updateTime } from '../../state/time'; import { DEFAULT_RANGE, fromURLRange } from '../../state/utils'; @@ -32,6 +32,7 @@ export function useStateSync(params: ExploreQueryParams) { const orgId = useSelector((state) => state.user.orgId); const prevParams = useRef(params); const initState = useRef<'notstarted' | 'pending' | 'done'>('notstarted'); + const paused = useRef(false); const { warning } = useAppNotification(); useEffect(() => { @@ -47,20 +48,35 @@ export function useStateSync(params: ExploreQueryParams) { const unsubscribe = dispatch( addListener({ predicate: (action) => - // We want to update the URL when: - // - a pane is opened or closed - // - a query is run - // - range is changed - // - panel state is updated - [ - splitClose.type, - splitOpen.fulfilled.type, - runQueries.pending.type, - changeRangeAction.type, - changePanelsStateAction.type, - ].includes(action.type), + /* + We want to update the URL when: + - a pane is opened or closed + - a query is run + - range is changed + - panel state is updated + - a datasource change has completed. + + Note: Changing datasource causes a bunch of actions to be dispatched, we want to update the URL + only when the change set has completed. This is done by checking if the changeDatasource.pending action + has been dispatched and pausing the listener until the changeDatasource.fulfilled action is dispatched. + */ + + { + paused.current = changeDatasource.pending.type === action.type; + + return ( + [ + splitClose.type, + splitOpen.fulfilled.type, + runQueries.pending.type, + changeRangeAction.type, + changePanelsStateAction.type, + changeDatasource.fulfilled.type, + ].includes(action.type) && !paused.current + ); + }, effect: async (_, { cancelActiveListeners, delay, getState }) => { - // The following 2 lines will throttle updates to avoid creating history entries when rapid changes + // The following 2 lines will debounce updates to avoid creating history entries when rapid changes // are committed to the store. cancelActiveListeners(); await delay(200); @@ -118,7 +134,7 @@ export function useStateSync(params: ExploreQueryParams) { dispatch(syncTimesAction({ syncedTimes: !paneTimesUnequal })); // if all time ranges are equal, keep them synced } - Object.entries(urlState.panes).forEach(([exploreId, urlPane], i) => { + Object.entries(urlState.panes).forEach(async ([exploreId, urlPane], i) => { const { datasource, queries, range, panelsState } = urlPane; const paneState = panesState[exploreId]; @@ -129,11 +145,11 @@ export function useStateSync(params: ExploreQueryParams) { Promise.resolve() .then(async () => { if (update.datasource && datasource) { - await dispatch(changeDatasource(exploreId, datasource)); + await dispatch(changeDatasource({ exploreId, datasource })); } return; }) - .then(() => { + .then(async () => { if (update.range) { dispatch(updateTime({ exploreId, rawRange: fromURLRange(range) })); } @@ -143,6 +159,7 @@ export function useStateSync(params: ExploreQueryParams) { } if (update.queries || update.range) { + await dispatch(cancelQueries(exploreId)); dispatch(runQueries({ exploreId })); } @@ -281,7 +298,9 @@ export function useStateSync(params: ExploreQueryParams) { prevParams.current = params; - isURLOutOfSync && initState.current === 'done' && sync(); + if (isURLOutOfSync && initState.current === 'done') { + sync(); + } }, [dispatch, panesState, orgId, location, params, warning]); } diff --git a/public/app/features/explore/state/datasource.ts b/public/app/features/explore/state/datasource.ts index f2585d4609e..762aebe3ee8 100644 --- a/public/app/features/explore/state/datasource.ts +++ b/public/app/features/explore/state/datasource.ts @@ -7,7 +7,7 @@ import { DataSourceRef } from '@grafana/schema'; import { RefreshPicker } from '@grafana/ui'; import { stopQueryState } from 'app/core/utils/explore'; import { getCorrelationsBySourceUIDs } from 'app/features/correlations/utils'; -import { ExploreItemState, ThunkResult } from 'app/types'; +import { ExploreItemState, createAsyncThunk } from 'app/types'; import { loadSupplementaryQueries } from '../utils/supplementaryQueries'; @@ -39,12 +39,15 @@ export const updateDatasourceInstanceAction = createAction> { - return async (dispatch, getState) => { + +interface ChangeDatasourcePayload { + exploreId: string; + datasource: string | DataSourceRef; + options?: { importQueries: boolean }; +} +export const changeDatasource = createAsyncThunk( + 'explore/changeDatasource', + async ({ datasource, exploreId, options }: ChangeDatasourcePayload, { getState, dispatch }) => { const orgId = getState().user.orgId; const { history, instance } = await loadAndInitDatasource(orgId, datasource); const currentDataSourceInstance = getState().explore.panes[exploreId]!.datasourceInstance; @@ -80,8 +83,8 @@ export function changeDatasource( if (options?.importQueries) { dispatch(runQueries({ exploreId })); } - }; -} + } +); // // Reducer