index.spec.tsx 27 KB

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