index.spec.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. import {OrganizationFixture} from 'sentry-fixture/organization';
  2. import {ProjectFixture} from 'sentry-fixture/project';
  3. import {RouterFixture} from 'sentry-fixture/routerFixture';
  4. import {
  5. render,
  6. screen,
  7. userEvent,
  8. waitFor,
  9. within,
  10. } from 'sentry-test/reactTestingLibrary';
  11. import {openAddToDashboardModal} from 'sentry/actionCreators/modal';
  12. import ProjectsStore from 'sentry/stores/projectsStore';
  13. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  14. import {
  15. PageParamsProvider,
  16. useExploreDataset,
  17. useExploreFields,
  18. useExploreGroupBys,
  19. useExploreMode,
  20. useExplorePageParams,
  21. useExploreSortBys,
  22. useExploreVisualizes,
  23. } from 'sentry/views/explore/contexts/pageParamsContext';
  24. import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode';
  25. import {ExploreToolbar} from 'sentry/views/explore/toolbar';
  26. import {ChartType} from 'sentry/views/insights/common/components/chart';
  27. import {SpanTagsProvider} from '../contexts/spanTagsContext';
  28. jest.mock('sentry/actionCreators/modal');
  29. describe('ExploreToolbar', function () {
  30. const organization = OrganizationFixture({
  31. features: ['alerts-eap', 'dashboards-eap', 'dashboards-edit'],
  32. });
  33. beforeEach(function () {
  34. // without this the `CompactSelect` component errors with a bunch of async updates
  35. jest.spyOn(console, 'error').mockImplementation();
  36. const project = ProjectFixture({
  37. id: '1',
  38. slug: 'proj-slug',
  39. organization,
  40. });
  41. ProjectsStore.loadInitialData([project]);
  42. MockApiClient.addMockResponse({
  43. url: `/organizations/${organization.slug}/spans/fields/`,
  44. method: 'GET',
  45. body: [],
  46. });
  47. });
  48. it('should not render dataset selector', function () {
  49. function Component() {
  50. return <ExploreToolbar />;
  51. }
  52. render(
  53. <PageParamsProvider>
  54. <SpanTagsProvider dataset={DiscoverDatasets.SPANS_EAP} enabled>
  55. <Component />
  56. </SpanTagsProvider>
  57. </PageParamsProvider>,
  58. {disableRouterMocks: true}
  59. );
  60. const section = screen.queryByTestId('section-dataset');
  61. expect(section).not.toBeInTheDocument();
  62. });
  63. it('allows changing datasets', async function () {
  64. let dataset: any;
  65. function Component() {
  66. dataset = useExploreDataset();
  67. return <ExploreToolbar extras={['dataset toggle']} />;
  68. }
  69. render(
  70. <PageParamsProvider>
  71. <SpanTagsProvider dataset={DiscoverDatasets.SPANS_EAP} enabled>
  72. <Component />
  73. </SpanTagsProvider>
  74. </PageParamsProvider>,
  75. {disableRouterMocks: true}
  76. );
  77. const section = screen.getByTestId('section-dataset');
  78. const eapSpans = within(section).getByRole('radio', {name: 'EAP Spans'});
  79. const rpcSpans = within(section).getByRole('radio', {name: 'EAP RPC Spans'});
  80. const indexedSpans = within(section).getByRole('radio', {name: 'Indexed Spans'});
  81. expect(eapSpans).toBeChecked();
  82. expect(rpcSpans).not.toBeChecked();
  83. expect(indexedSpans).not.toBeChecked();
  84. expect(dataset).toEqual(DiscoverDatasets.SPANS_EAP);
  85. await userEvent.click(rpcSpans);
  86. expect(eapSpans).not.toBeChecked();
  87. expect(rpcSpans).toBeChecked();
  88. expect(indexedSpans).not.toBeChecked();
  89. expect(dataset).toEqual(DiscoverDatasets.SPANS_EAP_RPC);
  90. await userEvent.click(indexedSpans);
  91. expect(eapSpans).not.toBeChecked();
  92. expect(rpcSpans).not.toBeChecked();
  93. expect(indexedSpans).toBeChecked();
  94. expect(dataset).toEqual(DiscoverDatasets.SPANS_INDEXED);
  95. });
  96. it('allows changing mode', async function () {
  97. let mode: any;
  98. function Component() {
  99. mode = useExploreMode();
  100. return <ExploreToolbar extras={['dataset toggle']} />;
  101. }
  102. render(
  103. <PageParamsProvider>
  104. <SpanTagsProvider dataset={DiscoverDatasets.SPANS_EAP} enabled>
  105. <Component />
  106. </SpanTagsProvider>
  107. </PageParamsProvider>,
  108. {disableRouterMocks: true}
  109. );
  110. const section = screen.getByTestId('section-mode');
  111. const samples = within(section).getByRole('radio', {name: 'Samples'});
  112. const aggregates = within(section).getByRole('radio', {name: 'Aggregates'});
  113. expect(samples).toBeChecked();
  114. expect(aggregates).not.toBeChecked();
  115. expect(mode).toEqual(Mode.SAMPLES);
  116. await userEvent.click(aggregates);
  117. expect(samples).not.toBeChecked();
  118. expect(aggregates).toBeChecked();
  119. expect(mode).toEqual(Mode.AGGREGATE);
  120. await userEvent.click(samples);
  121. expect(samples).toBeChecked();
  122. expect(aggregates).not.toBeChecked();
  123. expect(mode).toEqual(Mode.SAMPLES);
  124. });
  125. it('inserts group bys from aggregate mode as fields in samples mode', async function () {
  126. let fields, groupBys;
  127. function Component() {
  128. fields = useExploreFields();
  129. groupBys = useExploreGroupBys();
  130. return <ExploreToolbar extras={['dataset toggle']} />;
  131. }
  132. render(
  133. <PageParamsProvider>
  134. <SpanTagsProvider dataset={DiscoverDatasets.SPANS_EAP} enabled>
  135. <Component />
  136. </SpanTagsProvider>
  137. </PageParamsProvider>,
  138. {disableRouterMocks: true}
  139. );
  140. const section = screen.getByTestId('section-mode');
  141. const samples = within(section).getByRole('radio', {name: 'Samples'});
  142. const aggregates = within(section).getByRole('radio', {name: 'Aggregates'});
  143. expect(fields).toEqual([
  144. 'id',
  145. 'span.op',
  146. 'span.description',
  147. 'span.duration',
  148. 'transaction',
  149. 'timestamp',
  150. ]); // default
  151. // Add a group by, and leave one unselected
  152. await userEvent.click(aggregates);
  153. const groupBy = screen.getByTestId('section-group-by');
  154. await userEvent.click(within(groupBy).getByRole('button', {name: 'span.op'}));
  155. await userEvent.click(within(groupBy).getByRole('option', {name: 'release'}));
  156. expect(groupBys).toEqual(['release']);
  157. await userEvent.click(within(groupBy).getByRole('button', {name: 'Add Group'}));
  158. expect(groupBys).toEqual(['release', '']);
  159. await userEvent.click(samples);
  160. expect(fields).toEqual([
  161. 'id',
  162. 'span.op',
  163. 'span.description',
  164. 'span.duration',
  165. 'transaction',
  166. 'timestamp',
  167. 'release',
  168. ]); // default
  169. });
  170. it('allows changing visualizes', async function () {
  171. let visualizes: any;
  172. function Component() {
  173. visualizes = useExploreVisualizes();
  174. return <ExploreToolbar />;
  175. }
  176. render(
  177. <PageParamsProvider>
  178. <SpanTagsProvider dataset={DiscoverDatasets.SPANS_EAP} enabled>
  179. <Component />
  180. </SpanTagsProvider>
  181. </PageParamsProvider>,
  182. {disableRouterMocks: true}
  183. );
  184. const section = screen.getByTestId('section-visualizes');
  185. // this is the default
  186. expect(visualizes).toEqual([
  187. {
  188. chartType: ChartType.LINE,
  189. label: 'A',
  190. yAxes: ['avg(span.duration)'],
  191. },
  192. ]);
  193. // try changing the field
  194. await userEvent.click(within(section).getByRole('button', {name: 'span.duration'}));
  195. await userEvent.click(within(section).getByRole('option', {name: 'span.self_time'}));
  196. expect(visualizes).toEqual([
  197. {
  198. chartType: ChartType.LINE,
  199. label: 'A',
  200. yAxes: ['avg(span.self_time)'],
  201. },
  202. ]);
  203. // try changing the aggregate
  204. await userEvent.click(within(section).getByRole('button', {name: 'avg'}));
  205. await userEvent.click(within(section).getByRole('option', {name: 'count'}));
  206. expect(visualizes).toEqual([
  207. {
  208. chartType: ChartType.LINE,
  209. label: 'A',
  210. yAxes: ['count(span.self_time)'],
  211. },
  212. ]);
  213. // try adding an overlay
  214. await userEvent.click(within(section).getByRole('button', {name: 'Add Series'}));
  215. await userEvent.click(within(section).getByRole('button', {name: 'span.duration'}));
  216. await userEvent.click(within(section).getByRole('option', {name: 'span.self_time'}));
  217. expect(visualizes).toEqual([
  218. {
  219. chartType: ChartType.LINE,
  220. label: 'A',
  221. yAxes: ['count(span.self_time)', 'avg(span.self_time)'],
  222. },
  223. ]);
  224. // try adding a new chart
  225. await userEvent.click(within(section).getByRole('button', {name: 'Add Chart'}));
  226. expect(visualizes).toEqual([
  227. {
  228. chartType: ChartType.LINE,
  229. label: 'A',
  230. yAxes: ['count(span.self_time)', 'avg(span.self_time)'],
  231. },
  232. {
  233. chartType: ChartType.LINE,
  234. label: 'B',
  235. yAxes: ['avg(span.duration)'],
  236. },
  237. ]);
  238. // delete first overlay
  239. await userEvent.click(within(section).getAllByLabelText('Remove Overlay')[0]!);
  240. expect(visualizes).toEqual([
  241. {
  242. chartType: ChartType.LINE,
  243. label: 'A',
  244. yAxes: ['avg(span.self_time)'],
  245. },
  246. {
  247. chartType: ChartType.LINE,
  248. label: 'B',
  249. yAxes: ['avg(span.duration)'],
  250. },
  251. ]);
  252. // delete second chart
  253. await userEvent.click(within(section).getAllByLabelText('Remove Overlay')[1]!);
  254. expect(visualizes).toEqual([
  255. {
  256. chartType: ChartType.LINE,
  257. label: 'A',
  258. yAxes: ['avg(span.self_time)'],
  259. },
  260. ]);
  261. // only one left so cant be deleted
  262. expect(within(section).getByLabelText('Remove Overlay')).toBeDisabled();
  263. });
  264. it('allows changing group bys', async function () {
  265. let groupBys: any;
  266. function Component() {
  267. groupBys = useExploreGroupBys();
  268. return <ExploreToolbar />;
  269. }
  270. render(
  271. <PageParamsProvider>
  272. <SpanTagsProvider dataset={DiscoverDatasets.SPANS_EAP} enabled>
  273. <Component />
  274. </SpanTagsProvider>
  275. </PageParamsProvider>,
  276. {disableRouterMocks: true}
  277. );
  278. const section = screen.getByTestId('section-group-by');
  279. expect(
  280. within(section).getByRole('button', {name: 'Samples not grouped'})
  281. ).toBeInTheDocument();
  282. expect(groupBys).toEqual(['span.op']);
  283. // disabled in the samples mode
  284. expect(
  285. within(section).getByRole('button', {name: 'Samples not grouped'})
  286. ).toBeDisabled();
  287. // click the aggregates mode to enable
  288. await userEvent.click(
  289. within(screen.getByTestId('section-mode')).getByRole('radio', {
  290. name: 'Aggregates',
  291. })
  292. );
  293. expect(within(section).getByRole('button', {name: 'span.op'})).toBeEnabled();
  294. await userEvent.click(within(section).getByRole('button', {name: 'span.op'}));
  295. const groupByOptions1 = await within(section).findAllByRole('option');
  296. expect(groupByOptions1.length).toBeGreaterThan(0);
  297. await userEvent.click(within(section).getByRole('option', {name: 'project'}));
  298. expect(groupBys).toEqual(['project']);
  299. await userEvent.click(within(section).getByRole('button', {name: 'Add Group'}));
  300. expect(groupBys).toEqual(['project', '']);
  301. await userEvent.click(within(section).getByRole('button', {name: 'None'}));
  302. const groupByOptions2 = await within(section).findAllByRole('option');
  303. expect(groupByOptions2.length).toBeGreaterThan(0);
  304. await userEvent.click(
  305. within(section).getByRole('option', {name: 'span.description'})
  306. );
  307. expect(groupBys).toEqual(['project', 'span.description']);
  308. await userEvent.click(within(section).getAllByLabelText('Remove Column')[0]!);
  309. expect(groupBys).toEqual(['span.description']);
  310. // only 1 left but it's not empty
  311. expect(within(section).getByLabelText('Remove Column')).toBeEnabled();
  312. await userEvent.click(within(section).getByLabelText('Remove Column'));
  313. expect(groupBys).toEqual(['']);
  314. // last one and it's empty
  315. expect(within(section).getByLabelText('Remove Column')).toBeDisabled();
  316. });
  317. it('allows changing sort by', async function () {
  318. let sortBys: any;
  319. function Component() {
  320. sortBys = useExploreSortBys();
  321. return <ExploreToolbar />;
  322. }
  323. render(
  324. <PageParamsProvider>
  325. <SpanTagsProvider dataset={DiscoverDatasets.SPANS_EAP} enabled>
  326. <Component />
  327. </SpanTagsProvider>
  328. </PageParamsProvider>,
  329. {disableRouterMocks: true}
  330. );
  331. const section = screen.getByTestId('section-sort-by');
  332. // this is the default
  333. expect(within(section).getByRole('button', {name: 'timestamp'})).toBeInTheDocument();
  334. expect(within(section).getByRole('button', {name: 'Desc'})).toBeInTheDocument();
  335. expect(sortBys).toEqual([{field: 'timestamp', kind: 'desc'}]);
  336. // check the default field options
  337. const fields = [
  338. 'id',
  339. 'span.description',
  340. 'span.duration',
  341. 'span.op',
  342. 'timestamp',
  343. 'transaction',
  344. ];
  345. await userEvent.click(within(section).getByRole('button', {name: 'timestamp'}));
  346. const fieldOptions = await within(section).findAllByRole('option');
  347. expect(fieldOptions).toHaveLength(fields.length);
  348. fieldOptions.forEach((option, i) => {
  349. expect(option).toHaveTextContent(fields[i]!);
  350. });
  351. // try changing the field
  352. await userEvent.click(within(section).getByRole('option', {name: 'span.op'}));
  353. expect(within(section).getByRole('button', {name: 'span.op'})).toBeInTheDocument();
  354. expect(within(section).getByRole('button', {name: 'Desc'})).toBeInTheDocument();
  355. expect(sortBys).toEqual([{field: 'span.op', kind: 'desc'}]);
  356. // check the kind options
  357. await userEvent.click(within(section).getByRole('button', {name: 'Desc'}));
  358. const kindOptions = await within(section).findAllByRole('option');
  359. expect(kindOptions).toHaveLength(2);
  360. expect(kindOptions[0]).toHaveTextContent('Desc');
  361. expect(kindOptions[1]).toHaveTextContent('Asc');
  362. // try changing the kind
  363. await userEvent.click(within(section).getByRole('option', {name: 'Asc'}));
  364. expect(within(section).getByRole('button', {name: 'span.op'})).toBeInTheDocument();
  365. expect(within(section).getByRole('button', {name: 'Asc'})).toBeInTheDocument();
  366. expect(sortBys).toEqual([{field: 'span.op', kind: 'asc'}]);
  367. });
  368. it('takes you to suggested query', async function () {
  369. let pageParams: any;
  370. function Component() {
  371. pageParams = useExplorePageParams();
  372. return <ExploreToolbar />;
  373. }
  374. render(
  375. <PageParamsProvider>
  376. <SpanTagsProvider dataset={DiscoverDatasets.SPANS_EAP} enabled>
  377. <Component />
  378. </SpanTagsProvider>
  379. </PageParamsProvider>,
  380. {disableRouterMocks: true}
  381. );
  382. const section = screen.getByTestId('section-suggested-queries');
  383. await userEvent.click(within(section).getByText('Slowest Ops'));
  384. expect(pageParams).toEqual(
  385. expect.objectContaining({
  386. fields: [
  387. 'id',
  388. 'project',
  389. 'span.op',
  390. 'span.description',
  391. 'span.duration',
  392. 'timestamp',
  393. ],
  394. groupBys: ['span.op'],
  395. mode: Mode.AGGREGATE,
  396. query: '',
  397. sortBys: [{field: 'avg(span.duration)', kind: 'desc'}],
  398. visualizes: [
  399. {
  400. chartType: ChartType.LINE,
  401. label: 'A',
  402. yAxes: ['avg(span.duration)'],
  403. },
  404. {
  405. chartType: ChartType.LINE,
  406. label: 'B',
  407. yAxes: ['p50(span.duration)'],
  408. },
  409. ],
  410. })
  411. );
  412. });
  413. it('opens the right alert', async function () {
  414. const router = RouterFixture({
  415. location: {
  416. pathname: '/traces/',
  417. query: {
  418. visualize: encodeURIComponent('{"chartType":1,"yAxes":["avg(span.duration)"]}'),
  419. },
  420. },
  421. });
  422. function Component() {
  423. return <ExploreToolbar />;
  424. }
  425. render(
  426. <PageParamsProvider>
  427. <SpanTagsProvider dataset={DiscoverDatasets.SPANS_EAP} enabled>
  428. <Component />
  429. </SpanTagsProvider>
  430. </PageParamsProvider>,
  431. {router, organization}
  432. );
  433. const section = screen.getByTestId('section-save-as');
  434. await userEvent.click(within(section).getByText(/Save as/));
  435. await userEvent.hover(within(section).getByText('An Alert for'));
  436. await userEvent.click(screen.getByText('avg(span.duration)'));
  437. expect(router.push).toHaveBeenCalledWith({
  438. pathname: '/organizations/org-slug/alerts/new/metric/',
  439. query: expect.objectContaining({
  440. aggregate: 'avg(span.duration)',
  441. dataset: 'events_analytics_platform',
  442. }),
  443. });
  444. });
  445. it('add to dashboard options correctly', async function () {
  446. const router = RouterFixture({
  447. location: {
  448. pathname: '/traces/',
  449. query: {
  450. visualize: encodeURIComponent('{"chartType":1,"yAxes":["avg(span.duration)"]}'),
  451. },
  452. },
  453. });
  454. function Component() {
  455. return <ExploreToolbar />;
  456. }
  457. render(
  458. <PageParamsProvider>
  459. <SpanTagsProvider dataset={DiscoverDatasets.SPANS_EAP} enabled>
  460. <Component />
  461. </SpanTagsProvider>
  462. </PageParamsProvider>,
  463. {router, organization}
  464. );
  465. const section = screen.getByTestId('section-save-as');
  466. await userEvent.click(within(section).getByText(/Save as/));
  467. await userEvent.click(within(section).getByText('A Dashboard widget'));
  468. await waitFor(() => {
  469. expect(openAddToDashboardModal).toHaveBeenCalledWith(
  470. expect.objectContaining({
  471. widget: expect.objectContaining({
  472. displayType: 'line',
  473. queries: [
  474. {
  475. aggregates: ['avg(span.duration)'],
  476. columns: [],
  477. conditions: '',
  478. fields: ['avg(span.duration)'],
  479. name: '',
  480. orderby: '-timestamp',
  481. },
  482. ],
  483. title: 'Custom Widget',
  484. widgetType: 'spans',
  485. }),
  486. widgetAsQueryParams: expect.objectContaining({
  487. dataset: 'spans',
  488. defaultTableColumns: [
  489. 'id',
  490. 'span.op',
  491. 'span.description',
  492. 'span.duration',
  493. 'transaction',
  494. 'timestamp',
  495. ],
  496. defaultTitle: 'Custom Widget',
  497. defaultWidgetQuery:
  498. 'name=&aggregates=avg(span.duration)&columns=&fields=avg(span.duration)&conditions=&orderby=-timestamp',
  499. displayType: 'line',
  500. end: undefined,
  501. field: [
  502. 'id',
  503. 'span.op',
  504. 'span.description',
  505. 'span.duration',
  506. 'transaction',
  507. 'timestamp',
  508. ],
  509. limit: undefined,
  510. source: 'discoverv2',
  511. start: undefined,
  512. statsPeriod: '14d',
  513. }),
  514. })
  515. );
  516. });
  517. });
  518. });