Explore: Avoid changing queries twice when importing a query in mixed mode (#63804)

* Explore: avoid changing queries twice when importing a query in mixed mode

* move logic to thunk

* add tests

* improve tests
This commit is contained in:
Giordano Ricci 2023-03-06 10:40:39 +00:00 committed by GitHub
parent 0bdb105df2
commit 24a4a24a89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 173 additions and 25 deletions

View File

@ -2,7 +2,7 @@ import { createSelector } from '@reduxjs/toolkit';
import React, { useCallback, useMemo } from 'react';
import { CoreApp } from '@grafana/data';
import { getDataSourceSrv, reportInteraction } from '@grafana/runtime';
import { reportInteraction } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { getNextRefIdChar } from 'app/core/utils/query';
import { useDispatch, useSelector } from 'app/types';
@ -11,7 +11,7 @@ import { ExploreId } from 'app/types/explore';
import { getDatasourceSrv } from '../plugins/datasource_srv';
import { QueryEditorRows } from '../query/components/QueryEditorRows';
import { runQueries, changeQueriesAction, importQueries } from './state/query';
import { runQueries, changeQueries } from './state/query';
import { getExploreItemSelector } from './state/selectors';
interface Props {
@ -50,25 +50,10 @@ export const QueryRows = ({ exploreId }: Props) => {
}, [dispatch, exploreId]);
const onChange = useCallback(
async (newQueries: DataQuery[]) => {
dispatch(changeQueriesAction({ queries: newQueries, exploreId }));
for (const newQuery of newQueries) {
for (const oldQuery of queries) {
if (newQuery.refId === oldQuery.refId && newQuery.datasource?.type !== oldQuery.datasource?.type) {
const queryDatasource = await getDataSourceSrv().get(newQuery.datasource);
const targetDS = await getDataSourceSrv().get({ uid: newQuery.datasource?.uid });
dispatch(importQueries(exploreId, queries, queryDatasource, targetDS, newQuery.refId));
}
}
}
// if we are removing a query we want to run the remaining ones
if (newQueries.length < queries.length) {
onRunQueries();
}
(newQueries: DataQuery[]) => {
dispatch(changeQueries({ exploreId, queries: newQueries }));
},
[dispatch, exploreId, onRunQueries, queries]
[dispatch, exploreId]
);
const onAddQuery = useCallback(

View File

@ -16,7 +16,7 @@ import {
SupplementaryQueryType,
} from '@grafana/data';
import { config } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { DataQuery, DataSourceRef } from '@grafana/schema';
import { ExploreId, ExploreItemState, StoreState, ThunkDispatch } from 'app/types';
import { reducerTester } from '../../../../test/core/redux/reducerTester';
@ -41,7 +41,9 @@ import {
setSupplementaryQueryEnabled,
addQueryRow,
cleanSupplementaryQueryDataProviderAction,
changeQueries,
} from './query';
import * as actions from './query';
import { makeExplorePaneState } from './utils';
const { testRange, defaultInitialState } = createDefaultInitialState();
@ -58,10 +60,10 @@ const datasources: DataSourceApi[] = [
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>,
{
name: 'testDs2',
type: 'postgres',
type: 'mysql',
uid: 'ds2',
getRef: () => {
return { type: 'postgres', uid: 'ds2' };
return { type: 'mysql', uid: 'ds2' };
},
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>,
];
@ -81,7 +83,15 @@ jest.mock('@grafana/runtime', () => ({
}),
getDataSourceSrv: () => {
return {
get: (uid?: string) => datasources.find((ds) => ds.uid === uid) || datasources[0],
get: (ref?: DataSourceRef | string) => {
if (!ref) {
return datasources[0];
}
return (
datasources.find((ds) => (typeof ref === 'string' ? ds.uid === ref : ds.uid === ref.uid)) || datasources[0]
);
},
};
},
config: {
@ -228,6 +238,130 @@ describe('running queries', () => {
});
});
describe('changeQueries', () => {
// Due to how spyOn works (it removes `type`, `match` and `toString` from the spied function, on which we rely on in the reducer),
// we are repeating the following tests twice, once to chck the resulting state and once to check that the correct actions are dispatched.
describe('calls the correct actions', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('should import queries when datasource is changed', async () => {
jest.spyOn(actions, 'importQueries');
jest.spyOn(actions, 'changeQueriesAction');
const originalQueries = [{ refId: 'A', datasource: datasources[0].getRef() }];
const { dispatch } = configureStore({
...defaultInitialState,
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
datasourceInstance: datasources[0],
queries: originalQueries,
},
},
} as unknown as Partial<StoreState>);
await dispatch(
changeQueries({
queries: [{ refId: 'A', datasource: datasources[1].getRef() }],
exploreId: ExploreId.left,
})
);
expect(actions.changeQueriesAction).not.toHaveBeenCalled();
expect(actions.importQueries).toHaveBeenCalledWith(
ExploreId.left,
originalQueries,
datasources[0],
datasources[1],
originalQueries[0].refId
);
});
it('should not import queries when datasource is not changed', async () => {
jest.spyOn(actions, 'importQueries');
jest.spyOn(actions, 'changeQueriesAction');
const { dispatch } = configureStore({
...defaultInitialState,
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
datasourceInstance: datasources[0],
queries: [{ refId: 'A', datasource: datasources[0].getRef() }],
},
},
} as unknown as Partial<StoreState>);
await dispatch(
changeQueries({
queries: [{ refId: 'A', datasource: datasources[0].getRef(), queryType: 'someValue' }],
exploreId: ExploreId.left,
})
);
expect(actions.changeQueriesAction).toHaveBeenCalled();
expect(actions.importQueries).not.toHaveBeenCalled();
});
});
describe('correctly modifies the state', () => {
it('should import queries when datasource is changed', async () => {
const originalQueries = [{ refId: 'A', datasource: datasources[0].getRef() }];
const { dispatch, getState } = configureStore({
...defaultInitialState,
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
datasourceInstance: datasources[0],
queries: originalQueries,
},
},
} as unknown as Partial<StoreState>);
await dispatch(
changeQueries({
queries: [{ refId: 'A', datasource: datasources[1].getRef() }],
exploreId: ExploreId.left,
})
);
expect(getState().explore[ExploreId.left].queries[0]).toHaveProperty('refId', 'A');
expect(getState().explore[ExploreId.left].queries[0]).toHaveProperty('datasource', datasources[1].getRef());
});
it('should not import queries when datasource is not changed', async () => {
const { dispatch, getState } = configureStore({
...defaultInitialState,
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
datasourceInstance: datasources[0],
queries: [{ refId: 'A', datasource: datasources[0].getRef() }],
},
},
} as unknown as Partial<StoreState>);
await dispatch(
changeQueries({
queries: [{ refId: 'A', datasource: datasources[0].getRef(), queryType: 'someValue' }],
exploreId: ExploreId.left,
})
);
expect(getState().explore[ExploreId.left].queries[0]).toHaveProperty('refId', 'A');
expect(getState().explore[ExploreId.left].queries[0]).toHaveProperty('datasource', datasources[0].getRef());
expect(getState().explore[ExploreId.left].queries[0]).toEqual({
refId: 'A',
datasource: datasources[0].getRef(),
queryType: 'someValue',
});
});
});
});
describe('importing queries', () => {
describe('when importing queries between the same type of data source', () => {
it('remove datasource property from all of the queries', async () => {

View File

@ -36,7 +36,7 @@ import { CorrelationData } from 'app/features/correlations/useCorrelations';
import { getTimeZone } from 'app/features/profile/state/selectors';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
import { store } from 'app/store/store';
import { ExploreItemState, ExplorePanelData, ThunkDispatch, ThunkResult } from 'app/types';
import { createAsyncThunk, ExploreItemState, ExplorePanelData, ThunkDispatch, ThunkResult } from 'app/types';
import { ExploreId, ExploreState, QueryOptions, SupplementaryQueries } from 'app/types/explore';
import { notifyApp } from '../../../core/actions';
@ -291,6 +291,35 @@ const getImportableQueries = async (
return addDatasourceToQueries(targetDataSource, queriesOut);
};
export const changeQueries = createAsyncThunk<void, ChangeQueriesPayload>(
'explore/changeQueries',
async ({ queries, exploreId }, { getState, dispatch }) => {
let queriesImported = false;
const oldQueries = getState().explore[exploreId]!.queries;
for (const newQuery of queries) {
for (const oldQuery of oldQueries) {
if (newQuery.refId === oldQuery.refId && newQuery.datasource?.type !== oldQuery.datasource?.type) {
const queryDatasource = await getDataSourceSrv().get(oldQuery.datasource);
const targetDS = await getDataSourceSrv().get({ uid: newQuery.datasource?.uid });
await dispatch(importQueries(exploreId, oldQueries, queryDatasource, targetDS, newQuery.refId));
queriesImported = true;
}
}
}
// Importing queries changes the same state, therefore if we are importing queries we don't want to change the state again
if (!queriesImported) {
dispatch(changeQueriesAction({ queries, exploreId }));
}
// if we are removing a query we want to run the remaining ones
if (queries.length < queries.length) {
dispatch(runQueries(exploreId));
}
}
);
/**
* Import queries from previous datasource if possible eg Loki and Prometheus have similar query language so the
* labels part can be reused to get similar data.