Explore: Fix URL sync with async queries import (#79584)

* cancel pending queries

* Update public/app/features/explore/hooks/useStateSync/index.ts

Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>

* Update public/app/features/explore/hooks/useStateSync/index.ts

* Fix test

---------

Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>
This commit is contained in:
Giordano Ricci 2024-01-04 09:50:27 +00:00 committed by GitHub
parent 5ae3249c36
commit 2d18961768
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 58 additions and 34 deletions

View File

@ -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 {

View File

@ -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 } }));
}
}
};

View File

@ -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' });
});
});
});

View File

@ -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);

View File

@ -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]);
}

View File

@ -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<UpdateDatasourceInsta
/**
* Loads a new datasource identified by the given name.
*/
export function changeDatasource(
exploreId: string,
datasource: string | DataSourceRef,
options?: { importQueries: boolean }
): ThunkResult<Promise<void>> {
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