index.spec.tsx 29 KB

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