index.spec.tsx 29 KB

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