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/core/utils/explore.test.ts

237 lines
7.4 KiB

import { dateTime, ExploreUrlState, LogsSortOrder } from '@grafana/data';
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
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 datasourceSrv = new DatasourceSrvMock(defaultDs, {
'generate empty query': new MockDataSourceApi('generateEmptyQuery'),
ds1: {
name: 'testDs',
type: 'loki',
} 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', () => {
const args = {
panel: {
getSavedId: () => 1,
targets: [
{ refId: 'A', expr: 'query1', legendFormat: 'legendFormat1' },
{ refId: 'B', expr: 'query2', datasource: { type: '__expr__', uid: '__expr__' } },
],
},
datasourceSrv: {
get() {
return {
getRef: jest.fn(),
};
},
getDataSourceById: jest.fn(),
},
timeSrv: {
timeRange: () => ({ raw: { from: 'now-1h', to: 'now' } }),
},
} as unknown as GetExploreUrlArguments;
it('should omit legendFormat in explore url', () => {
expect(getExploreUrl(args).then((data) => expect(data).not.toMatch(/legendFormat1/g)));
});
it('should omit expression target in explore url', () => {
expect(getExploreUrl(args).then((data) => expect(data).not.toMatch(/__expr__/g)));
});
});
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');
});
});