mirror of
https://github.com/grafana/grafana.git
synced 2025-02-20 11:48:34 -06:00
* Fix broken interpolation for mixed datasources in explore * Add interpolation tests * Use Promise.all and be better about dealing with invalid empty ds in query scenario * condense logic * Remove type declarations * Fix tests
290 lines
9.5 KiB
TypeScript
290 lines
9.5 KiB
TypeScript
import { DataSourceApi, dateTime, ExploreUrlState, LogsSortOrder } from '@grafana/data';
|
|
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
|
|
import { DataQuery } from '@grafana/schema';
|
|
import { RefreshPicker } from '@grafana/ui';
|
|
import store from 'app/core/store';
|
|
import { DEFAULT_RANGE } from 'app/features/explore/state/utils';
|
|
|
|
import { DatasourceSrvMock, MockDataSourceApi } from '../../../test/mocks/datasource_srv';
|
|
|
|
import {
|
|
buildQueryTransaction,
|
|
hasNonEmptyQuery,
|
|
refreshIntervalToSortOrder,
|
|
updateHistory,
|
|
getExploreUrl,
|
|
GetExploreUrlArguments,
|
|
getTimeRange,
|
|
generateEmptyQuery,
|
|
} from './explore';
|
|
|
|
const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
|
|
datasource: '',
|
|
queries: [],
|
|
range: DEFAULT_RANGE,
|
|
};
|
|
|
|
const defaultDs = new MockDataSourceApi('default datasource', { data: ['default data'] });
|
|
const interpolateMockLoki = jest
|
|
.fn()
|
|
.mockReturnValue([{ refId: 'a', expr: 'replaced testDs loki' }]) as unknown as DataQuery[];
|
|
const interpolateMockProm = jest
|
|
.fn()
|
|
.mockReturnValue([{ refId: 'a', expr: 'replaced testDs2 prom' }]) as unknown as DataQuery[];
|
|
const datasourceSrv = new DatasourceSrvMock(defaultDs, {
|
|
'generate empty query': new MockDataSourceApi('generateEmptyQuery'),
|
|
ds1: {
|
|
name: 'testDs',
|
|
type: 'loki',
|
|
meta: { mixed: false },
|
|
interpolateVariablesInQueries: interpolateMockLoki,
|
|
getRef: () => {
|
|
return 'ds1';
|
|
},
|
|
} as unknown as DataSourceApi,
|
|
ds2: {
|
|
name: 'testDs2',
|
|
type: 'prom',
|
|
meta: { mixed: false },
|
|
interpolateVariablesInQueries: interpolateMockProm,
|
|
getRef: () => {
|
|
return 'ds2';
|
|
},
|
|
} as unknown as DataSourceApi,
|
|
dsMixed: {
|
|
name: 'testDSMixed',
|
|
type: 'mixed',
|
|
meta: { mixed: true },
|
|
} as MockDataSourceApi,
|
|
});
|
|
|
|
const getDataSourceSrvMock = jest.fn().mockReturnValue(datasourceSrv);
|
|
jest.mock('@grafana/runtime', () => ({
|
|
...jest.requireActual('@grafana/runtime'),
|
|
getDataSourceSrv: () => getDataSourceSrvMock(),
|
|
}));
|
|
|
|
describe('state functions', () => {
|
|
describe('serializeStateToUrlParam', () => {
|
|
it('returns url parameter value for a state object', () => {
|
|
const state = {
|
|
...DEFAULT_EXPLORE_STATE,
|
|
datasource: 'foo',
|
|
queries: [
|
|
{
|
|
expr: 'metric{test="a/b"}',
|
|
refId: 'A',
|
|
},
|
|
{
|
|
expr: 'super{foo="x/z"}',
|
|
refId: 'B',
|
|
},
|
|
],
|
|
range: {
|
|
from: 'now-5h',
|
|
to: 'now',
|
|
},
|
|
};
|
|
|
|
expect(serializeStateToUrlParam(state)).toBe(
|
|
'{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}","refId":"A"},' +
|
|
'{"expr":"super{foo=\\"x/z\\"}","refId":"B"}],"range":{"from":"now-5h","to":"now"}}'
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getExploreUrl', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
const args = {
|
|
queries: [
|
|
{ refId: 'A', expr: 'query1', legendFormat: 'legendFormat1' },
|
|
{ refId: 'B', expr: 'query2', datasource: { type: '__expr__', uid: '__expr__' } },
|
|
],
|
|
dsRef: {
|
|
uid: 'ds1',
|
|
},
|
|
timeRange: { from: dateTime(), to: dateTime(), raw: { from: 'now-1h', to: 'now' } },
|
|
} as unknown as GetExploreUrlArguments;
|
|
it('should use raw range in explore url', async () => {
|
|
expect(await getExploreUrl(args)).toMatch(/from%22:%22now-1h%22,%22to%22:%22now/g);
|
|
});
|
|
it('should omit expression target in explore url', async () => {
|
|
expect(await getExploreUrl(args)).not.toMatch(/__expr__/g);
|
|
});
|
|
it('should interpolate queries with variables in a non-mixed datasource scenario', async () => {
|
|
// this is not actually valid (see root and query DS being different) but it will test the root DS mock was called
|
|
const nonMixedArgs = {
|
|
queries: [{ refId: 'A', expr: 'query1', datasource: { type: 'prom', uid: 'ds2' } }],
|
|
dsRef: {
|
|
uid: 'ds1',
|
|
meta: { mixed: false },
|
|
},
|
|
timeRange: { from: dateTime(), to: dateTime(), raw: { from: 'now-1h', to: 'now' } },
|
|
scopedVars: {},
|
|
};
|
|
expect(await getExploreUrl(nonMixedArgs)).toMatch(/replaced%20testDs2%20prom/g);
|
|
expect(interpolateMockLoki).not.toBeCalled();
|
|
expect(interpolateMockProm).toBeCalled();
|
|
});
|
|
it('should interpolate queries with variables in a mixed datasource scenario', async () => {
|
|
const nonMixedArgs = {
|
|
queries: [
|
|
{ refId: 'A', expr: 'query1', datasource: { type: 'loki', uid: 'ds1' } },
|
|
{ refId: 'B', expr: 'query2', datasource: { type: 'prom', uid: 'ds2' } },
|
|
],
|
|
dsRef: {
|
|
uid: 'dsMixed',
|
|
meta: { mixed: true },
|
|
},
|
|
timeRange: { from: dateTime(), to: dateTime(), raw: { from: 'now-1h', to: 'now' } },
|
|
scopedVars: {},
|
|
};
|
|
const url = await getExploreUrl(nonMixedArgs);
|
|
expect(url).toMatch(/replaced%20testDs%20loki/g);
|
|
expect(url).toMatch(/replaced%20testDs2%20prom/g);
|
|
expect(interpolateMockLoki).toBeCalled();
|
|
expect(interpolateMockProm).toBeCalled();
|
|
});
|
|
});
|
|
|
|
describe('updateHistory()', () => {
|
|
const datasourceId = 'myDatasource';
|
|
const key = `grafana.explore.history.${datasourceId}`;
|
|
|
|
beforeEach(() => {
|
|
store.delete(key);
|
|
expect(store.exists(key)).toBeFalsy();
|
|
});
|
|
|
|
test('should save history item to localStorage', () => {
|
|
const expected = [
|
|
{
|
|
query: { refId: '1', expr: 'metric' },
|
|
},
|
|
];
|
|
expect(updateHistory([], datasourceId, [{ refId: '1', expr: 'metric' }])).toMatchObject(expected);
|
|
expect(store.exists(key)).toBeTruthy();
|
|
expect(store.getObject(key)).toMatchObject(expected);
|
|
});
|
|
});
|
|
|
|
describe('hasNonEmptyQuery', () => {
|
|
test('should return true if one query is non-empty', () => {
|
|
expect(hasNonEmptyQuery([{ refId: '1', key: '2', context: 'explore', expr: 'foo' }])).toBeTruthy();
|
|
});
|
|
|
|
test('should return false if query is empty', () => {
|
|
expect(hasNonEmptyQuery([{ refId: '1', key: '2', context: 'panel', datasource: { uid: 'some-ds' } }])).toBeFalsy();
|
|
});
|
|
|
|
test('should return false if no queries exist', () => {
|
|
expect(hasNonEmptyQuery([])).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
describe('getTimeRange', () => {
|
|
describe('should flip from and to when from is after to', () => {
|
|
const rawRange = {
|
|
from: 'now',
|
|
to: 'now-6h',
|
|
};
|
|
|
|
const range = getTimeRange('utc', rawRange, 0);
|
|
|
|
expect(range.from.isBefore(range.to)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('refreshIntervalToSortOrder', () => {
|
|
describe('when called with live option', () => {
|
|
it('then it should return ascending', () => {
|
|
const result = refreshIntervalToSortOrder(RefreshPicker.liveOption.value);
|
|
|
|
expect(result).toBe(LogsSortOrder.Ascending);
|
|
});
|
|
});
|
|
|
|
describe('when called with off option', () => {
|
|
it('then it should return descending', () => {
|
|
const result = refreshIntervalToSortOrder(RefreshPicker.offOption.value);
|
|
|
|
expect(result).toBe(LogsSortOrder.Descending);
|
|
});
|
|
});
|
|
|
|
describe('when called with 5s option', () => {
|
|
it('then it should return descending', () => {
|
|
const result = refreshIntervalToSortOrder('5s');
|
|
|
|
expect(result).toBe(LogsSortOrder.Descending);
|
|
});
|
|
});
|
|
|
|
describe('when called with undefined', () => {
|
|
it('then it should return descending', () => {
|
|
const result = refreshIntervalToSortOrder(undefined);
|
|
|
|
expect(result).toBe(LogsSortOrder.Descending);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when buildQueryTransaction', () => {
|
|
it('it should calculate interval based on time range', () => {
|
|
const queries = [{ refId: 'A' }];
|
|
const queryOptions = { maxDataPoints: 1000, minInterval: '15s' };
|
|
const range = { from: dateTime().subtract(1, 'd'), to: dateTime(), raw: { from: '1h', to: '1h' } };
|
|
const transaction = buildQueryTransaction('left', queries, queryOptions, range, false);
|
|
expect(transaction.request.intervalMs).toEqual(60000);
|
|
});
|
|
it('it should calculate interval taking minInterval into account', () => {
|
|
const queries = [{ refId: 'A' }];
|
|
const queryOptions = { maxDataPoints: 1000, minInterval: '15s' };
|
|
const range = { from: dateTime().subtract(1, 'm'), to: dateTime(), raw: { from: '1h', to: '1h' } };
|
|
const transaction = buildQueryTransaction('left', queries, queryOptions, range, false);
|
|
expect(transaction.request.intervalMs).toEqual(15000);
|
|
});
|
|
it('it should calculate interval taking maxDataPoints into account', () => {
|
|
const queries = [{ refId: 'A' }];
|
|
const queryOptions = { maxDataPoints: 10, minInterval: '15s' };
|
|
const range = { from: dateTime().subtract(1, 'd'), to: dateTime(), raw: { from: '1h', to: '1h' } };
|
|
const transaction = buildQueryTransaction('left', queries, queryOptions, range, false);
|
|
expect(transaction.request.interval).toEqual('2h');
|
|
});
|
|
});
|
|
|
|
describe('generateEmptyQuery', () => {
|
|
it('should generate query with dataSourceOverride and without queries', async () => {
|
|
const query = await generateEmptyQuery([], 1, { type: 'loki', uid: 'ds1' });
|
|
|
|
expect(query.datasource?.uid).toBe('ds1');
|
|
expect(query.datasource?.type).toBe('loki');
|
|
expect(query.refId).toBe('A');
|
|
});
|
|
it('should generate query without dataSourceOverride and with queries', async () => {
|
|
const query = await generateEmptyQuery(
|
|
[
|
|
{
|
|
datasource: { type: 'loki', uid: 'ds1' },
|
|
refId: 'A',
|
|
},
|
|
],
|
|
1
|
|
);
|
|
|
|
expect(query.datasource?.uid).toBe('ds1');
|
|
expect(query.datasource?.type).toBe('loki');
|
|
expect(query.refId).toBe('B');
|
|
});
|
|
|
|
it('should generate a query with a unique refId', async () => {
|
|
const query = await generateEmptyQuery([{ refId: 'A' }], 2);
|
|
|
|
expect(query.refId).not.toBe('A');
|
|
});
|
|
});
|