index.spec.tsx 24 KB

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