mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
0bdb105df2
commit
24a4a24a89
@ -2,7 +2,7 @@ import { createSelector } from '@reduxjs/toolkit';
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { CoreApp } from '@grafana/data';
|
import { CoreApp } from '@grafana/data';
|
||||||
import { getDataSourceSrv, reportInteraction } from '@grafana/runtime';
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
import { DataQuery } from '@grafana/schema';
|
import { DataQuery } from '@grafana/schema';
|
||||||
import { getNextRefIdChar } from 'app/core/utils/query';
|
import { getNextRefIdChar } from 'app/core/utils/query';
|
||||||
import { useDispatch, useSelector } from 'app/types';
|
import { useDispatch, useSelector } from 'app/types';
|
||||||
@ -11,7 +11,7 @@ import { ExploreId } from 'app/types/explore';
|
|||||||
import { getDatasourceSrv } from '../plugins/datasource_srv';
|
import { getDatasourceSrv } from '../plugins/datasource_srv';
|
||||||
import { QueryEditorRows } from '../query/components/QueryEditorRows';
|
import { QueryEditorRows } from '../query/components/QueryEditorRows';
|
||||||
|
|
||||||
import { runQueries, changeQueriesAction, importQueries } from './state/query';
|
import { runQueries, changeQueries } from './state/query';
|
||||||
import { getExploreItemSelector } from './state/selectors';
|
import { getExploreItemSelector } from './state/selectors';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -50,25 +50,10 @@ export const QueryRows = ({ exploreId }: Props) => {
|
|||||||
}, [dispatch, exploreId]);
|
}, [dispatch, exploreId]);
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
async (newQueries: DataQuery[]) => {
|
(newQueries: DataQuery[]) => {
|
||||||
dispatch(changeQueriesAction({ queries: newQueries, exploreId }));
|
dispatch(changeQueries({ exploreId, queries: newQueries }));
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[dispatch, exploreId, onRunQueries, queries]
|
[dispatch, exploreId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onAddQuery = useCallback(
|
const onAddQuery = useCallback(
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
SupplementaryQueryType,
|
SupplementaryQueryType,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
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 { ExploreId, ExploreItemState, StoreState, ThunkDispatch } from 'app/types';
|
||||||
|
|
||||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||||
@ -41,7 +41,9 @@ import {
|
|||||||
setSupplementaryQueryEnabled,
|
setSupplementaryQueryEnabled,
|
||||||
addQueryRow,
|
addQueryRow,
|
||||||
cleanSupplementaryQueryDataProviderAction,
|
cleanSupplementaryQueryDataProviderAction,
|
||||||
|
changeQueries,
|
||||||
} from './query';
|
} from './query';
|
||||||
|
import * as actions from './query';
|
||||||
import { makeExplorePaneState } from './utils';
|
import { makeExplorePaneState } from './utils';
|
||||||
|
|
||||||
const { testRange, defaultInitialState } = createDefaultInitialState();
|
const { testRange, defaultInitialState } = createDefaultInitialState();
|
||||||
@ -58,10 +60,10 @@ const datasources: DataSourceApi[] = [
|
|||||||
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>,
|
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>,
|
||||||
{
|
{
|
||||||
name: 'testDs2',
|
name: 'testDs2',
|
||||||
type: 'postgres',
|
type: 'mysql',
|
||||||
uid: 'ds2',
|
uid: 'ds2',
|
||||||
getRef: () => {
|
getRef: () => {
|
||||||
return { type: 'postgres', uid: 'ds2' };
|
return { type: 'mysql', uid: 'ds2' };
|
||||||
},
|
},
|
||||||
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>,
|
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>,
|
||||||
];
|
];
|
||||||
@ -81,7 +83,15 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
}),
|
}),
|
||||||
getDataSourceSrv: () => {
|
getDataSourceSrv: () => {
|
||||||
return {
|
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: {
|
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('importing queries', () => {
|
||||||
describe('when importing queries between the same type of data source', () => {
|
describe('when importing queries between the same type of data source', () => {
|
||||||
it('remove datasource property from all of the queries', async () => {
|
it('remove datasource property from all of the queries', async () => {
|
||||||
|
@ -36,7 +36,7 @@ import { CorrelationData } from 'app/features/correlations/useCorrelations';
|
|||||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||||
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||||
import { store } from 'app/store/store';
|
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 { ExploreId, ExploreState, QueryOptions, SupplementaryQueries } from 'app/types/explore';
|
||||||
|
|
||||||
import { notifyApp } from '../../../core/actions';
|
import { notifyApp } from '../../../core/actions';
|
||||||
@ -291,6 +291,35 @@ const getImportableQueries = async (
|
|||||||
return addDatasourceToQueries(targetDataSource, queriesOut);
|
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
|
* 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.
|
* labels part can be reused to get similar data.
|
||||||
|
Loading…
Reference in New Issue
Block a user