store.test.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. import { TemplateSrvStub } from 'test/specs/helpers';
  2. import { dispatch } from 'app/store/store';
  3. import gfunc from '../gfunc';
  4. import { actions } from '../state/actions';
  5. import {
  6. getAltSegmentsSelectables,
  7. getTagsSelectables,
  8. getTagsAsSegmentsSelectables,
  9. getTagValuesSelectables,
  10. } from '../state/providers';
  11. import { createStore } from '../state/store';
  12. import { GraphiteSegment } from '../types';
  13. jest.mock('app/angular/promiseToDigest', () => ({
  14. promiseToDigest: (scope: any) => {
  15. return (p: Promise<any>) => p;
  16. },
  17. }));
  18. jest.mock('app/store/store', () => ({
  19. dispatch: jest.fn(),
  20. }));
  21. const mockDispatch = dispatch as jest.Mock;
  22. /**
  23. * Simulate switching to text editor, changing the query and switching back to visual editor
  24. */
  25. async function changeTarget(ctx: any, target: string): Promise<void> {
  26. await ctx.dispatch(actions.toggleEditorMode());
  27. await ctx.dispatch(actions.updateQuery({ query: target }));
  28. await ctx.dispatch(actions.runQuery());
  29. await ctx.dispatch(actions.toggleEditorMode());
  30. }
  31. describe('Graphite actions', () => {
  32. const ctx = {
  33. datasource: {
  34. metricFindQuery: jest.fn(() => Promise.resolve([])),
  35. getFuncDefs: jest.fn(() => Promise.resolve(gfunc.getFuncDefs('1.0'))),
  36. getFuncDef: gfunc.getFuncDef,
  37. waitForFuncDefsLoaded: jest.fn(() => Promise.resolve(null)),
  38. createFuncInstance: gfunc.createFuncInstance,
  39. getTagsAutoComplete: jest.fn().mockReturnValue(Promise.resolve([])),
  40. getTagValuesAutoComplete: jest.fn().mockReturnValue(Promise.resolve([])),
  41. },
  42. target: { target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)' },
  43. } as any;
  44. beforeEach(async () => {
  45. jest.clearAllMocks();
  46. ctx.state = null;
  47. ctx.dispatch = createStore((state) => {
  48. ctx.state = state;
  49. });
  50. await ctx.dispatch(
  51. actions.init({
  52. datasource: ctx.datasource,
  53. target: ctx.target,
  54. refresh: jest.fn(),
  55. queries: [],
  56. //@ts-ignore
  57. templateSrv: new TemplateSrvStub(),
  58. })
  59. );
  60. });
  61. describe('init', () => {
  62. it('should validate metric key exists', () => {
  63. expect(ctx.datasource.metricFindQuery.mock.calls[0][0]).toBe('test.prod.*');
  64. });
  65. it('should not delete last segment if no metrics are found', () => {
  66. expect(ctx.state.segments[2].value).not.toBe('select metric');
  67. expect(ctx.state.segments[2].value).toBe('*');
  68. });
  69. it('should parse expression and build function model', () => {
  70. expect(ctx.state.queryModel.functions.length).toBe(2);
  71. });
  72. });
  73. describe('when toggling edit mode to raw and back again', () => {
  74. beforeEach(async () => {
  75. await ctx.dispatch(actions.toggleEditorMode());
  76. await ctx.dispatch(actions.toggleEditorMode());
  77. });
  78. it('should validate metric key exists', () => {
  79. const lastCallIndex = ctx.datasource.metricFindQuery.mock.calls.length - 1;
  80. expect(ctx.datasource.metricFindQuery.mock.calls[lastCallIndex][0]).toBe('test.prod.*');
  81. });
  82. it('should delete last segment if no metrics are found', () => {
  83. expect(ctx.state.segments[0].value).toBe('test');
  84. expect(ctx.state.segments[1].value).toBe('prod');
  85. expect(ctx.state.segments[2].value).toBe('select metric');
  86. });
  87. it('should parse expression and build function model', () => {
  88. expect(ctx.state.queryModel.functions.length).toBe(2);
  89. });
  90. });
  91. describe('when middle segment value of test.prod.* is changed', () => {
  92. beforeEach(async () => {
  93. const segment: GraphiteSegment = { type: 'metric', value: 'test', expandable: true };
  94. await ctx.dispatch(actions.segmentValueChanged({ segment: segment, index: 1 }));
  95. });
  96. it('should validate metric key exists', () => {
  97. const lastCallIndex = ctx.datasource.metricFindQuery.mock.calls.length - 1;
  98. expect(ctx.datasource.metricFindQuery.mock.calls[lastCallIndex][0]).toBe('test.test.*');
  99. });
  100. it('should delete last segment if no metrics are found', () => {
  101. expect(ctx.state.segments[0].value).toBe('test');
  102. expect(ctx.state.segments[1].value).toBe('test');
  103. expect(ctx.state.segments[2].value).toBe('select metric');
  104. });
  105. it('should parse expression and build function model', () => {
  106. expect(ctx.state.queryModel.functions.length).toBe(2);
  107. });
  108. });
  109. describe('when adding function', () => {
  110. beforeEach(async () => {
  111. ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
  112. await changeTarget(ctx, 'test.prod.*.count');
  113. await ctx.dispatch(actions.addFunction({ name: 'aliasByNode' }));
  114. });
  115. it('should add function with correct node number', () => {
  116. expect(ctx.state.queryModel.functions[0].params[0]).toBe(2);
  117. });
  118. it('should update target', () => {
  119. expect(ctx.state.target.target).toBe('aliasByNode(test.prod.*.count, 2)');
  120. });
  121. it('should call refresh', () => {
  122. expect(ctx.state.refresh).toHaveBeenCalled();
  123. });
  124. });
  125. describe('when adding function before any metric segment', () => {
  126. beforeEach(async () => {
  127. ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: true }]);
  128. await changeTarget(ctx, '');
  129. await ctx.dispatch(actions.addFunction({ name: 'asPercent' }));
  130. });
  131. it('should add function and remove select metric link', () => {
  132. expect(ctx.state.segments.length).toBe(0);
  133. });
  134. });
  135. describe('when initializing a target with single param func using variable', () => {
  136. beforeEach(async () => {
  137. ctx.state.datasource.metricFindQuery = () => Promise.resolve([]);
  138. await changeTarget(ctx, 'movingAverage(prod.count, $var)');
  139. });
  140. it('should add 2 segments', () => {
  141. expect(ctx.state.segments.length).toBe(2);
  142. });
  143. it('should add function param', () => {
  144. expect(ctx.state.queryModel.functions[0].params.length).toBe(1);
  145. });
  146. });
  147. describe('when changing the query from the outside', () => {
  148. it('should update the model', async () => {
  149. ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ text: '*' }]);
  150. await changeTarget(ctx, 'my.query.*');
  151. expect(ctx.state.target.target).toBe('my.query.*');
  152. expect(ctx.state.segments[0].value).toBe('my');
  153. expect(ctx.state.segments[1].value).toBe('query');
  154. await ctx.dispatch(actions.queryChanged({ target: 'new.metrics.*', refId: 'A' }));
  155. expect(ctx.state.target.target).toBe('new.metrics.*');
  156. expect(ctx.state.segments[0].value).toBe('new');
  157. expect(ctx.state.segments[1].value).toBe('metrics');
  158. });
  159. });
  160. describe('when initializing target without metric expression and function with series-ref', () => {
  161. beforeEach(async () => {
  162. ctx.state.datasource.metricFindQuery = () => Promise.resolve([]);
  163. await changeTarget(ctx, 'asPercent(metric.node.count, #A)');
  164. });
  165. it('should add segments', () => {
  166. expect(ctx.state.segments.length).toBe(3);
  167. });
  168. it('should have correct func params', () => {
  169. expect(ctx.state.queryModel.functions[0].params.length).toBe(1);
  170. });
  171. });
  172. describe('when getting altSegments and metricFindQuery returns empty array', () => {
  173. beforeEach(async () => {
  174. ctx.state.datasource.metricFindQuery = () => Promise.resolve([]);
  175. await changeTarget(ctx, 'test.count');
  176. ctx.altSegments = await getAltSegmentsSelectables(ctx.state, 1, '');
  177. });
  178. it('should have no segments', () => {
  179. expect(ctx.altSegments.length).toBe(0);
  180. });
  181. });
  182. it('current time range and limit is passed when getting list of tags when editing', async () => {
  183. const currentRange = { from: 0, to: 1 };
  184. ctx.state.range = currentRange;
  185. await getTagsSelectables(ctx.state, 0, 'any');
  186. expect(ctx.state.datasource.getTagsAutoComplete).toBeCalledWith([], 'any', { range: currentRange, limit: 5000 });
  187. });
  188. it('current time range and limit is passed when getting list of tags for adding', async () => {
  189. const currentRange = { from: 0, to: 1 };
  190. ctx.state.range = currentRange;
  191. await getTagsAsSegmentsSelectables(ctx.state, 'any');
  192. expect(ctx.state.datasource.getTagsAutoComplete).toBeCalledWith([], 'any', { range: currentRange, limit: 5000 });
  193. });
  194. it('limit is passed when getting list of tag values', async () => {
  195. await getTagValuesSelectables(ctx.state, { key: 'key', operator: '=', value: 'value' }, 1, 'test');
  196. expect(ctx.state.datasource.getTagValuesAutoComplete).toBeCalledWith([], 'key', 'test', { limit: 5000 });
  197. });
  198. describe('when autocomplete for metric names is not available', () => {
  199. beforeEach(() => {
  200. ctx.state.datasource.getTagsAutoComplete = jest.fn().mockReturnValue(Promise.resolve([]));
  201. ctx.state.datasource.metricFindQuery = jest.fn().mockReturnValue(
  202. new Promise(() => {
  203. throw new Error();
  204. })
  205. );
  206. });
  207. it('getAltSegmentsSelectables should handle autocomplete errors', async () => {
  208. await expect(async () => {
  209. await getAltSegmentsSelectables(ctx.state, 0, 'any');
  210. expect(mockDispatch).toBeCalledWith(
  211. expect.objectContaining({
  212. type: 'appNotifications/notifyApp',
  213. })
  214. );
  215. }).not.toThrow();
  216. });
  217. it('getAltSegmentsSelectables should display the error message only once', async () => {
  218. await getAltSegmentsSelectables(ctx.state, 0, 'any');
  219. expect(mockDispatch.mock.calls.length).toBe(1);
  220. await getAltSegmentsSelectables(ctx.state, 0, 'any');
  221. expect(mockDispatch.mock.calls.length).toBe(1);
  222. });
  223. });
  224. describe('when autocomplete for tags is not available', () => {
  225. beforeEach(() => {
  226. ctx.datasource.metricFindQuery = jest.fn().mockReturnValue(Promise.resolve([]));
  227. ctx.datasource.getTagsAutoComplete = jest.fn().mockReturnValue(
  228. new Promise(() => {
  229. throw new Error();
  230. })
  231. );
  232. });
  233. it('getTagsSelectables should handle autocomplete errors', async () => {
  234. await expect(async () => {
  235. await getTagsSelectables(ctx.state, 0, 'any');
  236. expect(mockDispatch).toBeCalledWith(
  237. expect.objectContaining({
  238. type: 'appNotifications/notifyApp',
  239. })
  240. );
  241. }).not.toThrow();
  242. });
  243. it('getTagsSelectables should display the error message only once', async () => {
  244. await getTagsSelectables(ctx.state, 0, 'any');
  245. expect(mockDispatch.mock.calls.length).toBe(1);
  246. await getTagsSelectables(ctx.state, 0, 'any');
  247. expect(mockDispatch.mock.calls.length).toBe(1);
  248. });
  249. it('getTagsAsSegmentsSelectables should handle autocomplete errors', async () => {
  250. await expect(async () => {
  251. await getTagsAsSegmentsSelectables(ctx.state, 'any');
  252. expect(mockDispatch).toBeCalledWith(
  253. expect.objectContaining({
  254. type: 'appNotifications/notifyApp',
  255. })
  256. );
  257. }).not.toThrow();
  258. });
  259. it('getTagsAsSegmentsSelectables should display the error message only once', async () => {
  260. await getTagsAsSegmentsSelectables(ctx.state, 'any');
  261. expect(mockDispatch.mock.calls.length).toBe(1);
  262. await getTagsAsSegmentsSelectables(ctx.state, 'any');
  263. expect(mockDispatch.mock.calls.length).toBe(1);
  264. });
  265. });
  266. describe('targetChanged', () => {
  267. beforeEach(async () => {
  268. const newQuery = 'aliasByNode(scaleToSeconds(test.prod.*, 1), 2)';
  269. ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
  270. await changeTarget(ctx, newQuery);
  271. });
  272. it('should rebuild target after expression model', () => {
  273. expect(ctx.state.target.target).toBe('aliasByNode(scaleToSeconds(test.prod.*, 1), 2)');
  274. });
  275. it('should call refresh', () => {
  276. expect(ctx.state.refresh).toHaveBeenCalled();
  277. });
  278. });
  279. describe('when updating targets with nested query', () => {
  280. beforeEach(async () => {
  281. ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
  282. await changeTarget(ctx, 'scaleToSeconds(#A, 60)');
  283. });
  284. it('should add function params', () => {
  285. expect(ctx.state.queryModel.segments.length).toBe(1);
  286. expect(ctx.state.queryModel.segments[0].value).toBe('#A');
  287. expect(ctx.state.queryModel.functions[0].params.length).toBe(1);
  288. expect(ctx.state.queryModel.functions[0].params[0]).toBe(60);
  289. });
  290. it('target should remain the same', () => {
  291. expect(ctx.state.target.target).toBe('scaleToSeconds(#A, 60)');
  292. });
  293. it('targetFull should include nested queries', async () => {
  294. await ctx.dispatch(
  295. actions.queriesChanged([
  296. {
  297. target: 'nested.query.count',
  298. refId: 'A',
  299. },
  300. ])
  301. );
  302. expect(ctx.state.target.target).toBe('scaleToSeconds(#A, 60)');
  303. expect(ctx.state.target.targetFull).toBe('scaleToSeconds(nested.query.count, 60)');
  304. });
  305. });
  306. describe('target interpolation', () => {
  307. beforeEach(async () => {
  308. ctx.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
  309. ctx.state.target.refId = 'A';
  310. await changeTarget(ctx, 'sumSeries(#B)');
  311. });
  312. it('when updating target used in other query, targetFull of other query should update', async () => {
  313. ctx.state.queries = [ctx.state.target, { target: 'metrics.foo.count', refId: 'B' }];
  314. await changeTarget(ctx, 'sumSeries(#B)');
  315. expect(ctx.state.queryModel.target.targetFull).toBe('sumSeries(metrics.foo.count)');
  316. });
  317. it('when updating target from a query from other data source, targetFull of other query should not update', async () => {
  318. ctx.state.queries = [ctx.state.target, { someOtherProperty: 'metrics.foo.count', refId: 'B' }];
  319. await changeTarget(ctx, 'sumSeries(#B)');
  320. expect(ctx.state.queryModel.target.targetFull).toBeUndefined();
  321. });
  322. });
  323. describe('when adding seriesByTag function', () => {
  324. beforeEach(async () => {
  325. ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
  326. await changeTarget(ctx, '');
  327. await ctx.dispatch(actions.addFunction({ name: 'seriesByTag' }));
  328. });
  329. it('should update functions', () => {
  330. expect(ctx.state.queryModel.getSeriesByTagFuncIndex()).toBe(0);
  331. });
  332. it('should update seriesByTagUsed flag', () => {
  333. expect(ctx.state.queryModel.seriesByTagUsed).toBe(true);
  334. });
  335. it('should update target', () => {
  336. expect(ctx.state.target.target).toBe('seriesByTag()');
  337. });
  338. it('should call refresh', () => {
  339. expect(ctx.state.refresh).toHaveBeenCalled();
  340. });
  341. });
  342. describe('when parsing seriesByTag function', () => {
  343. beforeEach(async () => {
  344. ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
  345. await changeTarget(ctx, "seriesByTag('tag1=value1', 'tag2!=~value2')");
  346. });
  347. it('should add tags', () => {
  348. const expected = [
  349. { key: 'tag1', operator: '=', value: 'value1' },
  350. { key: 'tag2', operator: '!=~', value: 'value2' },
  351. ];
  352. expect(ctx.state.queryModel.tags).toEqual(expected);
  353. });
  354. });
  355. describe('when tag added', () => {
  356. beforeEach(async () => {
  357. ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
  358. await changeTarget(ctx, 'seriesByTag()');
  359. await ctx.dispatch(actions.addNewTag({ segment: { value: 'tag1' } }));
  360. });
  361. it('should update tags with default value', () => {
  362. const expected = [{ key: 'tag1', operator: '=', value: '' }];
  363. expect(ctx.state.queryModel.tags).toEqual(expected);
  364. });
  365. it('should update target', () => {
  366. const expected = "seriesByTag('tag1=')";
  367. expect(ctx.state.target.target).toEqual(expected);
  368. });
  369. });
  370. describe('when tag changed', () => {
  371. beforeEach(async () => {
  372. ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
  373. await changeTarget(ctx, "seriesByTag('tag1=value1', 'tag2!=~value2')");
  374. await ctx.dispatch(actions.tagChanged({ tag: { key: 'tag1', operator: '=', value: 'new_value' }, index: 0 }));
  375. });
  376. it('should update tags', () => {
  377. const expected = [
  378. { key: 'tag1', operator: '=', value: 'new_value' },
  379. { key: 'tag2', operator: '!=~', value: 'value2' },
  380. ];
  381. expect(ctx.state.queryModel.tags).toEqual(expected);
  382. });
  383. it('should update target', () => {
  384. const expected = "seriesByTag('tag1=new_value', 'tag2!=~value2')";
  385. expect(ctx.state.target.target).toEqual(expected);
  386. });
  387. });
  388. describe('when tag removed', () => {
  389. beforeEach(async () => {
  390. ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
  391. await changeTarget(ctx, "seriesByTag('tag1=value1', 'tag2!=~value2')");
  392. await ctx.dispatch(
  393. actions.tagChanged({ tag: { key: ctx.state.removeTagValue, operator: '=', value: '' }, index: 0 })
  394. );
  395. });
  396. it('should update tags', () => {
  397. const expected = [{ key: 'tag2', operator: '!=~', value: 'value2' }];
  398. expect(ctx.state.queryModel.tags).toEqual(expected);
  399. });
  400. it('should update target', () => {
  401. const expected = "seriesByTag('tag2!=~value2')";
  402. expect(ctx.state.target.target).toEqual(expected);
  403. });
  404. });
  405. describe('when auto-completing over a large set of tags and metrics', () => {
  406. const manyMetrics: Array<{ text: string }> = [],
  407. max = 20000;
  408. beforeEach(() => {
  409. for (let i = 0; i < max; i++) {
  410. manyMetrics.push({ text: `metric${i}` });
  411. }
  412. ctx.state.datasource.metricFindQuery = jest.fn().mockReturnValue(Promise.resolve(manyMetrics));
  413. ctx.state.datasource.getTagsAutoComplete = jest.fn((_tag, _prefix, { limit }) => {
  414. const tags = [];
  415. for (let i = 0; i < limit; i++) {
  416. tags.push({ text: `tag${i}` });
  417. }
  418. return tags;
  419. });
  420. });
  421. it('uses limited metrics and tags list', async () => {
  422. ctx.state.supportsTags = true;
  423. const segments = await getAltSegmentsSelectables(ctx.state, 0, '');
  424. expect(segments).toHaveLength(10000);
  425. expect(segments[0].value!.value).toBe('*'); // * - is a fixed metric name, always added at the top
  426. expect(segments[4999].value!.value).toBe('metric4998');
  427. expect(segments[5000].value!.value).toBe('tag: tag0');
  428. expect(segments[9999].value!.value).toBe('tag: tag4999');
  429. });
  430. it('uses correct limit for metrics and tags list when tags are not supported', async () => {
  431. ctx.state.supportsTags = false;
  432. const segments = await getAltSegmentsSelectables(ctx.state, 0, '');
  433. expect(segments).toHaveLength(5000);
  434. expect(segments[0].value!.value).toBe('*'); // * - is a fixed metric name, always added at the top
  435. expect(segments[4999].value!.value).toBe('metric4998');
  436. });
  437. it('uses limited metrics when adding more metrics', async () => {
  438. const segments = await getAltSegmentsSelectables(ctx.state, 1, '');
  439. expect(segments).toHaveLength(5000);
  440. });
  441. });
  442. });