The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/explore/state/query.test.ts

1133 lines
40 KiB

import { snakeCase } from 'lodash';
import { EMPTY, interval, Observable, of } from 'rxjs';
import { thunkTester } from 'test/core/thunk/thunkTester';
import { assertIsDefined } from 'test/helpers/asserts';
import {
DataQueryRequest,
DataQueryResponse,
DataSourceApi,
DataSourceJsonData,
DataSourcePluginMeta,
DataSourceWithSupplementaryQueriesSupport,
LoadingState,
MutableDataFrame,
RawTimeRange,
SupplementaryQueryType,
} from '@grafana/data';
import { DataQuery, DataSourceRef } from '@grafana/schema';
import { createAsyncThunk, ExploreItemState, StoreState, ThunkDispatch } from 'app/types';
import { reducerTester } from '../../../../test/core/redux/reducerTester';
import { configureStore } from '../../../store/configureStore';
import { setTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv';
import { makeLogs } from '../__mocks__/makeLogs';
import { supplementaryQueryTypes } from '../utils/supplementaryQueries';
import { saveCorrelationsAction } from './explorePane';
import { createDefaultInitialState } from './helpers';
import {
addQueryRowAction,
addResultsToCache,
cancelQueries,
cancelQueriesAction,
cleanSupplementaryQueryAction,
clearCache,
importQueries,
queryReducer,
runQueries,
scanStartAction,
scanStopAction,
setSupplementaryQueryEnabled,
addQueryRow,
cleanSupplementaryQueryDataProviderAction,
clearLogs,
queryStreamUpdatedAction,
QueryEndedPayload,
changeQueries,
} from './query';
import * as actions from './query';
import { makeExplorePaneState } from './utils';
const { testRange, defaultInitialState } = createDefaultInitialState();
const exploreId = 'left';
const cleanUpMock = jest.fn();
const datasources: DataSourceApi[] = [
{
name: 'testDs',
type: 'postgres',
uid: 'ds1',
getRef: () => {
return { type: 'postgres', uid: 'ds1' };
},
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>,
{
name: 'testDs2',
type: 'mysql',
uid: 'ds2',
getRef: () => {
return { type: 'mysql', uid: 'ds2' };
},
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>,
];
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'),
getTemplateSrv: () => ({
updateTimeRange: jest.fn(),
}),
getDataSourceSrv: () => {
return {
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]
);
},
};
},
}));
function setupQueryResponse(state: StoreState) {
const leftDatasourceInstance = assertIsDefined(state.explore.panes.left!.datasourceInstance);
jest.mocked(leftDatasourceInstance.query).mockReturnValueOnce(
of({
error: { message: 'test error' },
data: [
new MutableDataFrame({
fields: [{ name: 'test', values: [] }],
meta: {
preferredVisualisationType: 'graph',
},
}),
],
} as DataQueryResponse)
);
}
async function setupStore(queries: DataQuery[], datasourceInstance: Partial<DataSourceApi>) {
let dispatch: ThunkDispatch, getState: () => StoreState;
const store: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
panes: {
[exploreId]: {
...defaultInitialState.explore.panes[exploreId],
queries: queries,
datasourceInstance: datasourceInstance,
},
},
},
} as unknown as Partial<StoreState>);
dispatch = store.dispatch;
getState = store.getState;
setupQueryResponse(getState());
await dispatch(addQueryRow(exploreId, 1));
return getState;
}
describe('runQueries', () => {
const setupTests = () => {
setTimeSrv({ init() {} } as unknown as TimeSrv);
return configureStore({
...defaultInitialState,
} as unknown as Partial<StoreState>);
};
it('should pass dataFrames to state even if there is error in response', async () => {
const { dispatch, getState } = setupTests();
setupQueryResponse(getState());
await dispatch(saveCorrelationsAction({ exploreId: 'left', correlations: [] }));
await dispatch(runQueries({ exploreId: 'left' }));
expect(getState().explore.panes.left!.showMetrics).toBeTruthy();
expect(getState().explore.panes.left!.graphResult).toBeDefined();
});
it('should modify the request-id for all supplementary queries', () => {
const { dispatch, getState } = setupTests();
setupQueryResponse(getState());
dispatch(saveCorrelationsAction({ exploreId: 'left', correlations: [] }));
dispatch(runQueries({ exploreId: 'left' }));
const state = getState().explore.panes.left!;
expect(state.queryResponse.request?.requestId).toBe('explore_left');
const datasource = state.datasourceInstance as unknown as DataSourceWithSupplementaryQueriesSupport<DataQuery>;
for (const type of supplementaryQueryTypes) {
expect(datasource.getDataProvider).toHaveBeenCalledWith(
type,
expect.objectContaining({
requestId: `explore_left_${snakeCase(type)}_0`,
})
);
}
});
it('should set state to done if query completes without emitting', async () => {
const { dispatch, getState } = setupTests();
const leftDatasourceInstance = assertIsDefined(getState().explore.panes.left!.datasourceInstance);
jest.mocked(leftDatasourceInstance.query).mockReturnValueOnce(EMPTY);
await dispatch(saveCorrelationsAction({ exploreId: 'left', correlations: [] }));
await dispatch(runQueries({ exploreId: 'left' }));
await new Promise((resolve) => setTimeout(() => resolve(''), 500));
expect(getState().explore.panes.left!.queryResponse.state).toBe(LoadingState.Done);
});
it('shows results only after correlations are loaded', async () => {
const { dispatch, getState } = setupTests();
setupQueryResponse(getState());
await dispatch(runQueries({ exploreId: 'left' }));
expect(getState().explore.panes.left!.graphResult).not.toBeDefined();
await dispatch(saveCorrelationsAction({ exploreId: 'left', correlations: [] }));
expect(getState().explore.panes.left!.graphResult).toBeDefined();
});
});
describe('running queries', () => {
it('should cancel running query when cancelQueries is dispatched', async () => {
const unsubscribable = interval(1000);
unsubscribable.subscribe();
const exploreId = 'left';
const initialState = {
explore: {
panes: {
[exploreId]: {
datasourceInstance: { name: 'testDs' },
initialized: true,
loading: true,
querySubscription: unsubscribable,
queries: ['A'],
range: testRange,
supplementaryQueries: {
[SupplementaryQueryType.LogsVolume]: { enabled: true },
[SupplementaryQueryType.LogsSample]: { enabled: true },
},
},
},
},
user: {
orgId: 'A',
},
};
const dispatchedActions = await thunkTester(initialState)
.givenThunk(cancelQueries)
.whenThunkIsDispatched(exploreId);
expect(dispatchedActions).toEqual([
scanStopAction({ exploreId }),
cancelQueriesAction({ exploreId }),
cleanSupplementaryQueryDataProviderAction({ exploreId, type: SupplementaryQueryType.LogsVolume }),
cleanSupplementaryQueryAction({ exploreId, type: SupplementaryQueryType.LogsVolume }),
cleanSupplementaryQueryDataProviderAction({ exploreId, type: SupplementaryQueryType.LogsSample }),
cleanSupplementaryQueryAction({ exploreId, type: SupplementaryQueryType.LogsSample }),
]);
});
it('should cancel running query when a new query is issued', async () => {
const initialState = {
...makeExplorePaneState(),
};
const dispatchedActions = await thunkTester(initialState)
.givenThunk(runQueries)
.whenThunkIsDispatched({ exploreId });
expect(dispatchedActions).toContainEqual(cancelQueriesAction({ exploreId }));
});
});
describe('changeQueries', () => {
afterEach(() => {
jest.restoreAllMocks();
});
// 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', () => {
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: {
panes: {
left: {
...defaultInitialState.explore.panes.left,
datasourceInstance: datasources[0],
queries: originalQueries,
},
},
},
} as unknown as Partial<StoreState>);
await dispatch(
changeQueries({
queries: [{ refId: 'A', datasource: datasources[1].getRef() }],
exploreId: 'left',
})
);
expect(actions.changeQueriesAction).not.toHaveBeenCalled();
expect(actions.importQueries).toHaveBeenCalledWith(
'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: {
panes: {
left: {
...defaultInitialState.explore.panes.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: '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: {
panes: {
left: {
...defaultInitialState.explore.panes.left,
datasourceInstance: datasources[0],
queries: originalQueries,
},
},
},
} as unknown as Partial<StoreState>);
await dispatch(
changeQueries({
queries: [{ refId: 'A', datasource: datasources[1].getRef() }],
exploreId: 'left',
})
);
expect(getState().explore.panes.left!.queries[0]).toHaveProperty('refId', 'A');
expect(getState().explore.panes.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: {
panes: {
left: {
...defaultInitialState.explore.panes.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: 'left',
})
);
expect(getState().explore.panes.left!.queries[0]).toHaveProperty('refId', 'A');
expect(getState().explore.panes.left!.queries[0]).toHaveProperty('datasource', datasources[0].getRef());
expect(getState().explore.panes.left!.queries[0]).toEqual({
refId: 'A',
datasource: datasources[0].getRef(),
queryType: 'someValue',
});
});
});
it('runs remaining queries when one query is removed', async () => {
jest.spyOn(actions, 'runQueries').mockImplementation(createAsyncThunk('@explore/runQueries', () => {}));
const originalQueries = [
{ refId: 'A', datasource: datasources[0].getRef() },
{ refId: 'B', datasource: datasources[0].getRef() },
];
const { dispatch } = configureStore({
...defaultInitialState,
explore: {
panes: {
left: {
...defaultInitialState.explore.panes.left,
datasourceInstance: datasources[0],
queries: originalQueries,
},
},
},
} as unknown as Partial<StoreState>);
await dispatch(
changeQueries({
queries: [originalQueries[0]],
exploreId: 'left',
})
);
expect(actions.runQueries).toHaveBeenCalled();
});
});
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,
explore: {
panes: {
left: {
...defaultInitialState.explore.panes.left,
datasourceInstance: datasources[0],
},
},
},
} as unknown as Partial<StoreState>);
await dispatch(
importQueries(
'left',
[
{ datasource: { type: 'postgresql', uid: 'ds1' }, refId: 'refId_A' },
{ datasource: { type: 'postgresql', uid: 'ds1' }, refId: 'refId_B' },
],
datasources[0],
datasources[1]
)
);
expect(getState().explore.panes.left!.queries[0]).toHaveProperty('refId', 'refId_A');
expect(getState().explore.panes.left!.queries[1]).toHaveProperty('refId', 'refId_B');
expect(getState().explore.panes.left!.queries[0]).toHaveProperty('datasource.uid', 'ds2');
expect(getState().explore.panes.left!.queries[1]).toHaveProperty('datasource.uid', 'ds2');
});
});
});
describe('adding new query rows', () => {
it('should add another query row if there are two rows already', async () => {
const queries = [
{
datasource: { type: 'loki', uid: 'ds3' },
refId: 'C',
},
{
datasource: { type: 'loki', uid: 'ds4' },
refId: 'D',
},
];
const datasourceInstance = {
query: jest.fn(),
getRef: jest.fn(),
meta: {
id: 'loki',
mixed: false,
} as unknown as DataSourcePluginMeta<{}>,
};
const getState = await setupStore(queries, datasourceInstance);
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.id).toBe('loki');
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.mixed).toBe(false);
expect(getState().explore.panes[exploreId]!.queries).toHaveLength(3);
expect(getState().explore.panes[exploreId]!.queryKeys).toEqual(['ds3-0', 'ds4-1', 'ds4-2']);
});
it('should add query row whith root ds (without overriding the default ds) when there is not yet a row', async () => {
const queries: DataQuery[] = [];
const datasourceInstance = {
query: jest.fn(),
getRef: jest.fn(),
meta: {
id: 'mixed',
mixed: true,
} as unknown as DataSourcePluginMeta<{}>,
};
const getState = await setupStore(queries, datasourceInstance);
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.id).toBe('mixed');
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.mixed).toBe(true);
expect(getState().explore.panes[exploreId]!.queries).toHaveLength(1);
expect(getState().explore.panes[exploreId]!.queries[0]?.datasource?.type).toBe('postgres');
expect(getState().explore.panes[exploreId]!.queryKeys).toEqual(['ds1-0']);
});
it('should add query row whith root ds (with overriding the default ds) when there is not yet a row', async () => {
const queries: DataQuery[] = [];
const datasourceInstance = {
query: jest.fn(),
getRef: () => {
return { type: 'loki', uid: 'uid-loki' };
},
meta: {
id: 'loki',
mixed: false,
} as unknown as DataSourcePluginMeta<{}>,
};
const getState = await setupStore(queries, datasourceInstance);
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.id).toBe('loki');
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.mixed).toBe(false);
expect(getState().explore.panes[exploreId]!.queries).toHaveLength(1);
expect(getState().explore.panes[exploreId]!.queries[0]?.datasource?.type).toBe('loki');
expect(getState().explore.panes[exploreId]!.queryKeys).toEqual(['uid-loki-0']);
});
it('should add another query row if there are two rows already', async () => {
const queries = [
{
datasource: { type: 'postgres', uid: 'ds3' },
refId: 'C',
},
{
datasource: { type: 'loki', uid: 'ds4' },
refId: 'D',
},
];
const datasourceInstance = {
query: jest.fn(),
getRef: jest.fn(),
meta: {
id: 'mixed',
mixed: true,
} as unknown as DataSourcePluginMeta<{}>,
};
const getState = await setupStore(queries, datasourceInstance);
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.id).toBe('mixed');
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.mixed).toBe(true);
expect(getState().explore.panes[exploreId]!.queries).toHaveLength(3);
expect(getState().explore.panes[exploreId]!.queries[2]?.datasource?.type).toBe('loki');
expect(getState().explore.panes[exploreId]!.queryKeys).toEqual(['ds3-0', 'ds4-1', 'ds4-2']);
});
});
describe('reducer', () => {
describe('scanning', () => {
it('should start scanning', () => {
const initialState: ExploreItemState = {
...makeExplorePaneState(),
scanning: false,
};
reducerTester<ExploreItemState>()
.givenReducer(queryReducer, initialState)
.whenActionIsDispatched(scanStartAction({ 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: 'left' }))
.thenStateShouldEqual({
...initialState,
scanning: false,
scanRange: undefined,
});
});
});
describe('query rows', () => {
it('should add query row when there is no query row yet', () => {
reducerTester<ExploreItemState>()
.givenReducer(queryReducer, {
queries: [],
} as unknown as ExploreItemState)
.whenActionIsDispatched(
addQueryRowAction({
exploreId: 'left',
query: { refId: 'A', key: 'mockKey' },
index: 0,
})
)
.thenStateShouldEqual({
queries: [{ refId: 'A', key: 'mockKey' }],
queryKeys: ['mockKey-0'],
} as unknown as ExploreItemState);
});
it('should add query row when there is already one query row', () => {
reducerTester<ExploreItemState>()
.givenReducer(queryReducer, {
queries: [{ refId: 'A', key: 'initialRow', datasource: { type: 'loki' } }],
} as unknown as ExploreItemState)
.whenActionIsDispatched(
addQueryRowAction({
exploreId: 'left',
query: { refId: 'B', key: 'mockKey', datasource: { type: 'loki' } },
index: 0,
})
)
.thenStateShouldEqual({
queries: [
{ refId: 'A', key: 'initialRow', datasource: { type: 'loki' } },
{ refId: 'B', key: 'mockKey', datasource: { type: 'loki' } },
],
queryKeys: ['initialRow-0', 'mockKey-1'],
} as unknown as ExploreItemState);
});
describe('addQueryRow', () => {
it('adds a query from root datasource if root is not mixed and there are no queries', async () => {
const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
...defaultInitialState.explore,
panes: {
left: {
...defaultInitialState.explore.panes.left,
queries: [],
datasourceInstance: {
meta: {
mixed: false,
},
getRef() {
return { type: 'loki', uid: 'uid-loki' };
},
},
},
},
},
} as unknown as Partial<StoreState>);
await dispatch(addQueryRow('left', 0));
expect(getState().explore.panes.left?.queries).toEqual([
expect.objectContaining({ datasource: { type: 'loki', uid: 'uid-loki' } }),
]);
});
it('adds a query from root datasource if root is not mixed and there are queries without a datasource specified', async () => {
const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
panes: {
left: {
...defaultInitialState.explore.panes.left,
queries: [{ expr: 1 }],
datasourceInstance: {
meta: {
mixed: false,
},
getRef() {
return { type: 'loki', uid: 'uid-loki' };
},
},
},
},
},
} as unknown as Partial<StoreState>);
await dispatch(addQueryRow('left', 0));
expect(getState().explore.panes.left?.queries).toEqual([
expect.anything(),
expect.objectContaining({ datasource: { type: 'loki', uid: 'uid-loki' } }),
]);
});
it('adds a query from default datasource if root is mixed and there are no queries', async () => {
const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
panes: {
left: {
...defaultInitialState.explore.panes.left,
queries: [],
datasourceInstance: {
meta: {
mixed: true,
},
getRef() {
return { type: 'mixed', uid: '-- Mixed --' };
},
},
},
},
},
} as unknown as Partial<StoreState>);
await dispatch(addQueryRow('left', 0));
expect(getState().explore.panes.left?.queries).toEqual([
expect.objectContaining({ datasource: { type: 'postgres', uid: 'ds1' } }),
]);
});
});
});
describe('caching', () => {
it('should add response to cache', async () => {
const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
panes: {
left: {
...defaultInitialState.explore.panes.left,
queryResponse: {
series: [{ name: 'test name' }],
state: LoadingState.Done,
},
absoluteRange: { from: 1621348027000, to: 1621348050000 },
},
},
},
} as unknown as Partial<StoreState>);
await dispatch(addResultsToCache('left'));
expect(getState().explore.panes.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,
explore: {
panes: {
left: {
...defaultInitialState.explore.panes.left,
queryResponse: { series: [{ name: 'test name' }], state: LoadingState.Loading },
absoluteRange: { from: 1621348027000, to: 1621348050000 },
},
},
},
} as unknown as Partial<StoreState>);
await dispatch(addResultsToCache('left'));
expect(getState().explore.panes.left!.cache).toEqual([]);
});
it('should not add duplicate response to cache', async () => {
const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
panes: {
left: {
...defaultInitialState.explore.panes.left,
queryResponse: {
series: [{ name: 'test name' }],
state: LoadingState.Done,
},
absoluteRange: { from: 1621348027000, to: 1621348050000 },
cache: [
{
key: 'from=1621348027000&to=1621348050000',
value: { series: [{ name: 'old test name' }], state: LoadingState.Done },
},
],
},
},
},
} as unknown as Partial<StoreState>);
await dispatch(addResultsToCache('left'));
expect(getState().explore.panes.left!.cache).toHaveLength(1);
expect(getState().explore.panes.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,
explore: {
panes: {
left: {
...defaultInitialState.explore.panes.left,
cache: [
{
key: 'from=1621348027000&to=1621348050000',
value: { series: [{ name: 'old test name' }], state: 'Done' },
},
],
},
},
},
} as unknown as Partial<StoreState>);
await dispatch(clearCache('left'));
expect(getState().explore.panes.left!.cache).toEqual([]);
});
});
describe('when data source does not support log volume supplementary query', () => {
it('cleans up query subscription correctly (regression #70049)', async () => {
const store: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
panes: {
left: {
...defaultInitialState.explore.panes.left,
datasourceInstance: {
getRef: jest.fn(),
meta: {
id: 'something',
},
query(
request: DataQueryRequest<DataQuery>
): Promise<DataQueryResponse> | Observable<DataQueryResponse> {
return new Observable(() => cleanUpMock);
},
},
},
},
},
} as unknown as Partial<StoreState>);
const dispatch = store.dispatch;
cleanUpMock.mockClear();
await dispatch(runQueries({ exploreId: 'left' }));
await dispatch(cancelQueries('left'));
expect(cleanUpMock).toBeCalledTimes(1);
});
});
describe('supplementary queries', () => {
let dispatch: ThunkDispatch,
getState: () => StoreState,
unsubscribes: Function[],
mockDataProvider: () => Observable<DataQueryResponse>;
beforeEach(() => {
unsubscribes = [];
mockDataProvider = () => {
return {
subscribe: () => {
const unsubscribe = jest.fn();
unsubscribes.push(unsubscribe);
return {
unsubscribe,
};
},
} as unknown as Observable<DataQueryResponse>;
};
const store: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
panes: {
left: {
...defaultInitialState.explore.panes.left,
datasourceInstance: {
query: jest.fn(),
getRef: jest.fn(),
meta: {
id: 'something',
},
getDataProvider: () => {
return mockDataProvider();
},
getSupportedSupplementaryQueryTypes: () => [
SupplementaryQueryType.LogsVolume,
SupplementaryQueryType.LogsSample,
],
getSupplementaryQuery: jest.fn(),
},
},
},
},
} as unknown as Partial<StoreState>);
dispatch = store.dispatch;
getState = store.getState;
setupQueryResponse(getState());
});
it('should cancel any unfinished supplementary queries when a new query is run', async () => {
dispatch(runQueries({ exploreId: 'left' }));
// first query is run automatically
// loading in progress - subscriptions for both supplementary queries are created, not cleaned up yet
expect(unsubscribes).toHaveLength(2);
expect(unsubscribes[0]).not.toBeCalled();
expect(unsubscribes[1]).not.toBeCalled();
setupQueryResponse(getState());
dispatch(runQueries({ exploreId: 'left' }));
// a new query is run while supplementary queries are not resolve yet...
expect(unsubscribes[0]).toBeCalled();
expect(unsubscribes[1]).toBeCalled();
// first subscriptions are cleaned up, a new subscriptions are created automatically
expect(unsubscribes).toHaveLength(4);
expect(unsubscribes[2]).not.toBeCalled();
expect(unsubscribes[3]).not.toBeCalled();
});
it('should cancel all supported supplementary queries when the main query is canceled', () => {
dispatch(runQueries({ exploreId: 'left' }));
expect(unsubscribes).toHaveLength(2);
expect(unsubscribes[0]).not.toBeCalled();
expect(unsubscribes[1]).not.toBeCalled();
dispatch(cancelQueries('left'));
expect(unsubscribes).toHaveLength(2);
expect(unsubscribes[0]).toBeCalled();
expect(unsubscribes[1]).toBeCalled();
for (const type of supplementaryQueryTypes) {
expect(getState().explore.panes.left!.supplementaryQueries[type].data).toBeUndefined();
expect(getState().explore.panes.left!.supplementaryQueries[type].dataProvider).toBeUndefined();
}
});
it('should load supplementary queries after running the query', () => {
dispatch(runQueries({ exploreId: 'left' }));
expect(unsubscribes).toHaveLength(2);
});
it('should clean any incomplete supplementary queries data when main query is canceled', () => {
mockDataProvider = () => {
return of({ state: LoadingState.Loading, error: undefined, data: [] });
};
dispatch(runQueries({ exploreId: 'left' }));
for (const type of supplementaryQueryTypes) {
expect(getState().explore.panes.left!.supplementaryQueries[type].data).toBeDefined();
expect(getState().explore.panes.left!.supplementaryQueries[type].data!.state).toBe(LoadingState.Loading);
expect(getState().explore.panes.left!.supplementaryQueries[type].dataProvider).toBeDefined();
}
for (const type of supplementaryQueryTypes) {
expect(getState().explore.panes.left!.supplementaryQueries[type].data).toBeDefined();
expect(getState().explore.panes.left!.supplementaryQueries[type].data!.state).toBe(LoadingState.Loading);
expect(getState().explore.panes.left!.supplementaryQueries[type].dataProvider).toBeDefined();
}
dispatch(cancelQueries('left'));
for (const type of supplementaryQueryTypes) {
expect(getState().explore.panes.left!.supplementaryQueries[type].data).toBeUndefined();
expect(getState().explore.panes.left!.supplementaryQueries[type].data).toBeUndefined();
}
});
it('keeps complete supplementary data when main query is canceled', async () => {
mockDataProvider = () => {
return of(
{ state: LoadingState.Loading, error: undefined, data: [] },
{ state: LoadingState.Done, error: undefined, data: [{}] }
);
};
dispatch(runQueries({ exploreId: 'left' }));
for (const types of supplementaryQueryTypes) {
expect(getState().explore.panes.left!.supplementaryQueries[types].data).toBeDefined();
expect(getState().explore.panes.left!.supplementaryQueries[types].data!.state).toBe(LoadingState.Done);
expect(getState().explore.panes.left!.supplementaryQueries[types].dataProvider).toBeDefined();
}
dispatch(cancelQueries('left'));
for (const types of supplementaryQueryTypes) {
expect(getState().explore.panes.left!.supplementaryQueries[types].data).toBeDefined();
expect(getState().explore.panes.left!.supplementaryQueries[types].data!.state).toBe(LoadingState.Done);
expect(getState().explore.panes.left!.supplementaryQueries[types].dataProvider).toBeUndefined();
}
});
it('do not load disabled supplementary query data', () => {
mockDataProvider = () => {
return of({ state: LoadingState.Done, error: undefined, data: [{}] });
};
// turn logs volume off (but keep logs sample on)
dispatch(setSupplementaryQueryEnabled('left', false, SupplementaryQueryType.LogsVolume));
expect(getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].enabled).toBe(
false
);
expect(getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsSample].enabled).toBe(true);
// verify that if we run a query, it will: 1) not do logs volume, 2) do logs sample 3) provider will still be set for both
dispatch(runQueries({ exploreId: 'left' }));
expect(
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].data
).toBeUndefined();
expect(
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].dataSubscription
).toBeUndefined();
expect(
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].dataProvider
).toBeDefined();
expect(getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsSample].data).toBeDefined();
expect(
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsSample].dataSubscription
).toBeDefined();
expect(
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsSample].dataProvider
).toBeDefined();
});
it('load data of supplementary query that gets enabled', async () => {
// first we start with both supplementary queries disabled
dispatch(setSupplementaryQueryEnabled('left', false, SupplementaryQueryType.LogsVolume));
dispatch(setSupplementaryQueryEnabled('left', false, SupplementaryQueryType.LogsSample));
// runQueries sets up providers, but does not run queries
dispatch(runQueries({ exploreId: 'left' }));
expect(
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].dataProvider
).toBeDefined();
expect(
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsSample].dataProvider
).toBeDefined();
// we turn 1 supplementary query (logs volume) on
dispatch(setSupplementaryQueryEnabled('left', true, SupplementaryQueryType.LogsVolume));
// verify it was turned on
expect(getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].enabled).toBe(true);
// verify that other stay off
expect(getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsSample].enabled).toBe(
false
);
expect(
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].dataSubscription
).toBeDefined();
expect(
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsSample].dataSubscription
).toBeUndefined();
});
});
describe('clear live logs', () => {
it('should clear current log rows', async () => {
const logRows = makeLogs(10);
const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
panes: {
['left']: {
...defaultInitialState.explore.panes['left'],
queryResponse: {
state: LoadingState.Streaming,
},
logsResult: {
hasUniqueLabels: false,
rows: logRows,
},
},
},
},
} as unknown as Partial<StoreState>);
expect(getState().explore.panes['left']?.logsResult?.rows.length).toBe(logRows.length);
await dispatch(clearLogs({ exploreId: 'left' }));
expect(getState().explore.panes['left']?.logsResult?.rows.length).toBe(0);
expect(getState().explore.panes['left']?.clearedAtIndex).toBe(logRows.length - 1);
});
it('should filter new log rows', async () => {
const oldLogRows = makeLogs(10);
const newLogRows = makeLogs(5);
const allLogRows = [...oldLogRows, ...newLogRows];
const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
panes: {
['left']: {
...defaultInitialState.explore.panes['left'],
isLive: true,
queryResponse: {
state: LoadingState.Streaming,
},
logsResult: {
hasUniqueLabels: false,
rows: oldLogRows,
},
},
},
},
} as unknown as Partial<StoreState>);
expect(getState().explore.panes['left']?.logsResult?.rows.length).toBe(oldLogRows.length);
await dispatch(clearLogs({ exploreId: 'left' }));
await dispatch(
queryStreamUpdatedAction({
exploreId: 'left',
response: {
request: true,
traceFrames: [],
nodeGraphFrames: [],
rawPrometheusFrames: [],
flameGraphFrames: [],
logsResult: {
hasUniqueLabels: false,
rows: allLogRows,
},
},
} as unknown as QueryEndedPayload)
);
expect(getState().explore.panes['left']?.logsResult?.rows.length).toBe(newLogRows.length);
expect(getState().explore.panes['left']?.clearedAtIndex).toBe(oldLogRows.length - 1);
});
});
});