cellAction.spec.jsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import EventView from 'sentry/utils/discover/eventView';
  3. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  4. import CellAction, {Actions, updateQuery} from 'sentry/views/eventsV2/table/cellAction';
  5. const defaultData = {
  6. transaction: 'best-transaction',
  7. count: 19,
  8. timestamp: '2020-06-09T01:46:25+00:00',
  9. release: 'F2520C43515BD1F0E8A6BD46233324641A370BF6',
  10. nullValue: null,
  11. 'measurements.fcp': 1234,
  12. 'percentile(measurements.fcp, 0.5)': 1234,
  13. 'error.handled': [null],
  14. 'error.type': [
  15. 'ServerException',
  16. 'ClickhouseError',
  17. 'QueryException',
  18. 'QueryException',
  19. ],
  20. };
  21. function makeWrapper(eventView, handleCellAction, columnIndex = 0, data = defaultData) {
  22. return mountWithTheme(
  23. <CellAction
  24. dataRow={data}
  25. eventView={eventView}
  26. column={eventView.getColumns()[columnIndex]}
  27. handleCellAction={handleCellAction}
  28. >
  29. <strong>some content</strong>
  30. </CellAction>
  31. );
  32. }
  33. describe('Discover -> CellAction', function () {
  34. const location = {
  35. query: {
  36. id: '42',
  37. name: 'best query',
  38. field: [
  39. 'transaction',
  40. 'count()',
  41. 'timestamp',
  42. 'release',
  43. 'nullValue',
  44. 'measurements.fcp',
  45. 'percentile(measurements.fcp, 0.5)',
  46. 'error.handled',
  47. 'error.type',
  48. ],
  49. widths: ['437', '647', '416', '905'],
  50. sort: ['title'],
  51. query: 'event.type:transaction',
  52. project: [123],
  53. start: '2019-10-01T00:00:00',
  54. end: '2019-10-02T00:00:00',
  55. statsPeriod: '14d',
  56. environment: ['staging'],
  57. yAxis: 'p95',
  58. },
  59. };
  60. const view = EventView.fromLocation(location);
  61. describe('hover menu button', function () {
  62. const wrapper = makeWrapper(view);
  63. it('shows no menu by default', function () {
  64. expect(wrapper.find('MenuButton')).toHaveLength(0);
  65. });
  66. it('shows a menu on hover, and hides again', function () {
  67. wrapper.find('Container').simulate('mouseEnter');
  68. expect(wrapper.find('MenuButton')).toHaveLength(1);
  69. wrapper.find('Container').simulate('mouseLeave');
  70. expect(wrapper.find('MenuButton')).toHaveLength(0);
  71. });
  72. });
  73. describe('opening the menu', function () {
  74. const wrapper = makeWrapper(view);
  75. wrapper.find('Container').simulate('mouseEnter');
  76. it('toggles the menu on click', function () {
  77. // Button should be rendered.
  78. expect(wrapper.find('MenuButton')).toHaveLength(1);
  79. wrapper.find('MenuButton').simulate('click');
  80. // Menu should show now.
  81. expect(wrapper.find('Menu')).toHaveLength(1);
  82. });
  83. });
  84. describe('per cell actions', function () {
  85. let wrapper;
  86. let handleCellAction;
  87. beforeEach(function () {
  88. handleCellAction = jest.fn();
  89. wrapper = makeWrapper(view, handleCellAction);
  90. // Show button and menu.
  91. wrapper.find('Container').simulate('mouseEnter');
  92. wrapper.find('MenuButton').simulate('click');
  93. });
  94. it('add button appends condition', function () {
  95. wrapper.find('button[data-test-id="add-to-filter"]').simulate('click');
  96. expect(handleCellAction).toHaveBeenCalledWith('add', 'best-transaction');
  97. });
  98. it('exclude button adds condition', function () {
  99. wrapper.find('button[data-test-id="exclude-from-filter"]').simulate('click');
  100. expect(handleCellAction).toHaveBeenCalledWith('exclude', 'best-transaction');
  101. });
  102. it('exclude button appends exclusions', function () {
  103. const excludeView = EventView.fromLocation({
  104. query: {...location.query, query: '!transaction:nope'},
  105. });
  106. wrapper = makeWrapper(excludeView, handleCellAction);
  107. // Show button and menu.
  108. wrapper.find('Container').simulate('mouseEnter');
  109. wrapper.find('MenuButton').simulate('click');
  110. wrapper.find('button[data-test-id="exclude-from-filter"]').simulate('click');
  111. expect(handleCellAction).toHaveBeenCalledWith('exclude', 'best-transaction');
  112. });
  113. it('go to summary button goes to transaction summary page', function () {
  114. wrapper.find('button[data-test-id="transaction-summary"]').simulate('click');
  115. expect(handleCellAction).toHaveBeenCalledWith('transaction', 'best-transaction');
  116. });
  117. it('go to release button goes to release health page', function () {
  118. wrapper = makeWrapper(view, handleCellAction, 3);
  119. // Show button and menu.
  120. wrapper.find('Container').simulate('mouseEnter');
  121. wrapper.find('MenuButton').simulate('click');
  122. wrapper.find('button[data-test-id="release"]').simulate('click');
  123. expect(handleCellAction).toHaveBeenCalledWith(
  124. 'release',
  125. 'F2520C43515BD1F0E8A6BD46233324641A370BF6'
  126. );
  127. });
  128. it('greater than button adds condition', function () {
  129. wrapper = makeWrapper(view, handleCellAction, 2);
  130. // Show button and menu.
  131. wrapper.find('Container').simulate('mouseEnter');
  132. wrapper.find('MenuButton').simulate('click');
  133. wrapper.find('button[data-test-id="show-values-greater-than"]').simulate('click');
  134. expect(handleCellAction).toHaveBeenCalledWith(
  135. 'show_greater_than',
  136. '2020-06-09T01:46:25+00:00'
  137. );
  138. });
  139. it('less than button adds condition', function () {
  140. wrapper = makeWrapper(view, handleCellAction, 2);
  141. // Show button and menu.
  142. wrapper.find('Container').simulate('mouseEnter');
  143. wrapper.find('MenuButton').simulate('click');
  144. wrapper.find('button[data-test-id="show-values-less-than"]').simulate('click');
  145. expect(handleCellAction).toHaveBeenCalledWith(
  146. 'show_less_than',
  147. '2020-06-09T01:46:25+00:00'
  148. );
  149. });
  150. it('error.handled with null adds condition', function () {
  151. wrapper = makeWrapper(view, handleCellAction, 7, defaultData);
  152. // Show button and menu.
  153. wrapper.find('Container').simulate('mouseEnter');
  154. wrapper.find('MenuButton').simulate('click');
  155. wrapper.find('button[data-test-id="add-to-filter"]').simulate('click');
  156. expect(handleCellAction).toHaveBeenCalledWith('add', 1);
  157. });
  158. it('error.type with array values adds condition', function () {
  159. wrapper = makeWrapper(view, handleCellAction, 8, defaultData);
  160. // Show button and menu.
  161. wrapper.find('Container').simulate('mouseEnter');
  162. wrapper.find('MenuButton').simulate('click');
  163. wrapper.find('button[data-test-id="add-to-filter"]').simulate('click');
  164. expect(handleCellAction).toHaveBeenCalledWith('add', [
  165. 'ServerException',
  166. 'ClickhouseError',
  167. 'QueryException',
  168. 'QueryException',
  169. ]);
  170. });
  171. it('error.handled with 0 adds condition', function () {
  172. wrapper = makeWrapper(view, handleCellAction, 7, {
  173. ...defaultData,
  174. 'error.handled': [0],
  175. });
  176. // Show button and menu.
  177. wrapper.find('Container').simulate('mouseEnter');
  178. wrapper.find('MenuButton').simulate('click');
  179. wrapper.find('button[data-test-id="add-to-filter"]').simulate('click');
  180. expect(handleCellAction).toHaveBeenCalledWith('add', [0]);
  181. });
  182. it('show appropriate actions for string cells', function () {
  183. wrapper = makeWrapper(view, handleCellAction, 0);
  184. wrapper.find('Container').simulate('mouseEnter');
  185. wrapper.find('MenuButton').simulate('click');
  186. expect(wrapper.find('button[data-test-id="add-to-filter"]').exists()).toBeTruthy();
  187. expect(
  188. wrapper.find('button[data-test-id="exclude-from-filter"]').exists()
  189. ).toBeTruthy();
  190. expect(
  191. wrapper.find('button[data-test-id="show-values-greater-than"]').exists()
  192. ).toBeFalsy();
  193. expect(
  194. wrapper.find('button[data-test-id="show-values-less-than"]').exists()
  195. ).toBeFalsy();
  196. });
  197. it('show appropriate actions for string cells with null values', function () {
  198. wrapper = makeWrapper(view, handleCellAction, 4);
  199. wrapper.find('Container').simulate('mouseEnter');
  200. wrapper.find('MenuButton').simulate('click');
  201. expect(wrapper.find('button[data-test-id="add-to-filter"]').exists()).toBeTruthy();
  202. expect(
  203. wrapper.find('button[data-test-id="exclude-from-filter"]').exists()
  204. ).toBeTruthy();
  205. });
  206. it('show appropriate actions for number cells', function () {
  207. wrapper = makeWrapper(view, handleCellAction, 1);
  208. wrapper.find('Container').simulate('mouseEnter');
  209. wrapper.find('MenuButton').simulate('click');
  210. expect(wrapper.find('button[data-test-id="add-to-filter"]').exists()).toBeFalsy();
  211. expect(
  212. wrapper.find('button[data-test-id="exclude-from-filter"]').exists()
  213. ).toBeFalsy();
  214. expect(
  215. wrapper.find('button[data-test-id="show-values-greater-than"]').exists()
  216. ).toBeTruthy();
  217. expect(
  218. wrapper.find('button[data-test-id="show-values-less-than"]').exists()
  219. ).toBeTruthy();
  220. });
  221. it('show appropriate actions for date cells', function () {
  222. wrapper = makeWrapper(view, handleCellAction, 2);
  223. wrapper.find('Container').simulate('mouseEnter');
  224. wrapper.find('MenuButton').simulate('click');
  225. expect(wrapper.find('button[data-test-id="add-to-filter"]').exists()).toBeTruthy();
  226. expect(
  227. wrapper.find('button[data-test-id="exclude-from-filter"]').exists()
  228. ).toBeFalsy();
  229. expect(
  230. wrapper.find('button[data-test-id="show-values-greater-than"]').exists()
  231. ).toBeTruthy();
  232. expect(
  233. wrapper.find('button[data-test-id="show-values-less-than"]').exists()
  234. ).toBeTruthy();
  235. });
  236. it('show appropriate actions for release cells', function () {
  237. wrapper = makeWrapper(view, handleCellAction, 3);
  238. wrapper.find('Container').simulate('mouseEnter');
  239. wrapper.find('MenuButton').simulate('click');
  240. expect(wrapper.find('button[data-test-id="release"]').exists()).toBeTruthy();
  241. wrapper = makeWrapper(view, handleCellAction, 3, {
  242. ...defaultData,
  243. release: null,
  244. });
  245. wrapper.find('Container').simulate('mouseEnter');
  246. wrapper.find('MenuButton').simulate('click');
  247. expect(wrapper.find('button[data-test-id="release"]').exists()).toBeFalsy();
  248. });
  249. it('show appropriate actions for measurement cells', function () {
  250. wrapper = makeWrapper(view, handleCellAction, 5);
  251. wrapper.find('Container').simulate('mouseEnter');
  252. wrapper.find('MenuButton').simulate('click');
  253. expect(
  254. wrapper.find('button[data-test-id="show-values-greater-than"]').exists()
  255. ).toBeTruthy();
  256. expect(
  257. wrapper.find('button[data-test-id="show-values-less-than"]').exists()
  258. ).toBeTruthy();
  259. expect(wrapper.find('button[data-test-id="add-to-filter"]').exists()).toBeFalsy();
  260. expect(
  261. wrapper.find('button[data-test-id="exclude-from-filter"]').exists()
  262. ).toBeFalsy();
  263. wrapper = makeWrapper(view, handleCellAction, 5, {
  264. ...defaultData,
  265. 'measurements.fcp': null,
  266. });
  267. wrapper.find('Container').simulate('mouseEnter');
  268. wrapper.find('MenuButton').simulate('click');
  269. expect(
  270. wrapper.find('button[data-test-id="show-values-greater-than"]').exists()
  271. ).toBeFalsy();
  272. expect(
  273. wrapper.find('button[data-test-id="show-values-less-than"]').exists()
  274. ).toBeFalsy();
  275. expect(wrapper.find('button[data-test-id="add-to-filter"]').exists()).toBeTruthy();
  276. expect(
  277. wrapper.find('button[data-test-id="exclude-from-filter"]').exists()
  278. ).toBeTruthy();
  279. });
  280. it('show appropriate actions for numeric function cells', function () {
  281. wrapper = makeWrapper(view, handleCellAction, 6);
  282. wrapper.find('Container').simulate('mouseEnter');
  283. wrapper.find('MenuButton').simulate('click');
  284. expect(
  285. wrapper.find('button[data-test-id="show-values-greater-than"]').exists()
  286. ).toBeTruthy();
  287. expect(
  288. wrapper.find('button[data-test-id="show-values-less-than"]').exists()
  289. ).toBeTruthy();
  290. wrapper = makeWrapper(view, handleCellAction, 6, {
  291. ...defaultData,
  292. 'percentile(measurements.fcp, 0.5)': null,
  293. });
  294. wrapper.find('Container').simulate('mouseEnter');
  295. expect(wrapper.find('MenuButton').exists()).toBeFalsy();
  296. });
  297. });
  298. });
  299. describe('updateQuery()', function () {
  300. const columnA = {
  301. key: 'a',
  302. name: 'a',
  303. type: 'number',
  304. isSortable: false,
  305. column: {
  306. kind: 'field',
  307. field: 'a',
  308. },
  309. width: -1,
  310. };
  311. const columnB = {
  312. key: 'b',
  313. name: 'b',
  314. type: 'number',
  315. isSortable: false,
  316. column: {
  317. kind: 'field',
  318. field: 'b',
  319. },
  320. width: -1,
  321. };
  322. it('modifies the query with has/!has', function () {
  323. let results = new MutableSearch([]);
  324. updateQuery(results, Actions.ADD, columnA, null);
  325. expect(results.formatString()).toEqual('!has:a');
  326. updateQuery(results, Actions.EXCLUDE, columnA, null);
  327. expect(results.formatString()).toEqual('has:a');
  328. updateQuery(results, Actions.ADD, columnA, null);
  329. expect(results.formatString()).toEqual('!has:a');
  330. results = new MutableSearch([]);
  331. updateQuery(results, Actions.ADD, columnA, [null]);
  332. expect(results.formatString()).toEqual('!has:a');
  333. });
  334. it('modifies the query with additions', function () {
  335. const results = new MutableSearch([]);
  336. updateQuery(results, Actions.ADD, columnA, '1');
  337. expect(results.formatString()).toEqual('a:1');
  338. updateQuery(results, Actions.ADD, columnB, '1');
  339. expect(results.formatString()).toEqual('a:1 b:1');
  340. updateQuery(results, Actions.ADD, columnA, '2');
  341. expect(results.formatString()).toEqual('b:1 a:2');
  342. updateQuery(results, Actions.ADD, columnA, ['1', '2', '3']);
  343. expect(results.formatString()).toEqual('b:1 a:2 a:1 a:3');
  344. });
  345. it('modifies the query with exclusions', function () {
  346. const results = new MutableSearch([]);
  347. updateQuery(results, Actions.EXCLUDE, columnA, '1');
  348. expect(results.formatString()).toEqual('!a:1');
  349. updateQuery(results, Actions.EXCLUDE, columnB, '1');
  350. expect(results.formatString()).toEqual('!a:1 !b:1');
  351. updateQuery(results, Actions.EXCLUDE, columnA, '2');
  352. expect(results.formatString()).toEqual('!b:1 !a:1 !a:2');
  353. updateQuery(results, Actions.EXCLUDE, columnA, ['1', '2', '3']);
  354. expect(results.formatString()).toEqual('!b:1 !a:1 !a:2 !a:3');
  355. });
  356. it('modifies the query with a mix of additions and exclusions', function () {
  357. const results = new MutableSearch([]);
  358. updateQuery(results, Actions.ADD, columnA, '1');
  359. expect(results.formatString()).toEqual('a:1');
  360. updateQuery(results, Actions.ADD, columnB, '2');
  361. expect(results.formatString()).toEqual('a:1 b:2');
  362. updateQuery(results, Actions.EXCLUDE, columnA, '3');
  363. expect(results.formatString()).toEqual('b:2 !a:3');
  364. updateQuery(results, Actions.EXCLUDE, columnB, '4');
  365. expect(results.formatString()).toEqual('!a:3 !b:4');
  366. results.addFilterValues('!a', ['*dontescapeme*'], false);
  367. expect(results.formatString()).toEqual('!a:3 !b:4 !a:*dontescapeme*');
  368. updateQuery(results, Actions.EXCLUDE, columnA, '*escapeme*');
  369. expect(results.formatString()).toEqual(
  370. '!b:4 !a:3 !a:*dontescapeme* !a:"\\*escapeme\\*"'
  371. );
  372. updateQuery(results, Actions.ADD, columnA, '5');
  373. expect(results.formatString()).toEqual('!b:4 a:5');
  374. updateQuery(results, Actions.ADD, columnB, '6');
  375. expect(results.formatString()).toEqual('a:5 b:6');
  376. });
  377. it('modifies the query with greater/less than', function () {
  378. const results = new MutableSearch([]);
  379. updateQuery(results, Actions.SHOW_GREATER_THAN, columnA, 1);
  380. expect(results.formatString()).toEqual('a:>1');
  381. updateQuery(results, Actions.SHOW_GREATER_THAN, columnA, 2);
  382. expect(results.formatString()).toEqual('a:>2');
  383. updateQuery(results, Actions.SHOW_LESS_THAN, columnA, 3);
  384. expect(results.formatString()).toEqual('a:<3');
  385. updateQuery(results, Actions.SHOW_LESS_THAN, columnA, 4);
  386. expect(results.formatString()).toEqual('a:<4');
  387. });
  388. it('modifies the query with greater/less than on duration fields', function () {
  389. const columnADuration = {...columnA, type: 'duration'};
  390. const results = new MutableSearch([]);
  391. updateQuery(results, Actions.SHOW_GREATER_THAN, columnADuration, 1);
  392. expect(results.formatString()).toEqual('a:>1.00ms');
  393. updateQuery(results, Actions.SHOW_GREATER_THAN, columnADuration, 2);
  394. expect(results.formatString()).toEqual('a:>2.00ms');
  395. updateQuery(results, Actions.SHOW_LESS_THAN, columnADuration, 3);
  396. expect(results.formatString()).toEqual('a:<3.00ms');
  397. updateQuery(results, Actions.SHOW_LESS_THAN, columnADuration, 4.1234);
  398. expect(results.formatString()).toEqual('a:<4.12ms');
  399. });
  400. it('does not error for special actions', function () {
  401. const results = new MutableSearch([]);
  402. updateQuery(results, Actions.TRANSACTION, columnA, '');
  403. updateQuery(results, Actions.RELEASE, columnA, '');
  404. updateQuery(results, Actions.DRILLDOWN, columnA, '');
  405. });
  406. it('errors for unknown actions', function () {
  407. const results = new MutableSearch([]);
  408. expect(() => updateQuery(results, 'unknown', columnA, '')).toThrow();
  409. });
  410. });