123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443 |
- import { EMPTY, interval, Observable, of } from 'rxjs';
- import { thunkTester } from 'test/core/thunk/thunkTester';
- import {
- ArrayVector,
- DataFrame,
- DataQuery,
- DataQueryResponse,
- DataSourceApi,
- DataSourceJsonData,
- DataSourceWithLogsVolumeSupport,
- LoadingState,
- MutableDataFrame,
- PanelData,
- RawTimeRange,
- } from '@grafana/data';
- import { ExploreId, ExploreItemState, StoreState, ThunkDispatch } from 'app/types';
- import { reducerTester } from '../../../../test/core/redux/reducerTester';
- import { configureStore } from '../../../store/configureStore';
- import { setTimeSrv } from '../../dashboard/services/TimeSrv';
- import { createDefaultInitialState } from './helpers';
- import {
- addQueryRowAction,
- addResultsToCache,
- cancelQueries,
- cancelQueriesAction,
- cleanLogsVolumeAction,
- clearCache,
- importQueries,
- queryReducer,
- runQueries,
- scanStartAction,
- scanStopAction,
- storeLogsVolumeDataProviderAction,
- } from './query';
- import { makeExplorePaneState } from './utils';
- import Mock = jest.Mock;
- const { testRange, defaultInitialState } = createDefaultInitialState();
- jest.mock('app/features/dashboard/services/TimeSrv', () => ({
- ...jest.requireActual('app/features/dashboard/services/TimeSrv'),
- getTimeSrv: () => ({
- init: jest.fn(),
- timeRange: jest.fn().mockReturnValue({}),
- }),
- }));
- jest.mock('@grafana/runtime', () => ({
- ...(jest.requireActual('@grafana/runtime') as unknown as object),
- getTemplateSrv: () => ({
- updateTimeRange: jest.fn(),
- }),
- }));
- function setupQueryResponse(state: StoreState) {
- (state.explore[ExploreId.left].datasourceInstance?.query as Mock).mockReturnValueOnce(
- of({
- error: { message: 'test error' },
- data: [
- new MutableDataFrame({
- fields: [{ name: 'test', values: new ArrayVector() }],
- meta: {
- preferredVisualisationType: 'graph',
- },
- }),
- ],
- } as DataQueryResponse)
- );
- }
- describe('runQueries', () => {
- it('should pass dataFrames to state even if there is error in response', async () => {
- setTimeSrv({ init() {} } as any);
- const { dispatch, getState } = configureStore({
- ...(defaultInitialState as any),
- });
- setupQueryResponse(getState());
- await dispatch(runQueries(ExploreId.left));
- expect(getState().explore[ExploreId.left].showMetrics).toBeTruthy();
- expect(getState().explore[ExploreId.left].graphResult).toBeDefined();
- });
- it('should modify the request-id for log-volume queries', async () => {
- setTimeSrv({ init() {} } as any);
- const { dispatch, getState } = configureStore({
- ...(defaultInitialState as any),
- });
- setupQueryResponse(getState());
- await dispatch(runQueries(ExploreId.left));
- const state = getState().explore[ExploreId.left];
- expect(state.queryResponse.request?.requestId).toBe('explore_left');
- const datasource = state.datasourceInstance as any as DataSourceWithLogsVolumeSupport<DataQuery>;
- expect(datasource.getLogsVolumeDataProvider).toBeCalledWith(
- expect.objectContaining({
- requestId: 'explore_left_log_volume',
- })
- );
- });
- it('should set state to done if query completes without emitting', async () => {
- setTimeSrv({ init() {} } as any);
- const { dispatch, getState } = configureStore({
- ...(defaultInitialState as any),
- });
- (getState().explore[ExploreId.left].datasourceInstance?.query as Mock).mockReturnValueOnce(EMPTY);
- await dispatch(runQueries(ExploreId.left));
- await new Promise((resolve) => setTimeout(() => resolve(''), 500));
- expect(getState().explore[ExploreId.left].queryResponse.state).toBe(LoadingState.Done);
- });
- });
- 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: { name: 'testDs' },
- 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 }),
- storeLogsVolumeDataProviderAction({ exploreId, logsVolumeDataProvider: undefined }),
- cleanLogsVolumeAction({ exploreId }),
- ]);
- });
- });
- describe('importing queries', () => {
- describe('when importing queries between the same type of data source', () => {
- it('remove datasource property from all of the queries', async () => {
- const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
- ...(defaultInitialState as any),
- explore: {
- [ExploreId.left]: {
- ...defaultInitialState.explore[ExploreId.left],
- datasourceInstance: { name: 'testDs', type: 'postgres' },
- },
- },
- });
- await dispatch(
- importQueries(
- ExploreId.left,
- [
- { datasource: { type: 'postgresql' }, refId: 'refId_A' },
- { datasource: { type: 'postgresql' }, refId: 'refId_B' },
- ],
- { name: 'Postgres1', type: 'postgres' } as DataSourceApi<DataQuery, DataSourceJsonData, {}>,
- { name: 'Postgres2', type: 'postgres' } as DataSourceApi<DataQuery, DataSourceJsonData, {}>
- )
- );
- expect(getState().explore[ExploreId.left].queries[0]).toHaveProperty('refId', 'refId_A');
- expect(getState().explore[ExploreId.left].queries[1]).toHaveProperty('refId', 'refId_B');
- expect(getState().explore[ExploreId.left].queries[0]).not.toHaveProperty('datasource');
- expect(getState().explore[ExploreId.left].queries[1]).not.toHaveProperty('datasource');
- });
- });
- });
- describe('reducer', () => {
- describe('scanning', () => {
- it('should start scanning', () => {
- const initialState: ExploreItemState = {
- ...makeExplorePaneState(),
- scanning: false,
- };
- reducerTester<ExploreItemState>()
- .givenReducer(queryReducer, initialState)
- .whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left }))
- .thenStateShouldEqual({
- ...initialState,
- scanning: true,
- });
- });
- it('should stop scanning', () => {
- const initialState = {
- ...makeExplorePaneState(),
- scanning: true,
- scanRange: {} as RawTimeRange,
- };
- reducerTester<ExploreItemState>()
- .givenReducer(queryReducer, initialState)
- .whenActionIsDispatched(scanStopAction({ exploreId: ExploreId.left }))
- .thenStateShouldEqual({
- ...initialState,
- scanning: false,
- scanRange: undefined,
- });
- });
- });
- describe('query rows', () => {
- it('adds a new query row', () => {
- reducerTester<ExploreItemState>()
- .givenReducer(queryReducer, {
- queries: [],
- } as unknown as ExploreItemState)
- .whenActionIsDispatched(
- addQueryRowAction({
- exploreId: ExploreId.left,
- query: { refId: 'A', key: 'mockKey' },
- index: 0,
- })
- )
- .thenStateShouldEqual({
- queries: [{ refId: 'A', key: 'mockKey' }],
- queryKeys: ['mockKey-0'],
- } as unknown as ExploreItemState);
- });
- });
- describe('caching', () => {
- it('should add response to cache', async () => {
- const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
- ...(defaultInitialState as any),
- explore: {
- [ExploreId.left]: {
- ...defaultInitialState.explore[ExploreId.left],
- queryResponse: {
- series: [{ name: 'test name' }] as DataFrame[],
- state: LoadingState.Done,
- } as PanelData,
- absoluteRange: { from: 1621348027000, to: 1621348050000 },
- },
- },
- });
- await dispatch(addResultsToCache(ExploreId.left));
- expect(getState().explore[ExploreId.left].cache).toEqual([
- { key: 'from=1621348027000&to=1621348050000', value: { series: [{ name: 'test name' }], state: 'Done' } },
- ]);
- });
- it('should not add response to cache if response is still loading', async () => {
- const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
- ...(defaultInitialState as any),
- explore: {
- [ExploreId.left]: {
- ...defaultInitialState.explore[ExploreId.left],
- queryResponse: { series: [{ name: 'test name' }] as DataFrame[], state: LoadingState.Loading } as PanelData,
- absoluteRange: { from: 1621348027000, to: 1621348050000 },
- },
- },
- });
- await dispatch(addResultsToCache(ExploreId.left));
- expect(getState().explore[ExploreId.left].cache).toEqual([]);
- });
- it('should not add duplicate response to cache', async () => {
- const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
- ...(defaultInitialState as any),
- explore: {
- [ExploreId.left]: {
- ...defaultInitialState.explore[ExploreId.left],
- queryResponse: {
- series: [{ name: 'test name' }] as DataFrame[],
- state: LoadingState.Done,
- } as PanelData,
- absoluteRange: { from: 1621348027000, to: 1621348050000 },
- cache: [
- {
- key: 'from=1621348027000&to=1621348050000',
- value: { series: [{ name: 'old test name' }], state: LoadingState.Done },
- },
- ],
- },
- },
- });
- await dispatch(addResultsToCache(ExploreId.left));
- expect(getState().explore[ExploreId.left].cache).toHaveLength(1);
- expect(getState().explore[ExploreId.left].cache).toEqual([
- { key: 'from=1621348027000&to=1621348050000', value: { series: [{ name: 'old test name' }], state: 'Done' } },
- ]);
- });
- it('should clear cache', async () => {
- const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
- ...(defaultInitialState as any),
- explore: {
- [ExploreId.left]: {
- ...defaultInitialState.explore[ExploreId.left],
- cache: [
- {
- key: 'from=1621348027000&to=1621348050000',
- value: { series: [{ name: 'old test name' }], state: 'Done' },
- },
- ],
- },
- },
- });
- await dispatch(clearCache(ExploreId.left));
- expect(getState().explore[ExploreId.left].cache).toEqual([]);
- });
- });
- describe('log volume', () => {
- let dispatch: ThunkDispatch,
- getState: () => StoreState,
- unsubscribes: Function[],
- mockLogsVolumeDataProvider: () => Observable<DataQueryResponse>;
- beforeEach(() => {
- unsubscribes = [];
- mockLogsVolumeDataProvider = () => {
- return {
- subscribe: () => {
- const unsubscribe = jest.fn();
- unsubscribes.push(unsubscribe);
- return {
- unsubscribe,
- };
- },
- } as unknown as Observable<DataQueryResponse>;
- };
- const store: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
- ...(defaultInitialState as any),
- explore: {
- [ExploreId.left]: {
- ...defaultInitialState.explore[ExploreId.left],
- datasourceInstance: {
- query: jest.fn(),
- getRef: jest.fn(),
- meta: {
- id: 'something',
- },
- getLogsVolumeDataProvider: () => {
- return mockLogsVolumeDataProvider();
- },
- },
- },
- },
- });
- dispatch = store.dispatch;
- getState = store.getState;
- setupQueryResponse(getState());
- });
- it('should cancel any unfinished logs volume queries when a new query is run', async () => {
- await dispatch(runQueries(ExploreId.left));
- // first query is run automatically
- // loading in progress - one subscription created, not cleaned up yet
- expect(unsubscribes).toHaveLength(1);
- expect(unsubscribes[0]).not.toBeCalled();
- setupQueryResponse(getState());
- await dispatch(runQueries(ExploreId.left));
- // a new query is run while log volume query is not resolve yet...
- expect(unsubscribes[0]).toBeCalled();
- // first subscription is cleaned up, a new subscription is created automatically
- expect(unsubscribes).toHaveLength(2);
- expect(unsubscribes[1]).not.toBeCalled();
- });
- it('should cancel log volume query when the main query is canceled', async () => {
- await dispatch(runQueries(ExploreId.left));
- expect(unsubscribes).toHaveLength(1);
- expect(unsubscribes[0]).not.toBeCalled();
- await dispatch(cancelQueries(ExploreId.left));
- expect(unsubscribes).toHaveLength(1);
- expect(unsubscribes[0]).toBeCalled();
- expect(getState().explore[ExploreId.left].logsVolumeData).toBeUndefined();
- expect(getState().explore[ExploreId.left].logsVolumeDataProvider).toBeUndefined();
- });
- it('should load logs volume after running the query', async () => {
- await dispatch(runQueries(ExploreId.left));
- expect(unsubscribes).toHaveLength(1);
- });
- it('should clean any incomplete log volume data when main query is canceled', async () => {
- mockLogsVolumeDataProvider = () => {
- return of({ state: LoadingState.Loading, error: undefined, data: [] });
- };
- await dispatch(runQueries(ExploreId.left));
- expect(getState().explore[ExploreId.left].logsVolumeData).toBeDefined();
- expect(getState().explore[ExploreId.left].logsVolumeData!.state).toBe(LoadingState.Loading);
- expect(getState().explore[ExploreId.left].logsVolumeDataProvider).toBeDefined();
- await dispatch(cancelQueries(ExploreId.left));
- expect(getState().explore[ExploreId.left].logsVolumeData).toBeUndefined();
- expect(getState().explore[ExploreId.left].logsVolumeDataProvider).toBeUndefined();
- });
- it('keeps complete log volume data when main query is canceled', async () => {
- mockLogsVolumeDataProvider = () => {
- return of(
- { state: LoadingState.Loading, error: undefined, data: [] },
- { state: LoadingState.Done, error: undefined, data: [{}] }
- );
- };
- await dispatch(runQueries(ExploreId.left));
- expect(getState().explore[ExploreId.left].logsVolumeData).toBeDefined();
- expect(getState().explore[ExploreId.left].logsVolumeData!.state).toBe(LoadingState.Done);
- expect(getState().explore[ExploreId.left].logsVolumeDataProvider).toBeDefined();
- await dispatch(cancelQueries(ExploreId.left));
- expect(getState().explore[ExploreId.left].logsVolumeData).toBeDefined();
- expect(getState().explore[ExploreId.left].logsVolumeData!.state).toBe(LoadingState.Done);
- expect(getState().explore[ExploreId.left].logsVolumeDataProvider).toBeUndefined();
- });
- });
- });
|