explore.test.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. import {
  2. DEFAULT_RANGE,
  3. serializeStateToUrlParam,
  4. parseUrlState,
  5. updateHistory,
  6. clearHistory,
  7. hasNonEmptyQuery,
  8. instanceOfDataQueryError,
  9. getValueWithRefId,
  10. getFirstQueryErrorWithoutRefId,
  11. getRefIds,
  12. refreshIntervalToSortOrder,
  13. SortOrder,
  14. sortLogsResult,
  15. } from './explore';
  16. import { ExploreUrlState, ExploreMode } from 'app/types/explore';
  17. import store from 'app/core/store';
  18. import { LogsDedupStrategy, LogsModel, LogLevel } 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('instanceOfDataQueryError', () => {
  189. describe('when called with a DataQueryError', () => {
  190. it('then it should return true', () => {
  191. const error: DataQueryError = {
  192. message: 'A message',
  193. status: '200',
  194. statusText: 'Ok',
  195. };
  196. const result = instanceOfDataQueryError(error);
  197. expect(result).toBe(true);
  198. });
  199. });
  200. describe('when called with a non DataQueryError', () => {
  201. it('then it should return false', () => {
  202. const error = {};
  203. const result = instanceOfDataQueryError(error);
  204. expect(result).toBe(false);
  205. });
  206. });
  207. });
  208. describe('hasRefId', () => {
  209. describe('when called with a null value', () => {
  210. it('then it should return null', () => {
  211. const input: any = null;
  212. const result = getValueWithRefId(input);
  213. expect(result).toBeNull();
  214. });
  215. });
  216. describe('when called with a non object value', () => {
  217. it('then it should return null', () => {
  218. const input = 123;
  219. const result = getValueWithRefId(input);
  220. expect(result).toBeNull();
  221. });
  222. });
  223. describe('when called with an object that has refId', () => {
  224. it('then it should return the object', () => {
  225. const input = { refId: 'A' };
  226. const result = getValueWithRefId(input);
  227. expect(result).toBe(input);
  228. });
  229. });
  230. describe('when called with an array that has refId', () => {
  231. it('then it should return the object', () => {
  232. const input = [123, null, {}, { refId: 'A' }];
  233. const result = getValueWithRefId(input);
  234. expect(result).toBe(input[3]);
  235. });
  236. });
  237. describe('when called with an object that has refId somewhere in the object tree', () => {
  238. it('then it should return the object', () => {
  239. const input: any = { data: [123, null, {}, { series: [123, null, {}, { refId: 'A' }] }] };
  240. const result = getValueWithRefId(input);
  241. expect(result).toBe(input.data[3].series[3]);
  242. });
  243. });
  244. });
  245. describe('getFirstQueryErrorWithoutRefId', () => {
  246. describe('when called with a null value', () => {
  247. it('then it should return null', () => {
  248. const errors: DataQueryError[] = null;
  249. const result = getFirstQueryErrorWithoutRefId(errors);
  250. expect(result).toBeNull();
  251. });
  252. });
  253. describe('when called with an array with only refIds', () => {
  254. it('then it should return undefined', () => {
  255. const errors: DataQueryError[] = [{ refId: 'A' }, { refId: 'B' }];
  256. const result = getFirstQueryErrorWithoutRefId(errors);
  257. expect(result).toBeUndefined();
  258. });
  259. });
  260. describe('when called with an array with and without refIds', () => {
  261. it('then it should return undefined', () => {
  262. const errors: DataQueryError[] = [
  263. { refId: 'A' },
  264. { message: 'A message' },
  265. { refId: 'B' },
  266. { message: 'B message' },
  267. ];
  268. const result = getFirstQueryErrorWithoutRefId(errors);
  269. expect(result).toBe(errors[1]);
  270. });
  271. });
  272. });
  273. describe('getRefIds', () => {
  274. describe('when called with a null value', () => {
  275. it('then it should return empty array', () => {
  276. const input: any = null;
  277. const result = getRefIds(input);
  278. expect(result).toEqual([]);
  279. });
  280. });
  281. describe('when called with a non object value', () => {
  282. it('then it should return empty array', () => {
  283. const input = 123;
  284. const result = getRefIds(input);
  285. expect(result).toEqual([]);
  286. });
  287. });
  288. describe('when called with an object that has refId', () => {
  289. it('then it should return an array with that refId', () => {
  290. const input = { refId: 'A' };
  291. const result = getRefIds(input);
  292. expect(result).toEqual(['A']);
  293. });
  294. });
  295. describe('when called with an array that has refIds', () => {
  296. it('then it should return an array with unique refIds', () => {
  297. const input = [123, null, {}, { refId: 'A' }, { refId: 'A' }, { refId: 'B' }];
  298. const result = getRefIds(input);
  299. expect(result).toEqual(['A', 'B']);
  300. });
  301. });
  302. describe('when called with an object that has refIds somewhere in the object tree', () => {
  303. it('then it should return return an array with unique refIds', () => {
  304. const input: any = {
  305. data: [
  306. 123,
  307. null,
  308. { refId: 'B', series: [{ refId: 'X' }] },
  309. { refId: 'B' },
  310. {},
  311. { series: [123, null, {}, { refId: 'A' }] },
  312. ],
  313. };
  314. const result = getRefIds(input);
  315. expect(result).toEqual(['B', 'X', 'A']);
  316. });
  317. });
  318. });
  319. describe('refreshIntervalToSortOrder', () => {
  320. describe('when called with live option', () => {
  321. it('then it should return ascending', () => {
  322. const result = refreshIntervalToSortOrder(liveOption.value);
  323. expect(result).toBe(SortOrder.Ascending);
  324. });
  325. });
  326. describe('when called with off option', () => {
  327. it('then it should return descending', () => {
  328. const result = refreshIntervalToSortOrder(offOption.value);
  329. expect(result).toBe(SortOrder.Descending);
  330. });
  331. });
  332. describe('when called with 5s option', () => {
  333. it('then it should return descending', () => {
  334. const result = refreshIntervalToSortOrder('5s');
  335. expect(result).toBe(SortOrder.Descending);
  336. });
  337. });
  338. describe('when called with undefined', () => {
  339. it('then it should return descending', () => {
  340. const result = refreshIntervalToSortOrder(undefined);
  341. expect(result).toBe(SortOrder.Descending);
  342. });
  343. });
  344. });
  345. describe('sortLogsResult', () => {
  346. const firstRow = {
  347. timestamp: '2019-01-01T21:00:0.0000000Z',
  348. entry: '',
  349. hasAnsi: false,
  350. labels: {},
  351. logLevel: LogLevel.info,
  352. raw: '',
  353. timeEpochMs: 0,
  354. timeFromNow: '',
  355. timeLocal: '',
  356. timeUtc: '',
  357. };
  358. const sameAsFirstRow = firstRow;
  359. const secondRow = {
  360. timestamp: '2019-01-01T22:00:0.0000000Z',
  361. entry: '',
  362. hasAnsi: false,
  363. labels: {},
  364. logLevel: LogLevel.info,
  365. raw: '',
  366. timeEpochMs: 0,
  367. timeFromNow: '',
  368. timeLocal: '',
  369. timeUtc: '',
  370. };
  371. describe('when called with SortOrder.Descending', () => {
  372. it('then it should sort descending', () => {
  373. const logsResult: LogsModel = {
  374. rows: [firstRow, sameAsFirstRow, secondRow],
  375. hasUniqueLabels: false,
  376. };
  377. const result = sortLogsResult(logsResult, SortOrder.Descending);
  378. expect(result).toEqual({
  379. rows: [secondRow, firstRow, sameAsFirstRow],
  380. hasUniqueLabels: false,
  381. });
  382. });
  383. });
  384. describe('when called with SortOrder.Ascending', () => {
  385. it('then it should sort ascending', () => {
  386. const logsResult: LogsModel = {
  387. rows: [secondRow, firstRow, sameAsFirstRow],
  388. hasUniqueLabels: false,
  389. };
  390. const result = sortLogsResult(logsResult, SortOrder.Ascending);
  391. expect(result).toEqual({
  392. rows: [firstRow, sameAsFirstRow, secondRow],
  393. hasUniqueLabels: false,
  394. });
  395. });
  396. });
  397. });