Files
grafana/public/app/core/utils/explore.test.ts

359 lines
9.8 KiB
TypeScript
Raw Normal View History

2018-11-22 11:02:53 +01:00
import {
DEFAULT_RANGE,
serializeStateToUrlParam,
parseUrlState,
updateHistory,
clearHistory,
hasNonEmptyQuery,
instanceOfDataQueryError,
getValueWithRefId,
getFirstQueryErrorWithoutRefId,
getRefIds,
2018-11-22 11:02:53 +01:00
} from './explore';
import { ExploreUrlState, ExploreMode } from 'app/types/explore';
2018-11-22 11:02:53 +01:00
import store from 'app/core/store';
import { LogsDedupStrategy } from '@grafana/data';
import { DataQueryError } from '@grafana/ui';
2018-09-28 17:39:53 +02:00
2019-01-12 23:22:28 +01:00
const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
2018-09-28 17:39:53 +02:00
datasource: null,
2019-01-12 23:22:28 +01:00
queries: [],
2018-09-28 17:39:53 +02:00
range: DEFAULT_RANGE,
mode: ExploreMode.Metrics,
2019-01-31 19:38:49 +01:00
ui: {
showingGraph: true,
showingTable: true,
showingLogs: true,
2019-02-07 17:46:33 +01:00
dedupStrategy: LogsDedupStrategy.none,
},
2018-09-28 17:39:53 +02:00
};
2018-10-01 12:56:26 +02:00
describe('state functions', () => {
2018-09-28 17:39:53 +02:00
describe('parseUrlState', () => {
it('returns default state on empty string', () => {
expect(parseUrlState('')).toMatchObject({
datasource: null,
2018-11-21 16:28:30 +01:00
queries: [],
2018-09-28 17:39:53 +02:00
range: DEFAULT_RANGE,
});
});
it('returns a valid Explore state from URL parameter', () => {
const paramValue =
2018-11-21 16:28:30 +01:00
'%7B"datasource":"Local","queries":%5B%7B"expr":"metric"%7D%5D,"range":%7B"from":"now-1h","to":"now"%7D%7D';
expect(parseUrlState(paramValue)).toMatchObject({
datasource: 'Local',
2018-11-21 16:28:30 +01:00
queries: [{ expr: 'metric' }],
range: {
from: 'now-1h',
to: 'now',
},
});
});
it('returns a valid Explore state from a compact URL parameter', () => {
Explore & Dashboard: New Refresh picker (#16505) * Added RefreshButton * Added RefreshSelect * Added RefreshSelectButton * Added RefreshPicker * Removed the magic string Paused * Minor style changes and using Off instead of Pause * Added HeadlessSelect * Added HeadlessSelect story * Added SelectButton * Removed RefreshSelectButton * Added TimePicker and moved ClickOutsideWrapper to ui/components * Added TimePickerPopOver * Added react-calendar * Missed yarn lock file * Added inputs to popover * Added TimePicker and RefreshPicker to DashNav * Moved TimePicker and RefreshPicker to app/core * Added react-calendar to app and removed from ui/components * Fixed PopOver onClick * Moved everything back to ui components because of typings problems * Exporing RefreshPicker and TimePicker * Added Apply and inputs * Added typings * Added TimePickerInput and logic * Fixed parsing of string to Moments * Fixed range string * Styling and connecting the calendars and inputs * Changed Calendar styling * Added backward forward and zoom * Fixed responsive styles * Moved TimePicker and RefreshPicker into app core * Renamed menuIsOpen to isOpen * Changed from className={} to className="" * Moved Popover to TimePickerOptionGroup * Renamed all PopOver to Popover * Renamed popOver to popover and some minor refactorings * Renamed files with git mv * Added ButtonSelect and refactored RefreshPicker * Refactored TimePicker to use new ButtonSelect * Removed HeadlessSelect as suggested * fix: Fix typings and misc errors after rebase * wip: Enable time picker on dashboard and add tooltip * Merge branch 'master' into hugoh/new-timepicker-and-unified-component # Conflicts: # packages/grafana-ui/package.json # packages/grafana-ui/src/components/Input/Input.test.tsx # packages/grafana-ui/src/components/Input/Input.tsx # packages/grafana-ui/src/utils/validate.ts # public/app/features/dashboard/panel_editor/QueryOptions.tsx # yarn.lock * fix: Snapshot update * Move TimePicker default options into the TimePicker as statics, pass the tooltipContent down the line when wanted and wrap the button in a tooltip element * fix: Override internal state prop if we provide one in a prop * Updated snapshots * Let dashnav control refreshPicker state * feat: Add a stringToMs function * wip: RefreshPicker * wip: Move RefreshPicker to @grafana/ui * wip: Move TimePicker to @grafana/ui * wip: Remove comments * wip: Add refreshPicker to explore * wip: Use default intervals if the prop is missing * wip: Nicer way of setting defaults * fix: Control the select component * wip: Add onMoveForward/onMoveBack * Remove code related to the new time picker and refresh picker from dashnav * Fix: Typings after merge * chore: Minor fix after merge * chore: Remove _.map usage * chore: Moved refresh-picker logic out of the refresh picker since it will work a little differently in explore and dashboards until we have replaced the TimeSrv * feat: Add an Interval component to @grafana/ui * chore: Remove intervalId from redux state and move setInterval logic from ExploreToolbar to its own Interval component * feat: Add refreshInterval to Explore's URL state * feat: Pick up refreshInterval from url on page load * fix: Set default refreshInterval when no value can be retained from URL * fix: Update test initial state with refreshInterval * fix: Handle URLs before RefreshPicker * fix: Move RefreshInterval to url position 3 since the segments can take multiple positions * fix: A better way of detecting urls without RefreshInterval in Explore * chore: Some Explore typings * fix: Attach refresh picker to interval picker * chore: Sass fix for refresh button border radius * fix: Remove refreshInterval from URL * fix: Intervals now start when previous interval is finished * fix: Use clearTimeout instead of clearInterval * fix: Make sure there's a delay set before adding a timeout when we have slow explore queries * wip: Add refresh picker to dashboard * feat: Add util for removing keys with empty values * feat: RefreshPicker in dashboards and tmp rem out old RefreshPicker * fix: Remove the jumpy:ness in the refreshpicker * Changed placement and made it hide when your in dashboard settings * chore: Move logic related to refresh picker out of DashNav to its own component * feat: Add tooltip to refreshpicker * fix: Fix bug with refreshpicker not updating when setting to 'off' * fix: Make it possible to override refresh intervals using the dashboard intervals * chore: Change name of Interval to SetInterval to align with ecmascripts naming since its basically the same but declarative and async * fix: Use default intervals when auto refresh is empty in dashboard settings * fix: Hide time/interval picker when hidden is true on the model, such as on the home dashboard * fix: Interval picker will have to handle location changes since timeSrv wont * RefreshPicker: Refactoring refresh picker * RefreshPicker: minor refactoring
2019-04-16 09:15:23 +02:00
const paramValue = '%5B"now-1h","now","Local","5m",%7B"expr":"metric"%7D,"ui"%5D';
expect(parseUrlState(paramValue)).toMatchObject({
datasource: 'Local',
2018-11-21 16:28:30 +01:00
queries: [{ expr: 'metric' }],
range: {
from: 'now-1h',
to: 'now',
},
});
});
2018-09-28 17:39:53 +02:00
});
2018-09-28 17:39:53 +02:00
describe('serializeStateToUrlParam', () => {
it('returns url parameter value for a state object', () => {
const state = {
...DEFAULT_EXPLORE_STATE,
2019-01-12 23:22:28 +01:00
datasource: 'foo',
queries: [
2018-09-28 17:39:53 +02:00
{
expr: 'metric{test="a/b"}',
2018-09-28 17:39:53 +02:00
},
{
expr: 'super{foo="x/z"}',
2018-09-28 17:39:53 +02:00
},
],
2019-01-12 23:22:28 +01:00
range: {
from: 'now-5h',
to: 'now',
},
2018-09-28 17:39:53 +02:00
};
2019-01-31 19:38:49 +01:00
2018-09-28 17:39:53 +02:00
expect(serializeStateToUrlParam(state)).toBe(
2018-11-21 16:28:30 +01:00
'{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
2019-01-31 19:38:49 +01:00
'{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"},' +
'"mode":"Metrics",' +
2019-02-07 17:46:33 +01:00
'"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true,"dedupStrategy":"none"}}'
);
});
it('returns url parameter value for a state object', () => {
const state = {
...DEFAULT_EXPLORE_STATE,
2019-01-12 23:22:28 +01:00
datasource: 'foo',
queries: [
{
expr: 'metric{test="a/b"}',
},
{
expr: 'super{foo="x/z"}',
},
],
2019-01-12 23:22:28 +01:00
range: {
from: 'now-5h',
to: 'now',
},
};
expect(serializeStateToUrlParam(state, true)).toBe(
'["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"},{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]'
2018-09-28 17:39:53 +02:00
);
});
});
2018-09-28 17:39:53 +02:00
describe('interplay', () => {
it('can parse the serialized state into the original state', () => {
const state = {
...DEFAULT_EXPLORE_STATE,
2019-01-12 23:22:28 +01:00
datasource: 'foo',
queries: [
2018-09-28 17:39:53 +02:00
{
expr: 'metric{test="a/b"}',
2018-09-28 17:39:53 +02:00
},
{
expr: 'super{foo="x/z"}',
2018-09-28 17:39:53 +02:00
},
],
2019-01-12 23:22:28 +01:00
range: {
from: 'now - 5h',
to: 'now',
},
2018-09-28 17:39:53 +02:00
};
const serialized = serializeStateToUrlParam(state);
const parsed = parseUrlState(serialized);
expect(state).toMatchObject(parsed);
});
2018-09-28 17:39:53 +02:00
it('can parse the compact serialized state into the original state', () => {
const state = {
...DEFAULT_EXPLORE_STATE,
datasource: 'foo',
queries: [
{
expr: 'metric{test="a/b"}',
},
{
expr: 'super{foo="x/z"}',
},
],
range: {
from: 'now - 5h',
to: 'now',
},
};
const serialized = serializeStateToUrlParam(state, true);
const parsed = parseUrlState(serialized);
2019-01-12 23:22:28 +01:00
expect(state).toMatchObject(parsed);
2018-09-28 17:39:53 +02:00
});
});
});
2018-11-22 11:02:53 +01:00
describe('updateHistory()', () => {
const datasourceId = 'myDatasource';
const key = `grafana.explore.history.${datasourceId}`;
beforeEach(() => {
clearHistory(datasourceId);
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();
2018-11-22 11:02:53 +01:00
});
test('should return false if query is empty', () => {
expect(hasNonEmptyQuery([{ refId: '1', key: '2', context: 'panel' }])).toBeFalsy();
2018-11-22 11:02:53 +01:00
});
test('should return false if no queries exist', () => {
expect(hasNonEmptyQuery([])).toBeFalsy();
});
});
describe('instanceOfDataQueryError', () => {
describe('when called with a DataQueryError', () => {
it('then it should return true', () => {
const error: DataQueryError = {
message: 'A message',
status: '200',
statusText: 'Ok',
};
const result = instanceOfDataQueryError(error);
expect(result).toBe(true);
});
});
describe('when called with a non DataQueryError', () => {
it('then it should return false', () => {
const error = {};
const result = instanceOfDataQueryError(error);
expect(result).toBe(false);
});
});
});
describe('hasRefId', () => {
describe('when called with a null value', () => {
it('then it should return null', () => {
const input = null;
const result = getValueWithRefId(input);
expect(result).toBeNull();
});
});
describe('when called with a non object value', () => {
it('then it should return null', () => {
const input = 123;
const result = getValueWithRefId(input);
expect(result).toBeNull();
});
});
describe('when called with an object that has refId', () => {
it('then it should return the object', () => {
const input = { refId: 'A' };
const result = getValueWithRefId(input);
expect(result).toBe(input);
});
});
describe('when called with an array that has refId', () => {
it('then it should return the object', () => {
const input = [123, null, {}, { refId: 'A' }];
const result = getValueWithRefId(input);
expect(result).toBe(input[3]);
});
});
describe('when called with an object that has refId somewhere in the object tree', () => {
it('then it should return the object', () => {
const input: any = { data: [123, null, {}, { series: [123, null, {}, { refId: 'A' }] }] };
const result = getValueWithRefId(input);
expect(result).toBe(input.data[3].series[3]);
});
});
});
describe('getFirstQueryErrorWithoutRefId', () => {
describe('when called with a null value', () => {
it('then it should return null', () => {
const errors: DataQueryError[] = null;
const result = getFirstQueryErrorWithoutRefId(errors);
expect(result).toBeNull();
});
});
describe('when called with an array with only refIds', () => {
it('then it should return undefined', () => {
const errors: DataQueryError[] = [{ refId: 'A' }, { refId: 'B' }];
const result = getFirstQueryErrorWithoutRefId(errors);
expect(result).toBeUndefined();
});
});
describe('when called with an array with and without refIds', () => {
it('then it should return undefined', () => {
const errors: DataQueryError[] = [
{ refId: 'A' },
{ message: 'A message' },
{ refId: 'B' },
{ message: 'B message' },
];
const result = getFirstQueryErrorWithoutRefId(errors);
expect(result).toBe(errors[1]);
});
});
});
describe('getRefIds', () => {
describe('when called with a null value', () => {
it('then it should return empty array', () => {
const input = null;
const result = getRefIds(input);
expect(result).toEqual([]);
});
});
describe('when called with a non object value', () => {
it('then it should return empty array', () => {
const input = 123;
const result = getRefIds(input);
expect(result).toEqual([]);
});
});
describe('when called with an object that has refId', () => {
it('then it should return an array with that refId', () => {
const input = { refId: 'A' };
const result = getRefIds(input);
expect(result).toEqual(['A']);
});
});
describe('when called with an array that has refIds', () => {
it('then it should return an array with unique refIds', () => {
const input = [123, null, {}, { refId: 'A' }, { refId: 'A' }, { refId: 'B' }];
const result = getRefIds(input);
expect(result).toEqual(['A', 'B']);
});
});
describe('when called with an object that has refIds somewhere in the object tree', () => {
it('then it should return return an array with unique refIds', () => {
const input: any = {
data: [
123,
null,
{ refId: 'B', series: [{ refId: 'X' }] },
{ refId: 'B' },
{},
{ series: [123, null, {}, { refId: 'A' }] },
],
};
const result = getRefIds(input);
expect(result).toEqual(['B', 'X', 'A']);
});
});
});