widgetCard.spec.tsx 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928
  1. import {initializeOrg} from 'sentry-test/initializeOrg';
  2. import {mountGlobalModal} from 'sentry-test/modal';
  3. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  4. import * as modal from 'sentry/actionCreators/modal';
  5. import {Client} from 'sentry/api';
  6. import * as LineChart from 'sentry/components/charts/lineChart';
  7. import SimpleTableChart from 'sentry/components/charts/simpleTableChart';
  8. import {DisplayType, Widget, WidgetType} from 'sentry/views/dashboardsV2/types';
  9. import WidgetCard from 'sentry/views/dashboardsV2/widgetCard';
  10. import ReleaseWidgetQueries from 'sentry/views/dashboardsV2/widgetCard/releaseWidgetQueries';
  11. jest.mock('sentry/components/charts/simpleTableChart');
  12. jest.mock('sentry/views/dashboardsV2/widgetCard/releaseWidgetQueries');
  13. describe('Dashboards > WidgetCard', function () {
  14. const {router, organization, routerContext} = initializeOrg({
  15. organization: TestStubs.Organization({
  16. features: ['dashboards-edit', 'discover-basic'],
  17. projects: [TestStubs.Project()],
  18. }),
  19. router: {orgId: 'orgId'},
  20. } as Parameters<typeof initializeOrg>[0]);
  21. const multipleQueryWidget: Widget = {
  22. title: 'Errors',
  23. interval: '5m',
  24. displayType: DisplayType.LINE,
  25. widgetType: WidgetType.DISCOVER,
  26. queries: [
  27. {
  28. conditions: 'event.type:error',
  29. fields: ['count()', 'failure_count()'],
  30. aggregates: ['count()', 'failure_count()'],
  31. columns: [],
  32. name: 'errors',
  33. orderby: '',
  34. },
  35. {
  36. conditions: 'event.type:default',
  37. fields: ['count()', 'failure_count()'],
  38. aggregates: ['count()', 'failure_count()'],
  39. columns: [],
  40. name: 'default',
  41. orderby: '',
  42. },
  43. ],
  44. };
  45. const selection = {
  46. projects: [1],
  47. environments: ['prod'],
  48. datetime: {
  49. period: '14d',
  50. start: null,
  51. end: null,
  52. utc: false,
  53. },
  54. };
  55. const api = new Client();
  56. let eventsv2Mock, eventsMock;
  57. beforeEach(function () {
  58. MockApiClient.addMockResponse({
  59. url: '/organizations/org-slug/events-stats/',
  60. body: {meta: {isMetricsData: false}},
  61. });
  62. MockApiClient.addMockResponse({
  63. url: '/organizations/org-slug/events-geo/',
  64. body: {meta: {isMetricsData: false}},
  65. });
  66. eventsv2Mock = MockApiClient.addMockResponse({
  67. url: '/organizations/org-slug/eventsv2/',
  68. body: {
  69. meta: {title: 'string'},
  70. data: [{title: 'title'}],
  71. },
  72. });
  73. eventsMock = MockApiClient.addMockResponse({
  74. url: '/organizations/org-slug/events/',
  75. body: {
  76. meta: {fields: {title: 'string'}},
  77. data: [{title: 'title'}],
  78. },
  79. });
  80. });
  81. afterEach(function () {
  82. MockApiClient.clearMockResponses();
  83. });
  84. it('renders with Open in Discover button and opens the Query Selector Modal when clicked', async function () {
  85. const spy = jest.spyOn(modal, 'openDashboardWidgetQuerySelectorModal');
  86. render(
  87. <WidgetCard
  88. api={api}
  89. organization={organization}
  90. widget={multipleQueryWidget}
  91. selection={selection}
  92. isEditing={false}
  93. onDelete={() => undefined}
  94. onEdit={() => undefined}
  95. onDuplicate={() => undefined}
  96. renderErrorMessage={() => undefined}
  97. isSorting={false}
  98. currentWidgetDragging={false}
  99. showContextMenu
  100. widgetLimitReached={false}
  101. />
  102. );
  103. userEvent.click(await screen.findByLabelText('Widget actions'));
  104. expect(screen.getByText('Open in Discover')).toBeInTheDocument();
  105. userEvent.click(screen.getByText('Open in Discover'));
  106. expect(spy).toHaveBeenCalledWith({
  107. isMetricsData: false,
  108. organization,
  109. widget: multipleQueryWidget,
  110. });
  111. });
  112. it('renders with Open in Discover button and opens in Discover when clicked', async function () {
  113. render(
  114. <WidgetCard
  115. api={api}
  116. organization={organization}
  117. widget={{...multipleQueryWidget, queries: [multipleQueryWidget.queries[0]]}}
  118. selection={selection}
  119. isEditing={false}
  120. onDelete={() => undefined}
  121. onEdit={() => undefined}
  122. onDuplicate={() => undefined}
  123. renderErrorMessage={() => undefined}
  124. isSorting={false}
  125. currentWidgetDragging={false}
  126. showContextMenu
  127. widgetLimitReached={false}
  128. />,
  129. {context: routerContext}
  130. );
  131. userEvent.click(await screen.findByLabelText('Widget actions'));
  132. expect(screen.getByText('Open in Discover')).toBeInTheDocument();
  133. userEvent.click(screen.getByText('Open in Discover'));
  134. expect(router.push).toHaveBeenCalledWith(
  135. '/organizations/org-slug/discover/results/?environment=prod&field=count%28%29&field=failure_count%28%29&name=Errors&project=1&query=event.type%3Aerror&statsPeriod=14d&yAxis=count%28%29&yAxis=failure_count%28%29'
  136. );
  137. });
  138. it('Opens in Discover with World Map', async function () {
  139. render(
  140. <WidgetCard
  141. api={api}
  142. organization={organization}
  143. widget={{
  144. ...multipleQueryWidget,
  145. displayType: DisplayType.WORLD_MAP,
  146. queries: [
  147. {
  148. ...multipleQueryWidget.queries[0],
  149. fields: ['count()'],
  150. aggregates: ['count()'],
  151. columns: [],
  152. },
  153. ],
  154. }}
  155. selection={selection}
  156. isEditing={false}
  157. onDelete={() => undefined}
  158. onEdit={() => undefined}
  159. onDuplicate={() => undefined}
  160. renderErrorMessage={() => undefined}
  161. isSorting={false}
  162. currentWidgetDragging={false}
  163. showContextMenu
  164. widgetLimitReached={false}
  165. />,
  166. {context: routerContext}
  167. );
  168. userEvent.click(await screen.findByLabelText('Widget actions'));
  169. expect(screen.getByText('Open in Discover')).toBeInTheDocument();
  170. userEvent.click(screen.getByText('Open in Discover'));
  171. expect(router.push).toHaveBeenCalledWith(
  172. '/organizations/org-slug/discover/results/?display=worldmap&environment=prod&field=geo.country_code&field=count%28%29&name=Errors&project=1&query=event.type%3Aerror%20has%3Ageo.country_code&statsPeriod=14d&yAxis=count%28%29'
  173. );
  174. });
  175. it('Opens in Discover with prepended fields pulled from equations', async function () {
  176. render(
  177. <WidgetCard
  178. api={api}
  179. organization={organization}
  180. widget={{
  181. ...multipleQueryWidget,
  182. queries: [
  183. {
  184. ...multipleQueryWidget.queries[0],
  185. fields: [
  186. 'equation|(count() + failure_count()) / count_if(transaction.duration,equals,300)',
  187. ],
  188. columns: [],
  189. aggregates: [
  190. 'equation|(count() + failure_count()) / count_if(transaction.duration,equals,300)',
  191. ],
  192. },
  193. ],
  194. }}
  195. selection={selection}
  196. isEditing={false}
  197. onDelete={() => undefined}
  198. onEdit={() => undefined}
  199. onDuplicate={() => undefined}
  200. renderErrorMessage={() => undefined}
  201. isSorting={false}
  202. currentWidgetDragging={false}
  203. showContextMenu
  204. widgetLimitReached={false}
  205. />,
  206. {context: routerContext}
  207. );
  208. userEvent.click(await screen.findByLabelText('Widget actions'));
  209. expect(screen.getByText('Open in Discover')).toBeInTheDocument();
  210. userEvent.click(screen.getByText('Open in Discover'));
  211. expect(router.push).toHaveBeenCalledWith(
  212. '/organizations/org-slug/discover/results/?environment=prod&field=count_if%28transaction.duration%2Cequals%2C300%29&field=failure_count%28%29&field=count%28%29&field=equation%7C%28count%28%29%20%2B%20failure_count%28%29%29%20%2F%20count_if%28transaction.duration%2Cequals%2C300%29&name=Errors&project=1&query=event.type%3Aerror&statsPeriod=14d&yAxis=equation%7C%28count%28%29%20%2B%20failure_count%28%29%29%20%2F%20count_if%28transaction.duration%2Cequals%2C300%29'
  213. );
  214. });
  215. it('Opens in Discover with Top N', async function () {
  216. render(
  217. <WidgetCard
  218. api={api}
  219. organization={organization}
  220. widget={{
  221. ...multipleQueryWidget,
  222. displayType: DisplayType.TOP_N,
  223. queries: [
  224. {
  225. ...multipleQueryWidget.queries[0],
  226. fields: ['transaction', 'count()'],
  227. columns: ['transaction'],
  228. aggregates: ['count()'],
  229. },
  230. ],
  231. }}
  232. selection={selection}
  233. isEditing={false}
  234. onDelete={() => undefined}
  235. onEdit={() => undefined}
  236. onDuplicate={() => undefined}
  237. renderErrorMessage={() => undefined}
  238. isSorting={false}
  239. currentWidgetDragging={false}
  240. showContextMenu
  241. widgetLimitReached={false}
  242. />,
  243. {context: routerContext}
  244. );
  245. userEvent.click(await screen.findByLabelText('Widget actions'));
  246. expect(screen.getByText('Open in Discover')).toBeInTheDocument();
  247. userEvent.click(screen.getByText('Open in Discover'));
  248. expect(router.push).toHaveBeenCalledWith(
  249. '/organizations/org-slug/discover/results/?display=top5&environment=prod&field=transaction&field=count%28%29&name=Errors&project=1&query=event.type%3Aerror&statsPeriod=14d&yAxis=count%28%29'
  250. );
  251. });
  252. it('disables Open in Discover when the widget contains custom measurements', async function () {
  253. render(
  254. <WidgetCard
  255. api={api}
  256. organization={organization}
  257. widget={{
  258. ...multipleQueryWidget,
  259. displayType: DisplayType.LINE,
  260. queries: [
  261. {
  262. ...multipleQueryWidget.queries[0],
  263. fields: [],
  264. columns: [],
  265. aggregates: ['p99(measurements.custom.measurement)'],
  266. },
  267. ],
  268. }}
  269. selection={selection}
  270. isEditing={false}
  271. onDelete={() => undefined}
  272. onEdit={() => undefined}
  273. onDuplicate={() => undefined}
  274. renderErrorMessage={() => undefined}
  275. isSorting={false}
  276. currentWidgetDragging={false}
  277. showContextMenu
  278. widgetLimitReached={false}
  279. />,
  280. {context: routerContext}
  281. );
  282. userEvent.click(await screen.findByLabelText('Widget actions'));
  283. expect(screen.getByText('Open in Discover')).toBeInTheDocument();
  284. userEvent.click(screen.getByText('Open in Discover'));
  285. expect(router.push).not.toHaveBeenCalledWith();
  286. });
  287. it('calls onDuplicate when Duplicate Widget is clicked', async function () {
  288. const mock = jest.fn();
  289. render(
  290. <WidgetCard
  291. api={api}
  292. organization={organization}
  293. widget={{
  294. ...multipleQueryWidget,
  295. displayType: DisplayType.WORLD_MAP,
  296. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  297. }}
  298. selection={selection}
  299. isEditing={false}
  300. onDelete={() => undefined}
  301. onEdit={() => undefined}
  302. onDuplicate={mock}
  303. renderErrorMessage={() => undefined}
  304. isSorting={false}
  305. currentWidgetDragging={false}
  306. showContextMenu
  307. widgetLimitReached={false}
  308. />
  309. );
  310. userEvent.click(await screen.findByLabelText('Widget actions'));
  311. expect(screen.getByText('Duplicate Widget')).toBeInTheDocument();
  312. userEvent.click(screen.getByText('Duplicate Widget'));
  313. expect(mock).toHaveBeenCalledTimes(1);
  314. });
  315. it('does not add duplicate widgets if max widget is reached', async function () {
  316. const mock = jest.fn();
  317. render(
  318. <WidgetCard
  319. api={api}
  320. organization={organization}
  321. widget={{
  322. ...multipleQueryWidget,
  323. displayType: DisplayType.WORLD_MAP,
  324. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  325. }}
  326. selection={selection}
  327. isEditing={false}
  328. onDelete={() => undefined}
  329. onEdit={() => undefined}
  330. onDuplicate={mock}
  331. renderErrorMessage={() => undefined}
  332. isSorting={false}
  333. currentWidgetDragging={false}
  334. showContextMenu
  335. widgetLimitReached
  336. />
  337. );
  338. userEvent.click(await screen.findByLabelText('Widget actions'));
  339. expect(screen.getByText('Duplicate Widget')).toBeInTheDocument();
  340. userEvent.click(screen.getByText('Duplicate Widget'));
  341. expect(mock).toHaveBeenCalledTimes(0);
  342. });
  343. it('calls onEdit when Edit Widget is clicked', async function () {
  344. const mock = jest.fn();
  345. render(
  346. <WidgetCard
  347. api={api}
  348. organization={organization}
  349. widget={{
  350. ...multipleQueryWidget,
  351. displayType: DisplayType.WORLD_MAP,
  352. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  353. }}
  354. selection={selection}
  355. isEditing={false}
  356. onDelete={() => undefined}
  357. onEdit={mock}
  358. onDuplicate={() => undefined}
  359. renderErrorMessage={() => undefined}
  360. isSorting={false}
  361. currentWidgetDragging={false}
  362. showContextMenu
  363. widgetLimitReached={false}
  364. />
  365. );
  366. userEvent.click(await screen.findByLabelText('Widget actions'));
  367. expect(screen.getByText('Edit Widget')).toBeInTheDocument();
  368. userEvent.click(screen.getByText('Edit Widget'));
  369. expect(mock).toHaveBeenCalledTimes(1);
  370. });
  371. it('renders delete widget option', async function () {
  372. const mock = jest.fn();
  373. render(
  374. <WidgetCard
  375. api={api}
  376. organization={organization}
  377. widget={{
  378. ...multipleQueryWidget,
  379. displayType: DisplayType.WORLD_MAP,
  380. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  381. }}
  382. selection={selection}
  383. isEditing={false}
  384. onDelete={mock}
  385. onEdit={() => undefined}
  386. onDuplicate={() => undefined}
  387. renderErrorMessage={() => undefined}
  388. isSorting={false}
  389. currentWidgetDragging={false}
  390. showContextMenu
  391. widgetLimitReached={false}
  392. />
  393. );
  394. userEvent.click(await screen.findByLabelText('Widget actions'));
  395. expect(screen.getByText('Delete Widget')).toBeInTheDocument();
  396. userEvent.click(screen.getByText('Delete Widget'));
  397. // Confirm Modal
  398. await mountGlobalModal();
  399. await screen.findByRole('dialog');
  400. userEvent.click(screen.getByTestId('confirm-button'));
  401. expect(mock).toHaveBeenCalled();
  402. });
  403. it('calls eventsV2 with a limit of 20 items', async function () {
  404. const mock = jest.fn();
  405. render(
  406. <WidgetCard
  407. api={api}
  408. organization={organization}
  409. widget={{
  410. ...multipleQueryWidget,
  411. displayType: DisplayType.TABLE,
  412. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  413. }}
  414. selection={selection}
  415. isEditing={false}
  416. onDelete={mock}
  417. onEdit={() => undefined}
  418. onDuplicate={() => undefined}
  419. renderErrorMessage={() => undefined}
  420. isSorting={false}
  421. currentWidgetDragging={false}
  422. showContextMenu
  423. widgetLimitReached={false}
  424. tableItemLimit={20}
  425. />
  426. );
  427. await tick();
  428. expect(eventsv2Mock).toHaveBeenCalledWith(
  429. '/organizations/org-slug/eventsv2/',
  430. expect.objectContaining({
  431. query: expect.objectContaining({
  432. per_page: 20,
  433. }),
  434. })
  435. );
  436. });
  437. it('calls eventsV2 with a default limit of 5 items', async function () {
  438. const mock = jest.fn();
  439. render(
  440. <WidgetCard
  441. api={api}
  442. organization={organization}
  443. widget={{
  444. ...multipleQueryWidget,
  445. displayType: DisplayType.TABLE,
  446. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  447. }}
  448. selection={selection}
  449. isEditing={false}
  450. onDelete={mock}
  451. onEdit={() => undefined}
  452. onDuplicate={() => undefined}
  453. renderErrorMessage={() => undefined}
  454. isSorting={false}
  455. currentWidgetDragging={false}
  456. showContextMenu
  457. widgetLimitReached={false}
  458. />
  459. );
  460. await tick();
  461. expect(eventsv2Mock).toHaveBeenCalledWith(
  462. '/organizations/org-slug/eventsv2/',
  463. expect.objectContaining({
  464. query: expect.objectContaining({
  465. per_page: 5,
  466. }),
  467. })
  468. );
  469. });
  470. it('has sticky table headers', async function () {
  471. const tableWidget: Widget = {
  472. title: 'Table Widget',
  473. interval: '5m',
  474. displayType: DisplayType.TABLE,
  475. widgetType: WidgetType.DISCOVER,
  476. queries: [
  477. {
  478. conditions: '',
  479. fields: ['transaction', 'count()'],
  480. columns: ['transaction'],
  481. aggregates: ['count()'],
  482. name: 'Table',
  483. orderby: '',
  484. },
  485. ],
  486. };
  487. render(
  488. <WidgetCard
  489. api={api}
  490. organization={organization}
  491. widget={tableWidget}
  492. selection={selection}
  493. isEditing={false}
  494. onDelete={() => undefined}
  495. onEdit={() => undefined}
  496. onDuplicate={() => undefined}
  497. renderErrorMessage={() => undefined}
  498. isSorting={false}
  499. currentWidgetDragging={false}
  500. showContextMenu
  501. widgetLimitReached={false}
  502. tableItemLimit={20}
  503. />
  504. );
  505. await waitFor(() => expect(eventsv2Mock).toHaveBeenCalled());
  506. expect(SimpleTableChart).toHaveBeenCalledWith(
  507. expect.objectContaining({stickyHeaders: true}),
  508. expect.anything()
  509. );
  510. });
  511. it('calls release queries', function () {
  512. const widget: Widget = {
  513. title: 'Release Widget',
  514. interval: '5m',
  515. displayType: DisplayType.LINE,
  516. widgetType: WidgetType.RELEASE,
  517. queries: [],
  518. };
  519. render(
  520. <WidgetCard
  521. api={api}
  522. organization={organization}
  523. widget={widget}
  524. selection={selection}
  525. isEditing={false}
  526. onDelete={() => undefined}
  527. onEdit={() => undefined}
  528. onDuplicate={() => undefined}
  529. renderErrorMessage={() => undefined}
  530. isSorting={false}
  531. currentWidgetDragging={false}
  532. showContextMenu
  533. widgetLimitReached={false}
  534. tableItemLimit={20}
  535. />
  536. );
  537. expect(ReleaseWidgetQueries).toHaveBeenCalledTimes(1);
  538. });
  539. it('opens the widget viewer modal when a widget has no id', async () => {
  540. const widget: Widget = {
  541. title: 'Widget',
  542. interval: '5m',
  543. displayType: DisplayType.LINE,
  544. widgetType: WidgetType.DISCOVER,
  545. queries: [],
  546. };
  547. render(
  548. <WidgetCard
  549. api={api}
  550. organization={organization}
  551. widget={widget}
  552. selection={selection}
  553. isEditing={false}
  554. onDelete={() => undefined}
  555. onEdit={() => undefined}
  556. onDuplicate={() => undefined}
  557. renderErrorMessage={() => undefined}
  558. isSorting={false}
  559. currentWidgetDragging={false}
  560. showContextMenu
  561. widgetLimitReached={false}
  562. showWidgetViewerButton
  563. index="10"
  564. isPreview
  565. />,
  566. {context: routerContext}
  567. );
  568. userEvent.click(await screen.findByLabelText('Open Widget Viewer'));
  569. expect(router.push).toHaveBeenCalledWith(
  570. expect.objectContaining({pathname: '/mock-pathname/widget/10/'})
  571. );
  572. });
  573. it('renders stored data disclaimer', async function () {
  574. MockApiClient.addMockResponse({
  575. url: '/organizations/org-slug/eventsv2/',
  576. body: {
  577. meta: {title: 'string', isMetricsData: false},
  578. data: [{title: 'title'}],
  579. },
  580. });
  581. render(
  582. <WidgetCard
  583. api={api}
  584. organization={{
  585. ...organization,
  586. features: [...organization.features, 'dashboards-mep'],
  587. }}
  588. widget={{
  589. ...multipleQueryWidget,
  590. displayType: DisplayType.TABLE,
  591. queries: [{...multipleQueryWidget.queries[0]}],
  592. }}
  593. selection={selection}
  594. isEditing={false}
  595. onDelete={() => undefined}
  596. onEdit={() => undefined}
  597. onDuplicate={() => undefined}
  598. renderErrorMessage={() => undefined}
  599. isSorting={false}
  600. currentWidgetDragging={false}
  601. showContextMenu
  602. widgetLimitReached={false}
  603. showStoredAlert
  604. />,
  605. {context: routerContext}
  606. );
  607. await waitFor(() => {
  608. // Badge in the widget header
  609. expect(screen.getByText('Indexed')).toBeInTheDocument();
  610. });
  611. await waitFor(() => {
  612. expect(
  613. // Alert below the widget
  614. screen.getByText(/we've automatically adjusted your results/i)
  615. ).toBeInTheDocument();
  616. });
  617. });
  618. describe('using events endpoint', () => {
  619. const organizationWithFlag = {
  620. ...organization,
  621. features: [...organization.features, 'discover-frontend-use-events-endpoint'],
  622. };
  623. it('calls eventsV2 with a limit of 20 items', async function () {
  624. const mock = jest.fn();
  625. render(
  626. <WidgetCard
  627. api={api}
  628. organization={organizationWithFlag}
  629. widget={{
  630. ...multipleQueryWidget,
  631. displayType: DisplayType.TABLE,
  632. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  633. }}
  634. selection={selection}
  635. isEditing={false}
  636. onDelete={mock}
  637. onEdit={() => undefined}
  638. onDuplicate={() => undefined}
  639. renderErrorMessage={() => undefined}
  640. isSorting={false}
  641. currentWidgetDragging={false}
  642. showContextMenu
  643. widgetLimitReached={false}
  644. tableItemLimit={20}
  645. />
  646. );
  647. await tick();
  648. expect(eventsMock).toHaveBeenCalledWith(
  649. '/organizations/org-slug/events/',
  650. expect.objectContaining({
  651. query: expect.objectContaining({
  652. per_page: 20,
  653. }),
  654. })
  655. );
  656. });
  657. it('calls eventsV2 with a default limit of 5 items', async function () {
  658. const mock = jest.fn();
  659. render(
  660. <WidgetCard
  661. api={api}
  662. organization={organizationWithFlag}
  663. widget={{
  664. ...multipleQueryWidget,
  665. displayType: DisplayType.TABLE,
  666. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  667. }}
  668. selection={selection}
  669. isEditing={false}
  670. onDelete={mock}
  671. onEdit={() => undefined}
  672. onDuplicate={() => undefined}
  673. renderErrorMessage={() => undefined}
  674. isSorting={false}
  675. currentWidgetDragging={false}
  676. showContextMenu
  677. widgetLimitReached={false}
  678. />
  679. );
  680. await tick();
  681. expect(eventsMock).toHaveBeenCalledWith(
  682. '/organizations/org-slug/events/',
  683. expect.objectContaining({
  684. query: expect.objectContaining({
  685. per_page: 5,
  686. }),
  687. })
  688. );
  689. });
  690. it('has sticky table headers', async function () {
  691. const tableWidget: Widget = {
  692. title: 'Table Widget',
  693. interval: '5m',
  694. displayType: DisplayType.TABLE,
  695. widgetType: WidgetType.DISCOVER,
  696. queries: [
  697. {
  698. conditions: '',
  699. fields: ['transaction', 'count()'],
  700. columns: ['transaction'],
  701. aggregates: ['count()'],
  702. name: 'Table',
  703. orderby: '',
  704. },
  705. ],
  706. };
  707. render(
  708. <WidgetCard
  709. api={api}
  710. organization={organizationWithFlag}
  711. widget={tableWidget}
  712. selection={selection}
  713. isEditing={false}
  714. onDelete={() => undefined}
  715. onEdit={() => undefined}
  716. onDuplicate={() => undefined}
  717. renderErrorMessage={() => undefined}
  718. isSorting={false}
  719. currentWidgetDragging={false}
  720. showContextMenu
  721. widgetLimitReached={false}
  722. tableItemLimit={20}
  723. />
  724. );
  725. await tick();
  726. expect(SimpleTableChart).toHaveBeenCalledWith(
  727. expect.objectContaining({stickyHeaders: true}),
  728. expect.anything()
  729. );
  730. });
  731. it('renders stored data disclaimer', async function () {
  732. MockApiClient.addMockResponse({
  733. url: '/organizations/org-slug/events/',
  734. body: {
  735. meta: {fields: {title: 'string'}, isMetricsData: false},
  736. data: [{title: 'title'}],
  737. },
  738. });
  739. render(
  740. <WidgetCard
  741. api={api}
  742. organization={{
  743. ...organizationWithFlag,
  744. features: [...organizationWithFlag.features, 'dashboards-mep'],
  745. }}
  746. widget={{
  747. ...multipleQueryWidget,
  748. displayType: DisplayType.TABLE,
  749. queries: [{...multipleQueryWidget.queries[0]}],
  750. }}
  751. selection={selection}
  752. isEditing={false}
  753. onDelete={() => undefined}
  754. onEdit={() => undefined}
  755. onDuplicate={() => undefined}
  756. renderErrorMessage={() => undefined}
  757. isSorting={false}
  758. currentWidgetDragging={false}
  759. showContextMenu
  760. widgetLimitReached={false}
  761. showStoredAlert
  762. />,
  763. {context: routerContext}
  764. );
  765. await waitFor(() => {
  766. // Badge in the widget header
  767. expect(screen.getByText('Indexed')).toBeInTheDocument();
  768. });
  769. await waitFor(() => {
  770. expect(
  771. // Alert below the widget
  772. screen.getByText(/we've automatically adjusted your results/i)
  773. ).toBeInTheDocument();
  774. });
  775. });
  776. it('renders chart using axis and tooltip formatters from custom measurement meta', async function () {
  777. const spy = jest.spyOn(LineChart, 'LineChart');
  778. const eventsStatsMock = MockApiClient.addMockResponse({
  779. url: '/organizations/org-slug/events-stats/',
  780. body: {
  781. data: [
  782. [
  783. 1658262600,
  784. [
  785. {
  786. count: 24,
  787. },
  788. ],
  789. ],
  790. ],
  791. meta: {
  792. fields: {
  793. time: 'date',
  794. p95_measurements_custom: 'duration',
  795. },
  796. units: {
  797. time: null,
  798. p95_measurements_custom: 'minute',
  799. },
  800. isMetricsData: true,
  801. tips: {},
  802. },
  803. },
  804. });
  805. render(
  806. <WidgetCard
  807. api={api}
  808. organization={organization}
  809. widget={{
  810. title: '',
  811. interval: '5m',
  812. widgetType: WidgetType.DISCOVER,
  813. displayType: DisplayType.LINE,
  814. queries: [
  815. {
  816. conditions: '',
  817. name: '',
  818. fields: [],
  819. columns: [],
  820. aggregates: ['p95(measurements.custom)'],
  821. orderby: '',
  822. },
  823. ],
  824. }}
  825. selection={selection}
  826. isEditing={false}
  827. onDelete={() => undefined}
  828. onEdit={() => undefined}
  829. onDuplicate={() => undefined}
  830. renderErrorMessage={() => undefined}
  831. isSorting={false}
  832. currentWidgetDragging={false}
  833. showContextMenu
  834. widgetLimitReached={false}
  835. />,
  836. {context: routerContext}
  837. );
  838. await waitFor(function () {
  839. expect(eventsStatsMock).toHaveBeenCalled();
  840. });
  841. const {tooltip, yAxis} = spy.mock.calls.pop()?.[0] ?? {};
  842. expect(tooltip).toBeDefined();
  843. expect(yAxis).toBeDefined();
  844. // @ts-ignore
  845. expect(tooltip.valueFormatter(24, 'p95(measurements.custom)')).toEqual('24.00ms');
  846. // @ts-ignore
  847. expect(yAxis.axisLabel.formatter(24, 'p95(measurements.custom)')).toEqual('24ms');
  848. });
  849. it('displays indexed badge in preview mode', async function () {
  850. render(
  851. <WidgetCard
  852. api={api}
  853. organization={{
  854. ...organization,
  855. features: [...organization.features, 'dashboards-mep'],
  856. }}
  857. widget={multipleQueryWidget}
  858. selection={selection}
  859. isEditing={false}
  860. onDelete={() => undefined}
  861. onEdit={() => undefined}
  862. onDuplicate={() => undefined}
  863. renderErrorMessage={() => undefined}
  864. isSorting={false}
  865. currentWidgetDragging={false}
  866. showContextMenu
  867. widgetLimitReached={false}
  868. isPreview
  869. />,
  870. {context: routerContext}
  871. );
  872. expect(await screen.findByText('Indexed')).toBeInTheDocument();
  873. });
  874. });
  875. });