explore.test.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. import {
  2. DEFAULT_RANGE,
  3. serializeStateToUrlParam,
  4. parseUrlState,
  5. updateHistory,
  6. clearHistory,
  7. hasNonEmptyQuery,
  8. getValueWithRefId,
  9. getFirstQueryErrorWithoutRefId,
  10. getRefIds,
  11. refreshIntervalToSortOrder,
  12. SortOrder,
  13. sortLogsResult,
  14. buildQueryTransaction,
  15. } from './explore';
  16. import { ExploreUrlState, ExploreMode } from 'app/types/explore';
  17. import store from 'app/core/store';
  18. import { LogsDedupStrategy, LogsModel, LogLevel, dateTime } from '@grafana/data';
  19. import { DataQueryError } from '@grafana/ui';
  20. import { liveOption, offOption } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker';
  21. const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
  22. datasource: null,
  23. queries: [],
  24. range: DEFAULT_RANGE,
  25. mode: ExploreMode.Metrics,
  26. ui: {
  27. showingGraph: true,
  28. showingTable: true,
  29. showingLogs: true,
  30. dedupStrategy: LogsDedupStrategy.none,
  31. },
  32. originPanelId: undefined,
  33. };
  34. describe('state functions', () => {
  35. describe('parseUrlState', () => {
  36. it('returns default state on empty string', () => {
  37. expect(parseUrlState('')).toMatchObject({
  38. datasource: null,
  39. queries: [],
  40. range: DEFAULT_RANGE,
  41. });
  42. });
  43. it('returns a valid Explore state from URL parameter', () => {
  44. const paramValue =
  45. '%7B"datasource":"Local","queries":%5B%7B"expr":"metric"%7D%5D,"range":%7B"from":"now-1h","to":"now"%7D%7D';
  46. expect(parseUrlState(paramValue)).toMatchObject({
  47. datasource: 'Local',
  48. queries: [{ expr: 'metric' }],
  49. range: {
  50. from: 'now-1h',
  51. to: 'now',
  52. },
  53. });
  54. });
  55. it('returns a valid Explore state from a compact URL parameter', () => {
  56. const paramValue = '%5B"now-1h","now","Local","5m",%7B"expr":"metric"%7D,"ui"%5D';
  57. expect(parseUrlState(paramValue)).toMatchObject({
  58. datasource: 'Local',
  59. queries: [{ expr: 'metric' }],
  60. range: {
  61. from: 'now-1h',
  62. to: 'now',
  63. },
  64. });
  65. });
  66. });
  67. describe('serializeStateToUrlParam', () => {
  68. it('returns url parameter value for a state object', () => {
  69. const state = {
  70. ...DEFAULT_EXPLORE_STATE,
  71. datasource: 'foo',
  72. queries: [
  73. {
  74. expr: 'metric{test="a/b"}',
  75. },
  76. {
  77. expr: 'super{foo="x/z"}',
  78. },
  79. ],
  80. range: {
  81. from: 'now-5h',
  82. to: 'now',
  83. },
  84. };
  85. expect(serializeStateToUrlParam(state)).toBe(
  86. '{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
  87. '{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"},' +
  88. '"mode":"Metrics",' +
  89. '"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true,"dedupStrategy":"none"}}'
  90. );
  91. });
  92. it('returns url parameter value for a state object', () => {
  93. const state = {
  94. ...DEFAULT_EXPLORE_STATE,
  95. datasource: 'foo',
  96. queries: [
  97. {
  98. expr: 'metric{test="a/b"}',
  99. },
  100. {
  101. expr: 'super{foo="x/z"}',
  102. },
  103. ],
  104. range: {
  105. from: 'now-5h',
  106. to: 'now',
  107. },
  108. };
  109. expect(serializeStateToUrlParam(state, true)).toBe(
  110. '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"},{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]'
  111. );
  112. });
  113. });
  114. describe('interplay', () => {
  115. it('can parse the serialized state into the original state', () => {
  116. const state = {
  117. ...DEFAULT_EXPLORE_STATE,
  118. datasource: 'foo',
  119. queries: [
  120. {
  121. expr: 'metric{test="a/b"}',
  122. },
  123. {
  124. expr: 'super{foo="x/z"}',
  125. },
  126. ],
  127. range: {
  128. from: 'now - 5h',
  129. to: 'now',
  130. },
  131. };
  132. const serialized = serializeStateToUrlParam(state);
  133. const parsed = parseUrlState(serialized);
  134. expect(state).toMatchObject(parsed);
  135. });
  136. it('can parse the compact serialized state into the original state', () => {
  137. const state = {
  138. ...DEFAULT_EXPLORE_STATE,
  139. datasource: 'foo',
  140. queries: [
  141. {
  142. expr: 'metric{test="a/b"}',
  143. },
  144. {
  145. expr: 'super{foo="x/z"}',
  146. },
  147. ],
  148. range: {
  149. from: 'now - 5h',
  150. to: 'now',
  151. },
  152. };
  153. const serialized = serializeStateToUrlParam(state, true);
  154. const parsed = parseUrlState(serialized);
  155. expect(state).toMatchObject(parsed);
  156. });
  157. });
  158. });
  159. describe('updateHistory()', () => {
  160. const datasourceId = 'myDatasource';
  161. const key = `grafana.explore.history.${datasourceId}`;
  162. beforeEach(() => {
  163. clearHistory(datasourceId);
  164. expect(store.exists(key)).toBeFalsy();
  165. });
  166. test('should save history item to localStorage', () => {
  167. const expected = [
  168. {
  169. query: { refId: '1', expr: 'metric' },
  170. },
  171. ];
  172. expect(updateHistory([], datasourceId, [{ refId: '1', expr: 'metric' }])).toMatchObject(expected);
  173. expect(store.exists(key)).toBeTruthy();
  174. expect(store.getObject(key)).toMatchObject(expected);
  175. });
  176. });
  177. describe('hasNonEmptyQuery', () => {
  178. test('should return true if one query is non-empty', () => {
  179. expect(hasNonEmptyQuery([{ refId: '1', key: '2', context: 'explore', expr: 'foo' }])).toBeTruthy();
  180. });
  181. test('should return false if query is empty', () => {
  182. expect(hasNonEmptyQuery([{ refId: '1', key: '2', context: 'panel' }])).toBeFalsy();
  183. });
  184. test('should return false if no queries exist', () => {
  185. expect(hasNonEmptyQuery([])).toBeFalsy();
  186. });
  187. });
  188. describe('hasRefId', () => {
  189. describe('when called with a null value', () => {
  190. it('then it should return null', () => {
  191. const input: any = null;
  192. const result = getValueWithRefId(input);
  193. expect(result).toBeNull();
  194. });
  195. });
  196. describe('when called with a non object value', () => {
  197. it('then it should return null', () => {
  198. const input = 123;
  199. const result = getValueWithRefId(input);
  200. expect(result).toBeNull();
  201. });
  202. });
  203. describe('when called with an object that has refId', () => {
  204. it('then it should return the object', () => {
  205. const input = { refId: 'A' };
  206. const result = getValueWithRefId(input);
  207. expect(result).toBe(input);
  208. });
  209. });
  210. describe('when called with an array that has refId', () => {
  211. it('then it should return the object', () => {
  212. const input = [123, null, {}, { refId: 'A' }];
  213. const result = getValueWithRefId(input);
  214. expect(result).toBe(input[3]);
  215. });
  216. });
  217. describe('when called with an object that has refId somewhere in the object tree', () => {
  218. it('then it should return the object', () => {
  219. const input: any = { data: [123, null, {}, { series: [123, null, {}, { refId: 'A' }] }] };
  220. const result = getValueWithRefId(input);
  221. expect(result).toBe(input.data[3].series[3]);
  222. });
  223. });
  224. });
  225. describe('getFirstQueryErrorWithoutRefId', () => {
  226. describe('when called with a null value', () => {
  227. it('then it should return null', () => {
  228. const errors: DataQueryError[] = null;
  229. const result = getFirstQueryErrorWithoutRefId(errors);
  230. expect(result).toBeNull();
  231. });
  232. });
  233. describe('when called with an array with only refIds', () => {
  234. it('then it should return undefined', () => {
  235. const errors: DataQueryError[] = [{ refId: 'A' }, { refId: 'B' }];
  236. const result = getFirstQueryErrorWithoutRefId(errors);
  237. expect(result).toBeUndefined();
  238. });
  239. });
  240. describe('when called with an array with and without refIds', () => {
  241. it('then it should return undefined', () => {
  242. const errors: DataQueryError[] = [
  243. { refId: 'A' },
  244. { message: 'A message' },
  245. { refId: 'B' },
  246. { message: 'B message' },
  247. ];
  248. const result = getFirstQueryErrorWithoutRefId(errors);
  249. expect(result).toBe(errors[1]);
  250. });
  251. });
  252. });
  253. describe('getRefIds', () => {
  254. describe('when called with a null value', () => {
  255. it('then it should return empty array', () => {
  256. const input: any = null;
  257. const result = getRefIds(input);
  258. expect(result).toEqual([]);
  259. });
  260. });
  261. describe('when called with a non object value', () => {
  262. it('then it should return empty array', () => {
  263. const input = 123;
  264. const result = getRefIds(input);
  265. expect(result).toEqual([]);
  266. });
  267. });
  268. describe('when called with an object that has refId', () => {
  269. it('then it should return an array with that refId', () => {
  270. const input = { refId: 'A' };
  271. const result = getRefIds(input);
  272. expect(result).toEqual(['A']);
  273. });
  274. });
  275. describe('when called with an array that has refIds', () => {
  276. it('then it should return an array with unique refIds', () => {
  277. const input = [123, null, {}, { refId: 'A' }, { refId: 'A' }, { refId: 'B' }];
  278. const result = getRefIds(input);
  279. expect(result).toEqual(['A', 'B']);
  280. });
  281. });
  282. describe('when called with an object that has refIds somewhere in the object tree', () => {
  283. it('then it should return return an array with unique refIds', () => {
  284. const input: any = {
  285. data: [
  286. 123,
  287. null,
  288. { refId: 'B', series: [{ refId: 'X' }] },
  289. { refId: 'B' },
  290. {},
  291. { series: [123, null, {}, { refId: 'A' }] },
  292. ],
  293. };
  294. const result = getRefIds(input);
  295. expect(result).toEqual(['B', 'X', 'A']);
  296. });
  297. });
  298. });
  299. describe('refreshIntervalToSortOrder', () => {
  300. describe('when called with live option', () => {
  301. it('then it should return ascending', () => {
  302. const result = refreshIntervalToSortOrder(liveOption.value);
  303. expect(result).toBe(SortOrder.Ascending);
  304. });
  305. });
  306. describe('when called with off option', () => {
  307. it('then it should return descending', () => {
  308. const result = refreshIntervalToSortOrder(offOption.value);
  309. expect(result).toBe(SortOrder.Descending);
  310. });
  311. });
  312. describe('when called with 5s option', () => {
  313. it('then it should return descending', () => {
  314. const result = refreshIntervalToSortOrder('5s');
  315. expect(result).toBe(SortOrder.Descending);
  316. });
  317. });
  318. describe('when called with undefined', () => {
  319. it('then it should return descending', () => {
  320. const result = refreshIntervalToSortOrder(undefined);
  321. expect(result).toBe(SortOrder.Descending);
  322. });
  323. });
  324. });
  325. describe('sortLogsResult', () => {
  326. const firstRow = {
  327. timestamp: '2019-01-01T21:00:0.0000000Z',
  328. entry: '',
  329. hasAnsi: false,
  330. labels: {},
  331. logLevel: LogLevel.info,
  332. raw: '',
  333. timeEpochMs: 0,
  334. timeFromNow: '',
  335. timeLocal: '',
  336. timeUtc: '',
  337. };
  338. const sameAsFirstRow = firstRow;
  339. const secondRow = {
  340. timestamp: '2019-01-01T22:00:0.0000000Z',
  341. entry: '',
  342. hasAnsi: false,
  343. labels: {},
  344. logLevel: LogLevel.info,
  345. raw: '',
  346. timeEpochMs: 0,
  347. timeFromNow: '',
  348. timeLocal: '',
  349. timeUtc: '',
  350. };
  351. describe('when called with SortOrder.Descending', () => {
  352. it('then it should sort descending', () => {
  353. const logsResult: LogsModel = {
  354. rows: [firstRow, sameAsFirstRow, secondRow],
  355. hasUniqueLabels: false,
  356. };
  357. const result = sortLogsResult(logsResult, SortOrder.Descending);
  358. expect(result).toEqual({
  359. rows: [secondRow, firstRow, sameAsFirstRow],
  360. hasUniqueLabels: false,
  361. });
  362. });
  363. });
  364. describe('when called with SortOrder.Ascending', () => {
  365. it('then it should sort ascending', () => {
  366. const logsResult: LogsModel = {
  367. rows: [secondRow, firstRow, sameAsFirstRow],
  368. hasUniqueLabels: false,
  369. };
  370. const result = sortLogsResult(logsResult, SortOrder.Ascending);
  371. expect(result).toEqual({
  372. rows: [firstRow, sameAsFirstRow, secondRow],
  373. hasUniqueLabels: false,
  374. });
  375. });
  376. });
  377. describe('when buildQueryTransaction', () => {
  378. it('it should calculate interval based on time range', () => {
  379. const queries = [{ refId: 'A' }];
  380. const queryOptions = { maxDataPoints: 1000, minInterval: '15s' };
  381. const range = { from: dateTime().subtract(1, 'd'), to: dateTime(), raw: { from: '1h', to: '1h' } };
  382. const transaction = buildQueryTransaction(queries, queryOptions, range, false);
  383. expect(transaction.request.intervalMs).toEqual(60000);
  384. });
  385. it('it should calculate interval taking minInterval into account', () => {
  386. const queries = [{ refId: 'A' }];
  387. const queryOptions = { maxDataPoints: 1000, minInterval: '15s' };
  388. const range = { from: dateTime().subtract(1, 'm'), to: dateTime(), raw: { from: '1h', to: '1h' } };
  389. const transaction = buildQueryTransaction(queries, queryOptions, range, false);
  390. expect(transaction.request.intervalMs).toEqual(15000);
  391. });
  392. it('it should calculate interval taking maxDataPoints into account', () => {
  393. const queries = [{ refId: 'A' }];
  394. const queryOptions = { maxDataPoints: 10, minInterval: '15s' };
  395. const range = { from: dateTime().subtract(1, 'd'), to: dateTime(), raw: { from: '1h', to: '1h' } };
  396. const transaction = buildQueryTransaction(queries, queryOptions, range, false);
  397. expect(transaction.request.interval).toEqual('2h');
  398. });
  399. });
  400. });