index.spec.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815
  1. import {DashboardFixture} from 'sentry-fixture/dashboard';
  2. import {LocationFixture} from 'sentry-fixture/locationFixture';
  3. import {OrganizationFixture} from 'sentry-fixture/organization';
  4. import {WidgetFixture} from 'sentry-fixture/widget';
  5. import {initializeOrg} from 'sentry-test/initializeOrg';
  6. import {
  7. render,
  8. renderGlobalModal,
  9. screen,
  10. userEvent,
  11. waitFor,
  12. } from 'sentry-test/reactTestingLibrary';
  13. import * as modal from 'sentry/actionCreators/modal';
  14. import * as LineChart from 'sentry/components/charts/lineChart';
  15. import SimpleTableChart from 'sentry/components/charts/simpleTableChart';
  16. import {DatasetSource} from 'sentry/utils/discover/types';
  17. import {MINUTE, SECOND} from 'sentry/utils/formatters';
  18. import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  19. import type {Widget} from 'sentry/views/dashboards/types';
  20. import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
  21. import WidgetCard from 'sentry/views/dashboards/widgetCard';
  22. import ReleaseWidgetQueries from 'sentry/views/dashboards/widgetCard/releaseWidgetQueries';
  23. import WidgetLegendSelectionState from '../widgetLegendSelectionState';
  24. import {DashboardsMEPProvider} from './dashboardsMEPContext';
  25. jest.mock('sentry/components/charts/simpleTableChart', () => jest.fn(() => <div />));
  26. jest.mock('sentry/views/dashboards/widgetCard/releaseWidgetQueries');
  27. describe('Dashboards > WidgetCard', function () {
  28. const {router, organization} = initializeOrg({
  29. organization: OrganizationFixture({
  30. features: ['dashboards-edit', 'discover-basic'],
  31. }),
  32. router: {orgId: 'orgId'},
  33. } as Parameters<typeof initializeOrg>[0]);
  34. const renderWithProviders = (component: React.ReactNode) =>
  35. render(
  36. <DashboardsMEPProvider>
  37. <MEPSettingProvider forceTransactions={false}>{component}</MEPSettingProvider>
  38. </DashboardsMEPProvider>,
  39. {organization, router}
  40. );
  41. const multipleQueryWidget: Widget = {
  42. title: 'Errors',
  43. description: 'Valid widget description',
  44. interval: '5m',
  45. displayType: DisplayType.LINE,
  46. widgetType: WidgetType.DISCOVER,
  47. queries: [
  48. {
  49. conditions: 'event.type:error',
  50. fields: ['count()', 'failure_count()'],
  51. aggregates: ['count()', 'failure_count()'],
  52. columns: [],
  53. name: 'errors',
  54. orderby: '',
  55. },
  56. {
  57. conditions: 'event.type:default',
  58. fields: ['count()', 'failure_count()'],
  59. aggregates: ['count()', 'failure_count()'],
  60. columns: [],
  61. name: 'default',
  62. orderby: '',
  63. },
  64. ],
  65. };
  66. const selection = {
  67. projects: [1],
  68. environments: ['prod'],
  69. datetime: {
  70. period: '14d',
  71. start: null,
  72. end: null,
  73. utc: false,
  74. },
  75. };
  76. const api = new MockApiClient();
  77. let eventsMock: jest.Mock;
  78. const widgetLegendState = new WidgetLegendSelectionState({
  79. location: LocationFixture(),
  80. dashboard: DashboardFixture([multipleQueryWidget]),
  81. organization,
  82. router,
  83. });
  84. beforeEach(function () {
  85. MockApiClient.addMockResponse({
  86. url: '/organizations/org-slug/events-stats/',
  87. body: {meta: {isMetricsData: false}},
  88. });
  89. eventsMock = MockApiClient.addMockResponse({
  90. url: '/organizations/org-slug/events/',
  91. body: {
  92. meta: {fields: {title: 'string'}},
  93. data: [{title: 'title'}],
  94. },
  95. });
  96. });
  97. afterEach(function () {
  98. MockApiClient.clearMockResponses();
  99. });
  100. it('renders with Open in Discover button and opens the Query Selector Modal when clicked', async function () {
  101. const spy = jest.spyOn(modal, 'openDashboardWidgetQuerySelectorModal');
  102. renderWithProviders(
  103. <WidgetCard
  104. api={api}
  105. widget={multipleQueryWidget}
  106. selection={selection}
  107. isEditingDashboard={false}
  108. onDelete={() => undefined}
  109. onEdit={() => undefined}
  110. onDuplicate={() => undefined}
  111. renderErrorMessage={() => undefined}
  112. showContextMenu
  113. widgetLimitReached={false}
  114. widgetLegendState={widgetLegendState}
  115. />
  116. );
  117. await userEvent.click(await screen.findByLabelText('Widget actions'));
  118. await userEvent.click(screen.getByRole('menuitemradio', {name: 'Open in Discover'}));
  119. expect(spy).toHaveBeenCalledWith({
  120. isMetricsData: false,
  121. organization,
  122. widget: multipleQueryWidget,
  123. });
  124. });
  125. it('renders with Open in Discover button', async function () {
  126. renderWithProviders(
  127. <WidgetCard
  128. api={api}
  129. widget={{...multipleQueryWidget, queries: [multipleQueryWidget.queries[0]]}}
  130. selection={selection}
  131. isEditingDashboard={false}
  132. onDelete={() => undefined}
  133. onEdit={() => undefined}
  134. onDuplicate={() => undefined}
  135. renderErrorMessage={() => undefined}
  136. showContextMenu
  137. widgetLimitReached={false}
  138. widgetLegendState={widgetLegendState}
  139. />
  140. );
  141. await userEvent.click(await screen.findByLabelText('Widget actions'));
  142. expect(screen.getByRole('link', {name: 'Open in Discover'})).toHaveAttribute(
  143. 'href',
  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('renders widget description in dashboard', async function () {
  148. renderWithProviders(
  149. <WidgetCard
  150. api={api}
  151. widget={multipleQueryWidget}
  152. selection={selection}
  153. isEditingDashboard={false}
  154. onDelete={() => undefined}
  155. onEdit={() => undefined}
  156. onDuplicate={() => undefined}
  157. renderErrorMessage={() => undefined}
  158. showContextMenu
  159. widgetLimitReached={false}
  160. widgetLegendState={widgetLegendState}
  161. />
  162. );
  163. await userEvent.hover(await screen.findByLabelText('Widget description'));
  164. expect(await screen.findByText('Valid widget description')).toBeInTheDocument();
  165. });
  166. it('renders Discover button with prepended fields pulled from equations', async function () {
  167. renderWithProviders(
  168. <WidgetCard
  169. api={api}
  170. widget={{
  171. ...multipleQueryWidget,
  172. queries: [
  173. {
  174. ...multipleQueryWidget.queries[0],
  175. fields: [
  176. 'equation|(count() + failure_count()) / count_if(transaction.duration,equals,300)',
  177. ],
  178. columns: [],
  179. aggregates: [
  180. 'equation|(count() + failure_count()) / count_if(transaction.duration,equals,300)',
  181. ],
  182. },
  183. ],
  184. }}
  185. selection={selection}
  186. isEditingDashboard={false}
  187. onDelete={() => undefined}
  188. onEdit={() => undefined}
  189. onDuplicate={() => undefined}
  190. renderErrorMessage={() => undefined}
  191. showContextMenu
  192. widgetLimitReached={false}
  193. widgetLegendState={widgetLegendState}
  194. />
  195. );
  196. await userEvent.click(await screen.findByLabelText('Widget actions'));
  197. expect(screen.getByRole('link', {name: 'Open in Discover'})).toHaveAttribute(
  198. 'href',
  199. '/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'
  200. );
  201. });
  202. it('renders button to open Discover with Top N', async function () {
  203. renderWithProviders(
  204. <WidgetCard
  205. api={api}
  206. widget={{
  207. ...multipleQueryWidget,
  208. displayType: DisplayType.TOP_N,
  209. queries: [
  210. {
  211. ...multipleQueryWidget.queries[0],
  212. fields: ['transaction', 'count()'],
  213. columns: ['transaction'],
  214. aggregates: ['count()'],
  215. },
  216. ],
  217. }}
  218. selection={selection}
  219. isEditingDashboard={false}
  220. onDelete={() => undefined}
  221. onEdit={() => undefined}
  222. onDuplicate={() => undefined}
  223. renderErrorMessage={() => undefined}
  224. showContextMenu
  225. widgetLimitReached={false}
  226. widgetLegendState={widgetLegendState}
  227. />
  228. );
  229. await userEvent.click(await screen.findByLabelText('Widget actions'));
  230. expect(screen.getByRole('link', {name: 'Open in Discover'})).toHaveAttribute(
  231. 'href',
  232. '/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'
  233. );
  234. });
  235. it('allows Open in Discover when the widget contains custom measurements', async function () {
  236. renderWithProviders(
  237. <WidgetCard
  238. api={api}
  239. widget={{
  240. ...multipleQueryWidget,
  241. displayType: DisplayType.LINE,
  242. queries: [
  243. {
  244. ...multipleQueryWidget.queries[0],
  245. conditions: '',
  246. fields: [],
  247. columns: [],
  248. aggregates: ['p99(measurements.custom.measurement)'],
  249. },
  250. ],
  251. }}
  252. selection={selection}
  253. isEditingDashboard={false}
  254. onDelete={() => undefined}
  255. onEdit={() => undefined}
  256. onDuplicate={() => undefined}
  257. renderErrorMessage={() => undefined}
  258. showContextMenu
  259. widgetLimitReached={false}
  260. widgetLegendState={widgetLegendState}
  261. />
  262. );
  263. await userEvent.click(await screen.findByLabelText('Widget actions'));
  264. expect(screen.getByRole('link', {name: 'Open in Discover'})).toHaveAttribute(
  265. 'href',
  266. '/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'
  267. );
  268. });
  269. it('calls onDuplicate when Duplicate Widget is clicked', async function () {
  270. const mock = jest.fn();
  271. renderWithProviders(
  272. <WidgetCard
  273. api={api}
  274. widget={{
  275. ...multipleQueryWidget,
  276. displayType: DisplayType.AREA,
  277. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  278. }}
  279. selection={selection}
  280. isEditingDashboard={false}
  281. onDelete={() => undefined}
  282. onEdit={() => undefined}
  283. onDuplicate={mock}
  284. renderErrorMessage={() => undefined}
  285. showContextMenu
  286. widgetLimitReached={false}
  287. widgetLegendState={widgetLegendState}
  288. />
  289. );
  290. await userEvent.click(await screen.findByLabelText('Widget actions'));
  291. await userEvent.click(screen.getByRole('menuitemradio', {name: 'Duplicate Widget'}));
  292. expect(mock).toHaveBeenCalledTimes(1);
  293. });
  294. it('does not add duplicate widgets if max widget is reached', async function () {
  295. const mock = jest.fn();
  296. renderWithProviders(
  297. <WidgetCard
  298. api={api}
  299. widget={{
  300. ...multipleQueryWidget,
  301. displayType: DisplayType.AREA,
  302. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  303. }}
  304. selection={selection}
  305. isEditingDashboard={false}
  306. onDelete={() => undefined}
  307. onEdit={() => undefined}
  308. onDuplicate={mock}
  309. renderErrorMessage={() => undefined}
  310. showContextMenu
  311. widgetLegendState={widgetLegendState}
  312. widgetLimitReached
  313. />
  314. );
  315. await userEvent.click(await screen.findByLabelText('Widget actions'));
  316. await userEvent.click(screen.getByRole('menuitemradio', {name: 'Duplicate Widget'}));
  317. expect(mock).toHaveBeenCalledTimes(0);
  318. });
  319. it('calls onEdit when Edit Widget is clicked', async function () {
  320. const mock = jest.fn();
  321. renderWithProviders(
  322. <WidgetCard
  323. api={api}
  324. widget={{
  325. ...multipleQueryWidget,
  326. displayType: DisplayType.AREA,
  327. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  328. }}
  329. selection={selection}
  330. isEditingDashboard={false}
  331. onDelete={() => undefined}
  332. onEdit={mock}
  333. onDuplicate={() => undefined}
  334. renderErrorMessage={() => undefined}
  335. showContextMenu
  336. widgetLimitReached={false}
  337. widgetLegendState={widgetLegendState}
  338. />
  339. );
  340. await userEvent.click(await screen.findByLabelText('Widget actions'));
  341. await userEvent.click(screen.getByRole('menuitemradio', {name: 'Edit Widget'}));
  342. expect(mock).toHaveBeenCalledTimes(1);
  343. });
  344. it('renders delete widget option', async function () {
  345. const mock = jest.fn();
  346. renderWithProviders(
  347. <WidgetCard
  348. api={api}
  349. widget={{
  350. ...multipleQueryWidget,
  351. displayType: DisplayType.AREA,
  352. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  353. }}
  354. selection={selection}
  355. isEditingDashboard={false}
  356. onDelete={mock}
  357. onEdit={() => undefined}
  358. onDuplicate={() => undefined}
  359. renderErrorMessage={() => undefined}
  360. showContextMenu
  361. widgetLimitReached={false}
  362. widgetLegendState={widgetLegendState}
  363. />
  364. );
  365. await userEvent.click(await screen.findByLabelText('Widget actions'));
  366. await userEvent.click(screen.getByRole('menuitemradio', {name: 'Delete Widget'}));
  367. // Confirm Modal
  368. renderGlobalModal();
  369. await screen.findByRole('dialog');
  370. await userEvent.click(screen.getByTestId('confirm-button'));
  371. expect(mock).toHaveBeenCalled();
  372. });
  373. it('calls events with a limit of 20 items', async function () {
  374. const mock = jest.fn();
  375. renderWithProviders(
  376. <WidgetCard
  377. api={api}
  378. widget={{
  379. ...multipleQueryWidget,
  380. displayType: DisplayType.TABLE,
  381. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  382. }}
  383. selection={selection}
  384. isEditingDashboard={false}
  385. onDelete={mock}
  386. onEdit={() => undefined}
  387. onDuplicate={() => undefined}
  388. renderErrorMessage={() => undefined}
  389. showContextMenu
  390. widgetLimitReached={false}
  391. tableItemLimit={20}
  392. widgetLegendState={widgetLegendState}
  393. />
  394. );
  395. await waitFor(() => {
  396. expect(eventsMock).toHaveBeenCalledWith(
  397. '/organizations/org-slug/events/',
  398. expect.objectContaining({
  399. query: expect.objectContaining({
  400. per_page: 20,
  401. }),
  402. })
  403. );
  404. });
  405. });
  406. it('calls events with a default limit of 5 items', async function () {
  407. const mock = jest.fn();
  408. renderWithProviders(
  409. <WidgetCard
  410. api={api}
  411. widget={{
  412. ...multipleQueryWidget,
  413. displayType: DisplayType.TABLE,
  414. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  415. }}
  416. selection={selection}
  417. isEditingDashboard={false}
  418. onDelete={mock}
  419. onEdit={() => undefined}
  420. onDuplicate={() => undefined}
  421. renderErrorMessage={() => undefined}
  422. showContextMenu
  423. widgetLimitReached={false}
  424. widgetLegendState={widgetLegendState}
  425. />
  426. );
  427. await waitFor(() => {
  428. expect(eventsMock).toHaveBeenCalledWith(
  429. '/organizations/org-slug/events/',
  430. expect.objectContaining({
  431. query: expect.objectContaining({
  432. per_page: 5,
  433. }),
  434. })
  435. );
  436. });
  437. });
  438. it('has sticky table headers', async function () {
  439. const tableWidget: Widget = {
  440. title: 'Table Widget',
  441. interval: '5m',
  442. displayType: DisplayType.TABLE,
  443. widgetType: WidgetType.DISCOVER,
  444. queries: [
  445. {
  446. conditions: '',
  447. fields: ['transaction', 'count()'],
  448. columns: ['transaction'],
  449. aggregates: ['count()'],
  450. name: 'Table',
  451. orderby: '',
  452. },
  453. ],
  454. };
  455. renderWithProviders(
  456. <WidgetCard
  457. api={api}
  458. widget={tableWidget}
  459. selection={selection}
  460. isEditingDashboard={false}
  461. onDelete={() => undefined}
  462. onEdit={() => undefined}
  463. onDuplicate={() => undefined}
  464. renderErrorMessage={() => undefined}
  465. showContextMenu
  466. widgetLimitReached={false}
  467. tableItemLimit={20}
  468. widgetLegendState={widgetLegendState}
  469. />
  470. );
  471. await waitFor(() => expect(eventsMock).toHaveBeenCalled());
  472. await waitFor(() =>
  473. expect(SimpleTableChart).toHaveBeenCalledWith(
  474. expect.objectContaining({stickyHeaders: true}),
  475. expect.anything()
  476. )
  477. );
  478. });
  479. it('calls release queries', function () {
  480. const widget: Widget = {
  481. title: 'Release Widget',
  482. interval: '5m',
  483. displayType: DisplayType.LINE,
  484. widgetType: WidgetType.RELEASE,
  485. queries: [],
  486. };
  487. renderWithProviders(
  488. <WidgetCard
  489. api={api}
  490. widget={widget}
  491. selection={selection}
  492. isEditingDashboard={false}
  493. onDelete={() => undefined}
  494. onEdit={() => undefined}
  495. onDuplicate={() => undefined}
  496. renderErrorMessage={() => undefined}
  497. showContextMenu
  498. widgetLimitReached={false}
  499. tableItemLimit={20}
  500. widgetLegendState={widgetLegendState}
  501. />
  502. );
  503. expect(ReleaseWidgetQueries).toHaveBeenCalledTimes(1);
  504. });
  505. it('opens the widget viewer modal when a widget has no id', async () => {
  506. const widget: Widget = {
  507. title: 'Widget',
  508. interval: '5m',
  509. displayType: DisplayType.LINE,
  510. widgetType: WidgetType.DISCOVER,
  511. queries: [],
  512. };
  513. renderWithProviders(
  514. <WidgetCard
  515. api={api}
  516. widget={widget}
  517. selection={selection}
  518. isEditingDashboard={false}
  519. onDelete={() => undefined}
  520. onEdit={() => undefined}
  521. onDuplicate={() => undefined}
  522. renderErrorMessage={() => undefined}
  523. showContextMenu
  524. widgetLimitReached={false}
  525. index="10"
  526. isPreview
  527. widgetLegendState={widgetLegendState}
  528. />
  529. );
  530. await userEvent.click(await screen.findByLabelText('Open Full-Screen View'));
  531. expect(router.push).toHaveBeenCalledWith(
  532. expect.objectContaining({pathname: '/mock-pathname/widget/10/'})
  533. );
  534. });
  535. it('renders chart using axis and tooltip formatters from custom measurement meta', async function () {
  536. const spy = jest.spyOn(LineChart, 'LineChart');
  537. const eventsStatsMock = MockApiClient.addMockResponse({
  538. url: '/organizations/org-slug/events-stats/',
  539. body: {
  540. data: [
  541. [
  542. 1658262600,
  543. [
  544. {
  545. count: 24,
  546. },
  547. ],
  548. ],
  549. ],
  550. meta: {
  551. fields: {
  552. time: 'date',
  553. p95_measurements_custom: 'duration',
  554. },
  555. units: {
  556. time: null,
  557. p95_measurements_custom: 'millisecond',
  558. },
  559. isMetricsData: true,
  560. tips: {},
  561. },
  562. },
  563. });
  564. renderWithProviders(
  565. <WidgetCard
  566. api={api}
  567. organization={organization}
  568. widget={{
  569. title: '',
  570. interval: '5m',
  571. widgetType: WidgetType.DISCOVER,
  572. displayType: DisplayType.LINE,
  573. queries: [
  574. {
  575. conditions: '',
  576. name: '',
  577. fields: [],
  578. columns: [],
  579. aggregates: ['p95(measurements.custom)'],
  580. orderby: '',
  581. },
  582. ],
  583. }}
  584. selection={selection}
  585. isEditingDashboard={false}
  586. onDelete={() => undefined}
  587. onEdit={() => undefined}
  588. onDuplicate={() => undefined}
  589. renderErrorMessage={() => undefined}
  590. showContextMenu
  591. widgetLimitReached={false}
  592. widgetLegendState={widgetLegendState}
  593. />
  594. );
  595. await waitFor(function () {
  596. expect(eventsStatsMock).toHaveBeenCalled();
  597. });
  598. await waitFor(() => {
  599. const mockCall = spy.mock.calls?.at(-1)?.[0];
  600. expect(mockCall?.tooltip).toBeDefined();
  601. // @ts-expect-error
  602. expect(mockCall?.yAxis.axisLabel.formatter(24, 'p95(measurements.custom)')).toEqual(
  603. '24ms'
  604. );
  605. });
  606. });
  607. it('renders label in seconds when there is a transition from seconds to minutes in the y axis', async function () {
  608. const spy = jest.spyOn(LineChart, 'LineChart');
  609. const eventsStatsMock = MockApiClient.addMockResponse({
  610. url: '/organizations/org-slug/events-stats/',
  611. body: {
  612. data: [
  613. [
  614. 1658262600,
  615. [
  616. {
  617. count: 40 * SECOND,
  618. },
  619. ],
  620. ],
  621. [
  622. 1658262601,
  623. [
  624. {
  625. count: 50 * SECOND,
  626. },
  627. ],
  628. ],
  629. [
  630. 1658262602,
  631. [
  632. {
  633. count: MINUTE,
  634. },
  635. ],
  636. ],
  637. [
  638. 1658262603,
  639. [
  640. {
  641. count: 1.3 * MINUTE,
  642. },
  643. ],
  644. ],
  645. ],
  646. meta: {
  647. fields: {
  648. time: 'date',
  649. p50_transaction_duration: 'duration',
  650. },
  651. units: {
  652. time: null,
  653. p50_transaction_duration: 'millisecond',
  654. },
  655. isMetricsData: false,
  656. tips: {},
  657. },
  658. },
  659. });
  660. renderWithProviders(
  661. <WidgetCard
  662. api={api}
  663. organization={organization}
  664. widget={{
  665. title: '',
  666. interval: '5m',
  667. widgetType: WidgetType.DISCOVER,
  668. displayType: DisplayType.LINE,
  669. queries: [
  670. {
  671. conditions: '',
  672. name: '',
  673. fields: [],
  674. columns: [],
  675. aggregates: ['p50(transaction.duration)'],
  676. orderby: '',
  677. },
  678. ],
  679. }}
  680. selection={selection}
  681. isEditingDashboard={false}
  682. onDelete={() => undefined}
  683. onEdit={() => undefined}
  684. onDuplicate={() => undefined}
  685. renderErrorMessage={() => undefined}
  686. showContextMenu
  687. widgetLimitReached={false}
  688. widgetLegendState={widgetLegendState}
  689. />
  690. );
  691. await waitFor(function () {
  692. expect(eventsStatsMock).toHaveBeenCalled();
  693. });
  694. await waitFor(() => {
  695. const mockCall = spy.mock.calls?.at(-1)?.[0];
  696. expect(mockCall?.yAxis).toBeDefined();
  697. expect(
  698. // @ts-expect-error
  699. mockCall?.yAxis.axisLabel.formatter(60000, 'p50(transaction.duration)')
  700. ).toEqual('60s');
  701. // @ts-expect-error
  702. expect(mockCall?.yAxis?.minInterval).toEqual(SECOND);
  703. });
  704. });
  705. it('displays indexed badge in preview mode', async function () {
  706. renderWithProviders(
  707. <WidgetCard
  708. api={api}
  709. organization={{
  710. ...organization,
  711. features: [...organization.features, 'dashboards-mep'],
  712. }}
  713. widget={multipleQueryWidget}
  714. selection={selection}
  715. isEditingDashboard={false}
  716. onDelete={() => undefined}
  717. onEdit={() => undefined}
  718. onDuplicate={() => undefined}
  719. renderErrorMessage={() => undefined}
  720. showContextMenu
  721. widgetLimitReached={false}
  722. isPreview
  723. widgetLegendState={widgetLegendState}
  724. />
  725. );
  726. expect(await screen.findByText('Indexed')).toBeInTheDocument();
  727. });
  728. it('displays the discover split warning icon when the dataset source is forced', async function () {
  729. const testWidget = {
  730. ...WidgetFixture(),
  731. datasetSource: DatasetSource.FORCED,
  732. widgetType: WidgetType.ERRORS,
  733. };
  734. renderWithProviders(
  735. <WidgetCard
  736. api={api}
  737. organization={organization}
  738. widget={testWidget}
  739. selection={selection}
  740. isEditingDashboard={false}
  741. onDelete={() => undefined}
  742. onEdit={() => undefined}
  743. onDuplicate={() => undefined}
  744. renderErrorMessage={() => undefined}
  745. showContextMenu
  746. widgetLimitReached={false}
  747. isPreview
  748. widgetLegendState={widgetLegendState}
  749. />
  750. );
  751. await userEvent.hover(screen.getByLabelText('Widget warnings'));
  752. expect(
  753. await screen.findByText(/We're splitting our datasets up/)
  754. ).toBeInTheDocument();
  755. });
  756. });