explore.test.ts 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. import {
  2. DEFAULT_RANGE,
  3. serializeStateToUrlParam,
  4. parseUrlState,
  5. updateHistory,
  6. clearHistory,
  7. hasNonEmptyQuery,
  8. instanceOfDataQueryError,
  9. getValueWithRefId,
  10. getFirstQueryErrorWithoutRefId,
  11. getRefIds,
  12. } from './explore';
  13. import { ExploreUrlState, ExploreMode } from 'app/types/explore';
  14. import store from 'app/core/store';
  15. import { LogsDedupStrategy } from '@grafana/data';
  16. import { DataQueryError } from '@grafana/ui';
  17. const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
  18. datasource: null,
  19. queries: [],
  20. range: DEFAULT_RANGE,
  21. mode: ExploreMode.Metrics,
  22. ui: {
  23. showingGraph: true,
  24. showingTable: true,
  25. showingLogs: true,
  26. dedupStrategy: LogsDedupStrategy.none,
  27. },
  28. };
  29. describe('state functions', () => {
  30. describe('parseUrlState', () => {
  31. it('returns default state on empty string', () => {
  32. expect(parseUrlState('')).toMatchObject({
  33. datasource: null,
  34. queries: [],
  35. range: DEFAULT_RANGE,
  36. });
  37. });
  38. it('returns a valid Explore state from URL parameter', () => {
  39. const paramValue =
  40. '%7B"datasource":"Local","queries":%5B%7B"expr":"metric"%7D%5D,"range":%7B"from":"now-1h","to":"now"%7D%7D';
  41. expect(parseUrlState(paramValue)).toMatchObject({
  42. datasource: 'Local',
  43. queries: [{ expr: 'metric' }],
  44. range: {
  45. from: 'now-1h',
  46. to: 'now',
  47. },
  48. });
  49. });
  50. it('returns a valid Explore state from a compact URL parameter', () => {
  51. const paramValue = '%5B"now-1h","now","Local","5m",%7B"expr":"metric"%7D,"ui"%5D';
  52. expect(parseUrlState(paramValue)).toMatchObject({
  53. datasource: 'Local',
  54. queries: [{ expr: 'metric' }],
  55. range: {
  56. from: 'now-1h',
  57. to: 'now',
  58. },
  59. });
  60. });
  61. });
  62. describe('serializeStateToUrlParam', () => {
  63. it('returns url parameter value for a state object', () => {
  64. const state = {
  65. ...DEFAULT_EXPLORE_STATE,
  66. datasource: 'foo',
  67. queries: [
  68. {
  69. expr: 'metric{test="a/b"}',
  70. },
  71. {
  72. expr: 'super{foo="x/z"}',
  73. },
  74. ],
  75. range: {
  76. from: 'now-5h',
  77. to: 'now',
  78. },
  79. };
  80. expect(serializeStateToUrlParam(state)).toBe(
  81. '{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
  82. '{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"},' +
  83. '"mode":"Metrics",' +
  84. '"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true,"dedupStrategy":"none"}}'
  85. );
  86. });
  87. it('returns url parameter value for a state object', () => {
  88. const state = {
  89. ...DEFAULT_EXPLORE_STATE,
  90. datasource: 'foo',
  91. queries: [
  92. {
  93. expr: 'metric{test="a/b"}',
  94. },
  95. {
  96. expr: 'super{foo="x/z"}',
  97. },
  98. ],
  99. range: {
  100. from: 'now-5h',
  101. to: 'now',
  102. },
  103. };
  104. expect(serializeStateToUrlParam(state, true)).toBe(
  105. '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"},{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]'
  106. );
  107. });
  108. });
  109. describe('interplay', () => {
  110. it('can parse the serialized state into the original state', () => {
  111. const state = {
  112. ...DEFAULT_EXPLORE_STATE,
  113. datasource: 'foo',
  114. queries: [
  115. {
  116. expr: 'metric{test="a/b"}',
  117. },
  118. {
  119. expr: 'super{foo="x/z"}',
  120. },
  121. ],
  122. range: {
  123. from: 'now - 5h',
  124. to: 'now',
  125. },
  126. };
  127. const serialized = serializeStateToUrlParam(state);
  128. const parsed = parseUrlState(serialized);
  129. expect(state).toMatchObject(parsed);
  130. });
  131. it('can parse the compact serialized state into the original state', () => {
  132. const state = {
  133. ...DEFAULT_EXPLORE_STATE,
  134. datasource: 'foo',
  135. queries: [
  136. {
  137. expr: 'metric{test="a/b"}',
  138. },
  139. {
  140. expr: 'super{foo="x/z"}',
  141. },
  142. ],
  143. range: {
  144. from: 'now - 5h',
  145. to: 'now',
  146. },
  147. };
  148. const serialized = serializeStateToUrlParam(state, true);
  149. const parsed = parseUrlState(serialized);
  150. expect(state).toMatchObject(parsed);
  151. });
  152. });
  153. });
  154. describe('updateHistory()', () => {
  155. const datasourceId = 'myDatasource';
  156. const key = `grafana.explore.history.${datasourceId}`;
  157. beforeEach(() => {
  158. clearHistory(datasourceId);
  159. expect(store.exists(key)).toBeFalsy();
  160. });
  161. test('should save history item to localStorage', () => {
  162. const expected = [
  163. {
  164. query: { refId: '1', expr: 'metric' },
  165. },
  166. ];
  167. expect(updateHistory([], datasourceId, [{ refId: '1', expr: 'metric' }])).toMatchObject(expected);
  168. expect(store.exists(key)).toBeTruthy();
  169. expect(store.getObject(key)).toMatchObject(expected);
  170. });
  171. });
  172. describe('hasNonEmptyQuery', () => {
  173. test('should return true if one query is non-empty', () => {
  174. expect(hasNonEmptyQuery([{ refId: '1', key: '2', context: 'explore', expr: 'foo' }])).toBeTruthy();
  175. });
  176. test('should return false if query is empty', () => {
  177. expect(hasNonEmptyQuery([{ refId: '1', key: '2', context: 'panel' }])).toBeFalsy();
  178. });
  179. test('should return false if no queries exist', () => {
  180. expect(hasNonEmptyQuery([])).toBeFalsy();
  181. });
  182. });
  183. describe('instanceOfDataQueryError', () => {
  184. describe('when called with a DataQueryError', () => {
  185. it('then it should return true', () => {
  186. const error: DataQueryError = {
  187. message: 'A message',
  188. status: '200',
  189. statusText: 'Ok',
  190. };
  191. const result = instanceOfDataQueryError(error);
  192. expect(result).toBe(true);
  193. });
  194. });
  195. describe('when called with a non DataQueryError', () => {
  196. it('then it should return false', () => {
  197. const error = {};
  198. const result = instanceOfDataQueryError(error);
  199. expect(result).toBe(false);
  200. });
  201. });
  202. });
  203. describe('hasRefId', () => {
  204. describe('when called with a null value', () => {
  205. it('then it should return null', () => {
  206. const input: any = null;
  207. const result = getValueWithRefId(input);
  208. expect(result).toBeNull();
  209. });
  210. });
  211. describe('when called with a non object value', () => {
  212. it('then it should return null', () => {
  213. const input = 123;
  214. const result = getValueWithRefId(input);
  215. expect(result).toBeNull();
  216. });
  217. });
  218. describe('when called with an object that has refId', () => {
  219. it('then it should return the object', () => {
  220. const input = { refId: 'A' };
  221. const result = getValueWithRefId(input);
  222. expect(result).toBe(input);
  223. });
  224. });
  225. describe('when called with an array that has refId', () => {
  226. it('then it should return the object', () => {
  227. const input = [123, null, {}, { refId: 'A' }];
  228. const result = getValueWithRefId(input);
  229. expect(result).toBe(input[3]);
  230. });
  231. });
  232. describe('when called with an object that has refId somewhere in the object tree', () => {
  233. it('then it should return the object', () => {
  234. const input: any = { data: [123, null, {}, { series: [123, null, {}, { refId: 'A' }] }] };
  235. const result = getValueWithRefId(input);
  236. expect(result).toBe(input.data[3].series[3]);
  237. });
  238. });
  239. });
  240. describe('getFirstQueryErrorWithoutRefId', () => {
  241. describe('when called with a null value', () => {
  242. it('then it should return null', () => {
  243. const errors: DataQueryError[] = null;
  244. const result = getFirstQueryErrorWithoutRefId(errors);
  245. expect(result).toBeNull();
  246. });
  247. });
  248. describe('when called with an array with only refIds', () => {
  249. it('then it should return undefined', () => {
  250. const errors: DataQueryError[] = [{ refId: 'A' }, { refId: 'B' }];
  251. const result = getFirstQueryErrorWithoutRefId(errors);
  252. expect(result).toBeUndefined();
  253. });
  254. });
  255. describe('when called with an array with and without refIds', () => {
  256. it('then it should return undefined', () => {
  257. const errors: DataQueryError[] = [
  258. { refId: 'A' },
  259. { message: 'A message' },
  260. { refId: 'B' },
  261. { message: 'B message' },
  262. ];
  263. const result = getFirstQueryErrorWithoutRefId(errors);
  264. expect(result).toBe(errors[1]);
  265. });
  266. });
  267. });
  268. describe('getRefIds', () => {
  269. describe('when called with a null value', () => {
  270. it('then it should return empty array', () => {
  271. const input: any = null;
  272. const result = getRefIds(input);
  273. expect(result).toEqual([]);
  274. });
  275. });
  276. describe('when called with a non object value', () => {
  277. it('then it should return empty array', () => {
  278. const input = 123;
  279. const result = getRefIds(input);
  280. expect(result).toEqual([]);
  281. });
  282. });
  283. describe('when called with an object that has refId', () => {
  284. it('then it should return an array with that refId', () => {
  285. const input = { refId: 'A' };
  286. const result = getRefIds(input);
  287. expect(result).toEqual(['A']);
  288. });
  289. });
  290. describe('when called with an array that has refIds', () => {
  291. it('then it should return an array with unique refIds', () => {
  292. const input = [123, null, {}, { refId: 'A' }, { refId: 'A' }, { refId: 'B' }];
  293. const result = getRefIds(input);
  294. expect(result).toEqual(['A', 'B']);
  295. });
  296. });
  297. describe('when called with an object that has refIds somewhere in the object tree', () => {
  298. it('then it should return return an array with unique refIds', () => {
  299. const input: any = {
  300. data: [
  301. 123,
  302. null,
  303. { refId: 'B', series: [{ refId: 'X' }] },
  304. { refId: 'B' },
  305. {},
  306. { series: [123, null, {}, { refId: 'A' }] },
  307. ],
  308. };
  309. const result = getRefIds(input);
  310. expect(result).toEqual(['B', 'X', 'A']);
  311. });
  312. });
  313. });