addDashboardWidgetModal.spec.jsx 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436
  1. import {browserHistory} from 'react-router';
  2. import {mountWithTheme} from 'sentry-test/enzyme';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {
  5. act,
  6. mountWithTheme as reactMountWithTheme,
  7. screen,
  8. userEvent,
  9. } from 'sentry-test/reactTestingLibrary';
  10. import {getOptionByLabel, openMenu, selectByLabel} from 'sentry-test/select-new';
  11. import {openDashboardWidgetLibraryModal} from 'sentry/actionCreators/modal';
  12. import AddDashboardWidgetModal from 'sentry/components/modals/addDashboardWidgetModal';
  13. import {t} from 'sentry/locale';
  14. import MetricsMetaStore from 'sentry/stores/metricsMetaStore';
  15. import MetricsTagStore from 'sentry/stores/metricsTagStore';
  16. import TagStore from 'sentry/stores/tagStore';
  17. import {SessionMetric} from 'sentry/utils/metrics/fields';
  18. import * as types from 'sentry/views/dashboardsV2/types';
  19. jest.mock('sentry/actionCreators/modal', () => ({
  20. openDashboardWidgetLibraryModal: jest.fn(),
  21. }));
  22. const stubEl = props => <div>{props.children}</div>;
  23. function mountModal({
  24. initialData,
  25. onAddWidget,
  26. onUpdateWidget,
  27. widget,
  28. dashboard,
  29. source,
  30. defaultWidgetQuery,
  31. displayType,
  32. defaultTableColumns,
  33. selectedWidgets,
  34. onAddLibraryWidget,
  35. }) {
  36. return mountWithTheme(
  37. <AddDashboardWidgetModal
  38. Header={stubEl}
  39. Footer={stubEl}
  40. Body={stubEl}
  41. organization={initialData.organization}
  42. onAddWidget={onAddWidget}
  43. onUpdateWidget={onUpdateWidget}
  44. widget={widget}
  45. dashboard={dashboard}
  46. closeModal={() => void 0}
  47. source={source || types.DashboardWidgetSource.DASHBOARDS}
  48. defaultWidgetQuery={defaultWidgetQuery}
  49. displayType={displayType}
  50. defaultTableColumns={defaultTableColumns}
  51. selectedWidgets={selectedWidgets}
  52. onAddLibraryWidget={onAddLibraryWidget}
  53. />,
  54. initialData.routerContext
  55. );
  56. }
  57. function mountModalWithRtl({initialData, onAddWidget, onUpdateWidget, widget, source}) {
  58. return reactMountWithTheme(
  59. <AddDashboardWidgetModal
  60. Header={stubEl}
  61. Body={stubEl}
  62. Footer={stubEl}
  63. CloseButton={stubEl}
  64. organization={initialData.organization}
  65. onAddWidget={onAddWidget}
  66. onUpdateWidget={onUpdateWidget}
  67. widget={widget}
  68. closeModal={() => void 0}
  69. source={source || types.DashboardWidgetSource.DASHBOARDS}
  70. />
  71. );
  72. }
  73. async function clickSubmit(wrapper) {
  74. // Click on submit.
  75. const button = wrapper.find('Button[data-test-id="add-widget"] button');
  76. button.simulate('click');
  77. // Wait for xhr to complete.
  78. return tick();
  79. }
  80. function getDisplayType(wrapper) {
  81. return wrapper.find('input[name="displayType"]');
  82. }
  83. function selectDashboard(wrapper, dashboard) {
  84. const input = wrapper.find('SelectControl[name="dashboard"]');
  85. input.props().onChange(dashboard);
  86. }
  87. async function setSearchConditions(el, query) {
  88. el.find('textarea')
  89. .simulate('change', {target: {value: query}})
  90. .getDOMNode()
  91. .setSelectionRange(query.length, query.length);
  92. await tick();
  93. await el.update();
  94. el.find('textarea').simulate('keydown', {key: 'Enter'});
  95. }
  96. describe('Modals -> AddDashboardWidgetModal', function () {
  97. const initialData = initializeOrg({
  98. organization: {
  99. features: ['performance-view', 'discover-query', 'issues-in-dashboards'],
  100. apdexThreshold: 400,
  101. },
  102. });
  103. const tags = [
  104. {name: 'browser.name', key: 'browser.name'},
  105. {name: 'custom-field', key: 'custom-field'},
  106. ];
  107. const metricsTags = [{key: 'environment'}, {key: 'release'}, {key: 'session.status'}];
  108. const metricsMeta = [
  109. {
  110. name: 'sentry.sessions.session',
  111. type: 'counter',
  112. operations: ['sum'],
  113. unit: null,
  114. },
  115. {
  116. name: 'sentry.sessions.session.error',
  117. type: 'set',
  118. operations: ['count_unique'],
  119. unit: null,
  120. },
  121. {
  122. name: 'sentry.sessions.user',
  123. type: 'set',
  124. operations: ['count_unique'],
  125. unit: null,
  126. },
  127. {
  128. name: 'not.on.allow.list',
  129. type: 'set',
  130. operations: ['count_unique'],
  131. unit: null,
  132. },
  133. ];
  134. const dashboard = TestStubs.Dashboard([], {
  135. id: '1',
  136. title: 'Test Dashboard',
  137. widgetDisplay: ['area'],
  138. });
  139. let eventsStatsMock, metricsDataMock;
  140. beforeEach(function () {
  141. TagStore.onLoadTagsSuccess(tags);
  142. MetricsTagStore.onLoadTagsSuccess(metricsTags);
  143. MetricsMetaStore.onLoadSuccess(metricsMeta);
  144. MockApiClient.addMockResponse({
  145. url: '/organizations/org-slug/dashboards/widgets/',
  146. method: 'POST',
  147. statusCode: 200,
  148. body: [],
  149. });
  150. eventsStatsMock = MockApiClient.addMockResponse({
  151. url: '/organizations/org-slug/events-stats/',
  152. body: [],
  153. });
  154. MockApiClient.addMockResponse({
  155. url: '/organizations/org-slug/eventsv2/',
  156. body: {data: [{'event.type': 'error'}], meta: {'event.type': 'string'}},
  157. });
  158. MockApiClient.addMockResponse({
  159. url: '/organizations/org-slug/events-geo/',
  160. body: {data: [], meta: {}},
  161. });
  162. MockApiClient.addMockResponse({
  163. url: '/organizations/org-slug/recent-searches/',
  164. body: [],
  165. });
  166. MockApiClient.addMockResponse({
  167. url: '/organizations/org-slug/dashboards/',
  168. body: [dashboard],
  169. });
  170. MockApiClient.addMockResponse({
  171. url: '/organizations/org-slug/issues/',
  172. body: [],
  173. });
  174. MockApiClient.addMockResponse({
  175. url: '/organizations/org-slug/metrics/tags/',
  176. body: [{key: 'environment'}, {key: 'release'}, {key: 'session.status'}],
  177. });
  178. metricsDataMock = MockApiClient.addMockResponse({
  179. method: 'GET',
  180. url: '/organizations/org-slug/metrics/data/',
  181. body: TestStubs.MetricsField({field: SessionMetric.SENTRY_SESSIONS_USER}),
  182. });
  183. });
  184. afterEach(() => {
  185. MockApiClient.clearMockResponses();
  186. });
  187. it('redirects correctly when creating a new dashboard', async function () {
  188. const wrapper = mountModal({
  189. initialData,
  190. source: types.DashboardWidgetSource.DISCOVERV2,
  191. });
  192. await tick();
  193. await wrapper.update();
  194. selectDashboard(wrapper, {label: t('+ Create New Dashboard'), value: 'new'});
  195. await clickSubmit(wrapper);
  196. expect(browserHistory.push).toHaveBeenCalledWith(
  197. expect.objectContaining({
  198. pathname: '/organizations/org-slug/dashboards/new/',
  199. })
  200. );
  201. wrapper.unmount();
  202. });
  203. it('redirects correctly when choosing an existing dashboard', async function () {
  204. const wrapper = mountModal({
  205. initialData,
  206. source: types.DashboardWidgetSource.DISCOVERV2,
  207. });
  208. await tick();
  209. await wrapper.update();
  210. selectDashboard(wrapper, {label: t('Test Dashboard'), value: '1'});
  211. await clickSubmit(wrapper);
  212. expect(browserHistory.push).toHaveBeenCalledWith(
  213. expect.objectContaining({
  214. pathname: '/organizations/org-slug/dashboard/1/',
  215. })
  216. );
  217. wrapper.unmount();
  218. });
  219. it('disables dashboards with max widgets', async function () {
  220. types.MAX_WIDGETS = 1;
  221. const wrapper = mountModal({
  222. initialData,
  223. source: types.DashboardWidgetSource.DISCOVERV2,
  224. });
  225. await tick();
  226. await wrapper.update();
  227. openMenu(wrapper, {name: 'dashboard', control: true});
  228. const input = wrapper.find('SelectControl[name="dashboard"]');
  229. expect(input.find('Option Option')).toHaveLength(2);
  230. expect(input.find('Option Option').at(0).props().isDisabled).toBe(false);
  231. expect(input.find('Option Option').at(1).props().isDisabled).toBe(true);
  232. wrapper.unmount();
  233. });
  234. it('can update the title', async function () {
  235. let widget = undefined;
  236. const wrapper = mountModal({
  237. initialData,
  238. onAddWidget: data => (widget = data),
  239. });
  240. const input = wrapper.find('Input[name="title"] input');
  241. input.simulate('change', {target: {value: 'Unique Users'}});
  242. await clickSubmit(wrapper);
  243. expect(widget.title).toEqual('Unique Users');
  244. wrapper.unmount();
  245. });
  246. it('can add conditions', async function () {
  247. jest.useFakeTimers();
  248. let widget = undefined;
  249. const wrapper = mountModal({
  250. initialData,
  251. onAddWidget: data => (widget = data),
  252. });
  253. // Change the search text on the first query.
  254. const input = wrapper.find('#smart-search-input').first();
  255. input.simulate('change', {target: {value: 'color:blue'}}).simulate('blur');
  256. jest.runAllTimers();
  257. jest.useRealTimers();
  258. await clickSubmit(wrapper);
  259. expect(widget.queries).toHaveLength(1);
  260. expect(widget.queries[0].conditions).toEqual('color:blue');
  261. wrapper.unmount();
  262. });
  263. it('can choose a field', async function () {
  264. let widget = undefined;
  265. const wrapper = mountModal({
  266. initialData,
  267. onAddWidget: data => (widget = data),
  268. });
  269. // No delete button as there is only one field.
  270. expect(wrapper.find('IconDelete')).toHaveLength(0);
  271. selectByLabel(wrapper, 'p95(\u2026)', {name: 'field', at: 0, control: true});
  272. await clickSubmit(wrapper);
  273. expect(widget.queries).toHaveLength(1);
  274. expect(widget.queries[0].fields).toEqual(['p95(transaction.duration)']);
  275. wrapper.unmount();
  276. });
  277. it('can add additional fields', async function () {
  278. let widget = undefined;
  279. const wrapper = mountModal({
  280. initialData,
  281. onAddWidget: data => (widget = data),
  282. });
  283. // Select Line chart display
  284. selectByLabel(wrapper, 'Line Chart', {name: 'displayType', at: 0, control: true});
  285. // Click the add button
  286. const add = wrapper.find('button[aria-label="Add Overlay"]');
  287. add.simulate('click');
  288. wrapper.update();
  289. // Should be another field input.
  290. expect(wrapper.find('QueryField')).toHaveLength(2);
  291. selectByLabel(wrapper, 'p95(\u2026)', {name: 'field', at: 1, control: true});
  292. await clickSubmit(wrapper);
  293. expect(widget.queries).toHaveLength(1);
  294. expect(widget.queries[0].fields).toEqual(['count()', 'p95(transaction.duration)']);
  295. wrapper.unmount();
  296. });
  297. it('can add equation fields', async function () {
  298. let widget = undefined;
  299. const wrapper = mountModal({
  300. initialData,
  301. onAddWidget: data => (widget = data),
  302. });
  303. // Select Line chart display
  304. selectByLabel(wrapper, 'Line Chart', {name: 'displayType', at: 0, control: true});
  305. // Click the add button
  306. const add = wrapper.find('button[aria-label="Add an Equation"]');
  307. add.simulate('click');
  308. wrapper.update();
  309. // Should be another field input.
  310. expect(wrapper.find('QueryField')).toHaveLength(2);
  311. expect(wrapper.find('ArithmeticInput')).toHaveLength(1);
  312. wrapper
  313. .find('QueryFieldWrapper input[name="arithmetic"]')
  314. .simulate('change', {target: {value: 'count() + 100'}})
  315. .simulate('blur');
  316. wrapper.update();
  317. await clickSubmit(wrapper);
  318. expect(widget.queries).toHaveLength(1);
  319. expect(widget.queries[0].fields).toEqual(['count()', 'equation|count() + 100']);
  320. wrapper.unmount();
  321. });
  322. it('additional fields get added to new seach filters', async function () {
  323. MockApiClient.addMockResponse({
  324. url: '/organizations/org-slug/recent-searches/',
  325. method: 'POST',
  326. body: [],
  327. });
  328. let widget = undefined;
  329. const wrapper = mountModal({
  330. initialData,
  331. onAddWidget: data => (widget = data),
  332. });
  333. // Select Line chart display
  334. selectByLabel(wrapper, 'Line Chart', {name: 'displayType', at: 0, control: true});
  335. // Click the add button
  336. const add = wrapper.find('button[aria-label="Add Overlay"]');
  337. add.simulate('click');
  338. wrapper.update();
  339. // Should be another field input.
  340. expect(wrapper.find('QueryField')).toHaveLength(2);
  341. selectByLabel(wrapper, 'p95(\u2026)', {name: 'field', at: 1, control: true});
  342. await clickSubmit(wrapper);
  343. expect(widget.queries).toHaveLength(1);
  344. expect(widget.queries[0].fields).toEqual(['count()', 'p95(transaction.duration)']);
  345. // Add another search filter
  346. const addQuery = wrapper.find('button[aria-label="Add Query"]');
  347. addQuery.simulate('click');
  348. wrapper.update();
  349. // Set second query search conditions
  350. const secondSearchBar = wrapper.find('SearchConditionsWrapper StyledSearchBar').at(1);
  351. await setSearchConditions(secondSearchBar, 'event.type:error');
  352. // Set second query legend alias
  353. wrapper
  354. .find('SearchConditionsWrapper input[placeholder="Legend Alias"]')
  355. .at(1)
  356. .simulate('change', {target: {value: 'Errors'}});
  357. // Save widget
  358. await clickSubmit(wrapper);
  359. expect(widget.queries[0]).toMatchObject({
  360. name: '',
  361. conditions: '',
  362. fields: ['count()', 'p95(transaction.duration)'],
  363. });
  364. expect(widget.queries[1]).toMatchObject({
  365. name: 'Errors',
  366. conditions: 'event.type:error',
  367. fields: ['count()', 'p95(transaction.duration)'],
  368. });
  369. wrapper.unmount();
  370. });
  371. it('can add and delete additional queries', async function () {
  372. MockApiClient.addMockResponse({
  373. url: '/organizations/org-slug/tags/event.type/values/',
  374. body: [{count: 2, name: 'Nvidia 1080ti'}],
  375. });
  376. MockApiClient.addMockResponse({
  377. url: '/organizations/org-slug/recent-searches/',
  378. method: 'POST',
  379. body: [],
  380. });
  381. let widget = undefined;
  382. const wrapper = mountModal({
  383. initialData,
  384. onAddWidget: data => (widget = data),
  385. });
  386. // Select Line chart display
  387. selectByLabel(wrapper, 'Line Chart', {name: 'displayType', at: 0, control: true});
  388. // Set first query search conditions
  389. await setSearchConditions(
  390. wrapper.find('SearchConditionsWrapper StyledSearchBar'),
  391. 'event.type:transaction'
  392. );
  393. // Set first query legend alias
  394. wrapper
  395. .find('SearchConditionsWrapper input[placeholder="Legend Alias"]')
  396. .simulate('change', {target: {value: 'Transactions'}});
  397. // Click the "Add Query" button twice
  398. const addQuery = wrapper.find('button[aria-label="Add Query"]');
  399. addQuery.simulate('click');
  400. wrapper.update();
  401. addQuery.simulate('click');
  402. wrapper.update();
  403. // Expect three search bars
  404. expect(wrapper.find('StyledSearchBar')).toHaveLength(3);
  405. // Expect "Add Query" button to be hidden since we're limited to at most 3 search conditions
  406. expect(wrapper.find('button[aria-label="Add Query"]')).toHaveLength(0);
  407. // Delete second query
  408. expect(wrapper.find('button[aria-label="Remove query"]')).toHaveLength(3);
  409. wrapper.find('button[aria-label="Remove query"]').at(1).simulate('click');
  410. wrapper.update();
  411. // Expect "Add Query" button to be shown again
  412. expect(wrapper.find('button[aria-label="Add Query"]')).toHaveLength(1);
  413. // Set second query search conditions
  414. const secondSearchBar = wrapper.find('SearchConditionsWrapper StyledSearchBar').at(1);
  415. await setSearchConditions(secondSearchBar, 'event.type:error');
  416. // Set second query legend alias
  417. wrapper
  418. .find('SearchConditionsWrapper input[placeholder="Legend Alias"]')
  419. .at(1)
  420. .simulate('change', {target: {value: 'Errors'}});
  421. // Save widget
  422. await clickSubmit(wrapper);
  423. expect(widget.queries).toHaveLength(2);
  424. expect(widget.queries[0]).toMatchObject({
  425. name: 'Transactions',
  426. conditions: 'event.type:transaction',
  427. fields: ['count()'],
  428. });
  429. expect(widget.queries[1]).toMatchObject({
  430. name: 'Errors',
  431. conditions: 'event.type:error',
  432. fields: ['count()'],
  433. });
  434. wrapper.unmount();
  435. });
  436. it('can respond to validation feedback', async function () {
  437. MockApiClient.addMockResponse({
  438. url: '/organizations/org-slug/dashboards/widgets/',
  439. method: 'POST',
  440. statusCode: 400,
  441. body: {
  442. title: ['This field is required'],
  443. queries: [{conditions: ['Invalid value']}],
  444. },
  445. });
  446. let widget = undefined;
  447. const wrapper = mountModal({
  448. initialData,
  449. onAddWidget: data => (widget = data),
  450. });
  451. await clickSubmit(wrapper);
  452. await wrapper.update();
  453. // API request should fail and not add widget.
  454. expect(widget).toBeUndefined();
  455. const errors = wrapper.find('FieldErrorReason');
  456. expect(errors).toHaveLength(2);
  457. // Nested object error should display
  458. const conditionError = wrapper.find('WidgetQueriesForm FieldErrorReason');
  459. expect(conditionError).toHaveLength(1);
  460. wrapper.unmount();
  461. });
  462. it('can edit a widget', async function () {
  463. let widget = {
  464. id: '9',
  465. title: 'Errors over time',
  466. interval: '5m',
  467. displayType: 'line',
  468. queries: [
  469. {
  470. id: '9',
  471. name: 'errors',
  472. conditions: 'event.type:error',
  473. fields: ['count()', 'count_unique(id)'],
  474. },
  475. {
  476. id: '9',
  477. name: 'csp',
  478. conditions: 'event.type:csp',
  479. fields: ['count()', 'count_unique(id)'],
  480. },
  481. ],
  482. };
  483. const onAdd = jest.fn();
  484. const wrapper = mountModal({
  485. initialData,
  486. widget,
  487. onAddWidget: onAdd,
  488. onUpdateWidget: data => {
  489. widget = data;
  490. },
  491. });
  492. // Should be in edit 'mode'
  493. const heading = wrapper.find('h4');
  494. expect(heading.text()).toContain('Edit');
  495. // Should set widget data up.
  496. const title = wrapper.find('Input[name="title"]');
  497. expect(title.props().value).toEqual(widget.title);
  498. expect(wrapper.find('input[name="displayType"]').props().value).toEqual(
  499. widget.displayType
  500. );
  501. expect(wrapper.find('WidgetQueriesForm')).toHaveLength(1);
  502. expect(wrapper.find('StyledSearchBar')).toHaveLength(2);
  503. expect(wrapper.find('QueryField')).toHaveLength(2);
  504. // Expect events-stats endpoint to be called for each search conditions with
  505. // the same y-axis parameters
  506. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  507. 1,
  508. '/organizations/org-slug/events-stats/',
  509. expect.objectContaining({
  510. query: expect.objectContaining({
  511. query: 'event.type:error',
  512. yAxis: ['count()', 'count_unique(id)'],
  513. }),
  514. })
  515. );
  516. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  517. 2,
  518. '/organizations/org-slug/events-stats/',
  519. expect.objectContaining({
  520. query: expect.objectContaining({
  521. query: 'event.type:csp',
  522. yAxis: ['count()', 'count_unique(id)'],
  523. }),
  524. })
  525. );
  526. title.simulate('change', {target: {value: 'New title'}});
  527. await clickSubmit(wrapper);
  528. expect(onAdd).not.toHaveBeenCalled();
  529. expect(widget.title).toEqual('New title');
  530. expect(eventsStatsMock).toHaveBeenCalledTimes(2);
  531. wrapper.unmount();
  532. });
  533. it('renders column inputs for table widgets', async function () {
  534. MockApiClient.addMockResponse({
  535. url: '/organizations/org-slug/eventsv2/',
  536. method: 'GET',
  537. statusCode: 200,
  538. body: {
  539. meta: {},
  540. data: [],
  541. },
  542. });
  543. let widget = {
  544. id: '9',
  545. title: 'sdk usage',
  546. interval: '5m',
  547. displayType: 'table',
  548. queries: [
  549. {
  550. id: '9',
  551. name: 'errors',
  552. conditions: 'event.type:error',
  553. fields: ['sdk.name', 'count()'],
  554. orderby: '',
  555. },
  556. ],
  557. };
  558. const wrapper = mountModal({
  559. initialData,
  560. widget,
  561. onAddWidget: jest.fn(),
  562. onUpdateWidget: data => {
  563. widget = data;
  564. },
  565. });
  566. // Should be in edit 'mode'
  567. const heading = wrapper.find('h4').first();
  568. expect(heading.text()).toContain('Edit');
  569. // Should set widget data up.
  570. const title = wrapper.find('Input[name="title"]');
  571. expect(title.props().value).toEqual(widget.title);
  572. expect(wrapper.find('input[name="displayType"]').props().value).toEqual(
  573. widget.displayType
  574. );
  575. expect(wrapper.find('WidgetQueriesForm')).toHaveLength(1);
  576. // Should have an orderby select
  577. expect(wrapper.find('WidgetQueriesForm SelectControl[name="orderby"]')).toHaveLength(
  578. 1
  579. );
  580. // Add a column, and choose a value,
  581. wrapper.find('button[aria-label="Add a Column"]').simulate('click');
  582. await wrapper.update();
  583. selectByLabel(wrapper, 'trace', {name: 'field', at: 2, control: true});
  584. await wrapper.update();
  585. await clickSubmit(wrapper);
  586. // A new field should be added.
  587. expect(widget.queries[0].fields).toHaveLength(3);
  588. expect(widget.queries[0].fields[2]).toEqual('trace');
  589. wrapper.unmount();
  590. });
  591. it('uses count() columns if there are no aggregate fields remaining when switching from table to chart', async function () {
  592. let widget = undefined;
  593. const wrapper = mountModal({
  594. initialData,
  595. onAddWidget: data => (widget = data),
  596. });
  597. // No delete button as there is only one field.
  598. expect(wrapper.find('IconDelete')).toHaveLength(0);
  599. // Select Table display
  600. selectByLabel(wrapper, 'Table', {name: 'displayType', at: 0, control: true});
  601. expect(getDisplayType(wrapper).props().value).toEqual('table');
  602. // Add field column
  603. selectByLabel(wrapper, 'event.type', {name: 'field', at: 0, control: true});
  604. let fieldColumn = wrapper.find('input[name="field"]');
  605. expect(fieldColumn.props().value).toEqual({
  606. kind: 'field',
  607. meta: {dataType: 'string', name: 'event.type'},
  608. });
  609. // Select Line chart display
  610. selectByLabel(wrapper, 'Line Chart', {name: 'displayType', at: 0, control: true});
  611. expect(getDisplayType(wrapper).props().value).toEqual('line');
  612. // Expect event.type field to be converted to count()
  613. fieldColumn = wrapper.find('input[name="field"]');
  614. expect(fieldColumn.props().value).toEqual({
  615. kind: 'function',
  616. meta: {name: 'count', parameters: []},
  617. });
  618. await clickSubmit(wrapper);
  619. expect(widget.queries).toHaveLength(1);
  620. expect(widget.queries[0].fields).toEqual(['count()']);
  621. wrapper.unmount();
  622. });
  623. it('should filter out non-aggregate fields when switching from table to chart', async function () {
  624. let widget = undefined;
  625. const wrapper = mountModal({
  626. initialData,
  627. onAddWidget: data => (widget = data),
  628. });
  629. // No delete button as there is only one field.
  630. expect(wrapper.find('IconDelete')).toHaveLength(0);
  631. // Select Table display
  632. selectByLabel(wrapper, 'Table', {name: 'displayType', at: 0, control: true});
  633. expect(getDisplayType(wrapper).props().value).toEqual('table');
  634. // Click the add button
  635. const add = wrapper.find('button[aria-label="Add a Column"]');
  636. add.simulate('click');
  637. wrapper.update();
  638. // Add columns
  639. selectByLabel(wrapper, 'event.type', {name: 'field', at: 0, control: true});
  640. let fieldColumn = wrapper.find('input[name="field"]').at(0);
  641. expect(fieldColumn.props().value).toEqual({
  642. kind: 'field',
  643. meta: {dataType: 'string', name: 'event.type'},
  644. });
  645. selectByLabel(wrapper, 'p95(\u2026)', {name: 'field', at: 1, control: true});
  646. fieldColumn = wrapper.find('input[name="field"]').at(1);
  647. expect(fieldColumn.props().value).toMatchObject({
  648. kind: 'function',
  649. meta: {
  650. name: 'p95',
  651. parameters: [{defaultValue: 'transaction.duration', kind: 'column'}],
  652. },
  653. });
  654. // Select Line chart display
  655. selectByLabel(wrapper, 'Line Chart', {name: 'displayType', at: 0, control: true});
  656. expect(getDisplayType(wrapper).props().value).toEqual('line');
  657. // Expect event.type field to be converted to count()
  658. fieldColumn = wrapper.find('input[name="field"]');
  659. expect(fieldColumn.length).toEqual(1);
  660. expect(fieldColumn.props().value).toMatchObject({
  661. kind: 'function',
  662. meta: {
  663. name: 'p95',
  664. parameters: [{defaultValue: 'transaction.duration', kind: 'column'}],
  665. },
  666. });
  667. await clickSubmit(wrapper);
  668. expect(widget.queries).toHaveLength(1);
  669. expect(widget.queries[0].fields).toEqual(['p95(transaction.duration)']);
  670. wrapper.unmount();
  671. });
  672. it('should filter non-legal y-axis choices for timeseries widget charts', async function () {
  673. let widget = undefined;
  674. const wrapper = mountModal({
  675. initialData,
  676. onAddWidget: data => (widget = data),
  677. });
  678. // Select Line chart display
  679. selectByLabel(wrapper, 'Line Chart', {name: 'displayType', at: 0, control: true});
  680. // No delete button as there is only one field.
  681. expect(wrapper.find('IconDelete')).toHaveLength(0);
  682. selectByLabel(wrapper, 'any(\u2026)', {
  683. name: 'field',
  684. at: 0,
  685. control: true,
  686. });
  687. // Expect user.display to not be an available parameter option for any()
  688. // for line (timeseries) widget charts
  689. const option = getOptionByLabel(wrapper, 'user.display', {
  690. name: 'parameter',
  691. at: 0,
  692. control: true,
  693. });
  694. expect(option.exists()).toEqual(false);
  695. // Be able to choose a numeric-like option for any()
  696. selectByLabel(wrapper, 'measurements.lcp', {
  697. name: 'parameter',
  698. at: 0,
  699. control: true,
  700. });
  701. await clickSubmit(wrapper);
  702. expect(widget.displayType).toEqual('line');
  703. expect(widget.queries).toHaveLength(1);
  704. expect(widget.queries[0].fields).toEqual(['any(measurements.lcp)']);
  705. wrapper.unmount();
  706. });
  707. it('should not filter y-axis choices for big number widget charts', async function () {
  708. let widget = undefined;
  709. const wrapper = mountModal({
  710. initialData,
  711. onAddWidget: data => (widget = data),
  712. });
  713. // No delete button as there is only one field.
  714. expect(wrapper.find('IconDelete')).toHaveLength(0);
  715. // Select Big number display
  716. selectByLabel(wrapper, 'Big Number', {name: 'displayType', at: 0, control: true});
  717. expect(getDisplayType(wrapper).props().value).toEqual('big_number');
  718. selectByLabel(wrapper, 'count_unique(\u2026)', {
  719. name: 'field',
  720. at: 0,
  721. control: true,
  722. });
  723. // Be able to choose a non numeric-like option for count_unique()
  724. selectByLabel(wrapper, 'user.display', {
  725. name: 'parameter',
  726. at: 0,
  727. control: true,
  728. });
  729. await clickSubmit(wrapper);
  730. expect(widget.displayType).toEqual('big_number');
  731. expect(widget.queries).toHaveLength(1);
  732. expect(widget.queries[0].fields).toEqual(['count_unique(user.display)']);
  733. wrapper.unmount();
  734. });
  735. it('should filter y-axis choices for world map widget charts', async function () {
  736. let widget = undefined;
  737. const wrapper = mountModal({
  738. initialData,
  739. onAddWidget: data => (widget = data),
  740. });
  741. // No delete button as there is only one field.
  742. expect(wrapper.find('IconDelete')).toHaveLength(0);
  743. // Select World Map display
  744. selectByLabel(wrapper, 'World Map', {name: 'displayType', at: 0, control: true});
  745. expect(getDisplayType(wrapper).props().value).toEqual('world_map');
  746. // Choose any()
  747. selectByLabel(wrapper, 'any(\u2026)', {
  748. name: 'field',
  749. at: 0,
  750. control: true,
  751. });
  752. // user.display should be filtered out for any()
  753. const option = getOptionByLabel(wrapper, 'user.display', {
  754. name: 'parameter',
  755. at: 0,
  756. control: true,
  757. });
  758. expect(option.exists()).toEqual(false);
  759. selectByLabel(wrapper, 'measurements.lcp', {
  760. name: 'parameter',
  761. at: 0,
  762. control: true,
  763. });
  764. // Choose count_unique()
  765. selectByLabel(wrapper, 'count_unique(\u2026)', {
  766. name: 'field',
  767. at: 0,
  768. control: true,
  769. });
  770. // user.display not should be filtered out for count_unique()
  771. selectByLabel(wrapper, 'user.display', {
  772. name: 'parameter',
  773. at: 0,
  774. control: true,
  775. });
  776. // Be able to choose a numeric-like option
  777. selectByLabel(wrapper, 'measurements.lcp', {
  778. name: 'parameter',
  779. at: 0,
  780. control: true,
  781. });
  782. await clickSubmit(wrapper);
  783. expect(widget.displayType).toEqual('world_map');
  784. expect(widget.queries).toHaveLength(1);
  785. expect(widget.queries[0].fields).toEqual(['count_unique(measurements.lcp)']);
  786. wrapper.unmount();
  787. });
  788. it('should filter y-axis choices by output type when switching from big number to line chart', async function () {
  789. let widget = undefined;
  790. const wrapper = mountModal({
  791. initialData,
  792. onAddWidget: data => (widget = data),
  793. });
  794. // No delete button as there is only one field.
  795. expect(wrapper.find('IconDelete')).toHaveLength(0);
  796. // Select Big Number display
  797. selectByLabel(wrapper, 'Big Number', {name: 'displayType', at: 0, control: true});
  798. expect(getDisplayType(wrapper).props().value).toEqual('big_number');
  799. // Choose any()
  800. selectByLabel(wrapper, 'any(\u2026)', {
  801. name: 'field',
  802. at: 0,
  803. control: true,
  804. });
  805. selectByLabel(wrapper, 'id', {
  806. name: 'parameter',
  807. at: 0,
  808. control: true,
  809. });
  810. // Select Line chart display
  811. selectByLabel(wrapper, 'Line Chart', {name: 'displayType', at: 0, control: true});
  812. expect(getDisplayType(wrapper).props().value).toEqual('line');
  813. // Expect event.type field to be converted to count()
  814. const fieldColumn = wrapper.find('input[name="field"]');
  815. expect(fieldColumn.length).toEqual(1);
  816. expect(fieldColumn.props().value).toMatchObject({
  817. kind: 'function',
  818. meta: {
  819. name: 'count',
  820. parameters: [],
  821. },
  822. });
  823. await clickSubmit(wrapper);
  824. expect(widget.displayType).toEqual('line');
  825. expect(widget.queries).toHaveLength(1);
  826. expect(widget.queries[0].fields).toEqual(['count()']);
  827. wrapper.unmount();
  828. });
  829. it('should automatically add columns for top n widget charts', async function () {
  830. const wrapper = mountModal({
  831. initialData,
  832. onAddWidget: data => (widget = data),
  833. displayType: types.DisplayType.TOP_N,
  834. defaultTableColumns: ['title', 'count()', 'count_unique(user)', 'epm()'],
  835. defaultWidgetQuery: {
  836. name: '',
  837. fields: ['title', 'count()', 'count_unique(user)', 'epm()', 'count()'],
  838. conditions: 'tag:value',
  839. orderby: '',
  840. },
  841. });
  842. // Select Top n display
  843. expect(getDisplayType(wrapper).props().value).toEqual('top_n');
  844. // No delete button as there is only one field.
  845. expect(wrapper.find('IconDelete')).toHaveLength(0);
  846. // Restricting to a single query
  847. expect(wrapper.find('button[aria-label="Add Query"]')).toHaveLength(0);
  848. // Restricting to a single y-axis
  849. expect(wrapper.find('button[aria-label="Add Overlay"]')).toHaveLength(0);
  850. const titleColumn = wrapper.find('input[name="field"]').at(0);
  851. expect(titleColumn.props().value).toEqual({
  852. kind: 'field',
  853. meta: {dataType: 'string', name: 'title'},
  854. });
  855. const countColumn = wrapper.find('input[name="field"]').at(1);
  856. expect(countColumn.props().value).toEqual({
  857. kind: 'function',
  858. meta: {parameters: [], name: 'count'},
  859. });
  860. expect(wrapper.find('WidgetQueriesForm Field[data-test-id="y-axis"]')).toHaveLength(
  861. 1
  862. );
  863. expect(wrapper.find('WidgetQueriesForm SelectControl[name="orderby"]')).toHaveLength(
  864. 1
  865. );
  866. await tick();
  867. wrapper.unmount();
  868. });
  869. it('should use defaultWidgetQuery Y-Axis and Conditions if given a defaultWidgetQuery', async function () {
  870. const wrapper = mountModal({
  871. initialData,
  872. onAddWidget: () => undefined,
  873. onUpdateWidget: () => undefined,
  874. widget: undefined,
  875. source: types.DashboardWidgetSource.DISCOVERV2,
  876. defaultWidgetQuery: {
  877. name: '',
  878. fields: ['count()', 'failure_count()', 'count_unique(user)'],
  879. conditions: 'tag:value',
  880. orderby: '',
  881. },
  882. });
  883. expect(wrapper.find('SearchBar').props().query).toEqual('tag:value');
  884. const queryFields = wrapper.find('QueryField');
  885. expect(queryFields.length).toEqual(3);
  886. expect(queryFields.at(0).props().fieldValue.function[0]).toEqual('count');
  887. expect(queryFields.at(1).props().fieldValue.function[0]).toEqual('failure_count');
  888. expect(queryFields.at(2).props().fieldValue.function[0]).toEqual('count_unique');
  889. await tick();
  890. wrapper.unmount();
  891. });
  892. it('uses displayType if given a displayType', async function () {
  893. const wrapper = mountModal({
  894. initialData,
  895. onAddWidget: () => undefined,
  896. onUpdateWidget: () => undefined,
  897. source: types.DashboardWidgetSource.DISCOVERV2,
  898. displayType: types.DisplayType.BAR,
  899. });
  900. expect(wrapper.find('SelectPicker').at(1).props().value.value).toEqual('bar');
  901. wrapper.unmount();
  902. });
  903. it('correctly defaults fields and orderby when in Top N display', async function () {
  904. const wrapper = mountModal({
  905. initialData,
  906. onAddWidget: () => undefined,
  907. onUpdateWidget: () => undefined,
  908. source: types.DashboardWidgetSource.DISCOVERV2,
  909. displayType: types.DisplayType.TOP_N,
  910. defaultWidgetQuery: {
  911. fields: ['title', 'count()', 'count_unique(user)'],
  912. orderby: '-count_unique_user',
  913. },
  914. defaultTableColumns: ['title', 'count()'],
  915. });
  916. expect(wrapper.find('SelectPicker').at(1).props().value.value).toEqual('top_n');
  917. expect(wrapper.find('WidgetQueriesForm').props().queries[0].fields).toEqual([
  918. 'title',
  919. 'count()',
  920. 'count_unique(user)',
  921. ]);
  922. expect(wrapper.find('WidgetQueriesForm').props().queries[0].orderby).toEqual(
  923. '-count_unique_user'
  924. );
  925. wrapper.unmount();
  926. });
  927. it('submits custom widget correctly', async function () {
  928. const onAddLibraryWidgetMock = jest.fn();
  929. const wrapper = mountModal({
  930. initialData,
  931. dashboard,
  932. onAddLibraryWidget: onAddLibraryWidgetMock,
  933. source: types.DashboardWidgetSource.LIBRARY,
  934. });
  935. const input = wrapper.find('Input[name="title"] input');
  936. input.simulate('change', {target: {value: 'All Events'}});
  937. await clickSubmit(wrapper);
  938. expect(onAddLibraryWidgetMock).toHaveBeenCalledTimes(1);
  939. wrapper.unmount();
  940. });
  941. it('renders the tab button bar from widget library', async function () {
  942. const onAddLibraryWidgetMock = jest.fn();
  943. const wrapper = mountModal({
  944. initialData,
  945. dashboard,
  946. onAddLibraryWidget: onAddLibraryWidgetMock,
  947. source: types.DashboardWidgetSource.LIBRARY,
  948. });
  949. expect(wrapper.find('LibraryButton')).toHaveLength(1);
  950. expect(wrapper.find('CustomButton')).toHaveLength(1);
  951. wrapper.find('LibraryButton button').simulate('click');
  952. expect(openDashboardWidgetLibraryModal).toHaveBeenCalledTimes(1);
  953. wrapper.unmount();
  954. });
  955. it('sets widgetType to discover', async function () {
  956. const onAdd = jest.fn();
  957. const wrapper = mountModal({
  958. initialData,
  959. onAddWidget: onAdd,
  960. onUpdateWidget: () => undefined,
  961. });
  962. await clickSubmit(wrapper);
  963. expect(onAdd).toHaveBeenCalledWith(expect.objectContaining({widgetType: 'discover'}));
  964. wrapper.unmount();
  965. });
  966. it('limits TopN display to one query when switching from another visualization', async () => {
  967. reactMountWithTheme(
  968. <AddDashboardWidgetModal
  969. Header={stubEl}
  970. Body={stubEl}
  971. Footer={stubEl}
  972. CloseButton={stubEl}
  973. organization={initialData.organization}
  974. onAddWidget={() => undefined}
  975. onUpdateWidget={() => undefined}
  976. widget={initialData.widget}
  977. closeModal={() => void 0}
  978. source={types.DashboardWidgetSource.DASHBOARDS}
  979. />
  980. );
  981. userEvent.click(screen.getByText('Table'));
  982. userEvent.click(await screen.findByText('Bar Chart'));
  983. userEvent.click(screen.getByText('Add Query'));
  984. userEvent.click(screen.getByText('Add Query'));
  985. expect(
  986. screen.getAllByPlaceholderText('Search for events, users, tags, and more').length
  987. ).toEqual(3);
  988. userEvent.click(screen.getByText('Bar Chart'));
  989. userEvent.click(await screen.findByText('Top 5 Events'));
  990. expect(
  991. screen.getAllByPlaceholderText('Search for events, users, tags, and more').length
  992. ).toEqual(1);
  993. });
  994. describe('Issue Widgets', function () {
  995. it('sets widgetType to issues', async function () {
  996. initialData.organization.features = [
  997. 'performance-view',
  998. 'discover-query',
  999. 'issues-in-dashboards',
  1000. ];
  1001. const onAdd = jest.fn(() => {});
  1002. const wrapper = mountModalWithRtl({
  1003. initialData,
  1004. onAddWidget: onAdd,
  1005. onUpdateWidget: () => undefined,
  1006. });
  1007. userEvent.click(screen.getByText('Issues (States, Assignment, Time, etc.)'));
  1008. userEvent.click(screen.getByTestId('add-widget'));
  1009. await tick();
  1010. expect(onAdd).toHaveBeenCalledWith(
  1011. expect.objectContaining({
  1012. displayType: 'table',
  1013. interval: '5m',
  1014. queries: [
  1015. {
  1016. conditions: '',
  1017. fields: ['issue', 'assignee', 'title'],
  1018. name: '',
  1019. orderby: '',
  1020. },
  1021. ],
  1022. title: '',
  1023. widgetType: 'issue',
  1024. })
  1025. );
  1026. wrapper.unmount();
  1027. });
  1028. it('does not render the dataset selector', async function () {
  1029. initialData.organization.features = [
  1030. 'performance-view',
  1031. 'discover-query',
  1032. 'issues-in-dashboards',
  1033. ];
  1034. const wrapper = mountModalWithRtl({
  1035. initialData,
  1036. onAddWidget: () => undefined,
  1037. onUpdateWidget: () => undefined,
  1038. source: types.DashboardWidgetSource.DISCOVERV2,
  1039. });
  1040. await tick();
  1041. userEvent.click(screen.getByText('Table'));
  1042. userEvent.click(screen.getByText('Line Chart'));
  1043. expect(screen.queryByText('Data Set')).not.toBeInTheDocument();
  1044. wrapper.unmount();
  1045. });
  1046. it('renders the dataset selector', function () {
  1047. initialData.organization.features = [
  1048. 'performance-view',
  1049. 'discover-query',
  1050. 'issues-in-dashboards',
  1051. ];
  1052. const wrapper = mountModalWithRtl({
  1053. initialData,
  1054. onAddWidget: () => undefined,
  1055. onUpdateWidget: () => undefined,
  1056. source: types.DashboardWidgetSource.DASHBOARDS,
  1057. });
  1058. expect(metricsDataMock).not.toHaveBeenCalled();
  1059. expect(screen.getByText('Data Set')).toBeInTheDocument();
  1060. expect(
  1061. screen.getByText('All Events (Errors and Transactions)')
  1062. ).toBeInTheDocument();
  1063. expect(
  1064. screen.getByText('Issues (States, Assignment, Time, etc.)')
  1065. ).toBeInTheDocument();
  1066. // Hide without the dashboards-metrics feature flag
  1067. expect(screen.queryByText('Metrics (Release Health)')).not.toBeInTheDocument();
  1068. wrapper.unmount();
  1069. });
  1070. it('disables moving and deleting issue column', async function () {
  1071. initialData.organization.features = [
  1072. 'performance-view',
  1073. 'discover-query',
  1074. 'issues-in-dashboards',
  1075. ];
  1076. const wrapper = mountModalWithRtl({
  1077. initialData,
  1078. onAddWidget: () => undefined,
  1079. onUpdateWidget: () => undefined,
  1080. source: types.DashboardWidgetSource.DASHBOARDS,
  1081. });
  1082. userEvent.click(screen.getByText('Issues (States, Assignment, Time, etc.)'));
  1083. await tick();
  1084. expect(screen.getByText('issue')).toBeInTheDocument();
  1085. expect(screen.getByText('assignee')).toBeInTheDocument();
  1086. expect(screen.getByText('title')).toBeInTheDocument();
  1087. expect(screen.getAllByRole('button', {name: 'Remove column'}).length).toEqual(2);
  1088. expect(screen.getAllByRole('button', {name: 'Drag to reorder'}).length).toEqual(3);
  1089. userEvent.click(screen.getAllByRole('button', {name: 'Remove column'})[1]);
  1090. userEvent.click(screen.getAllByRole('button', {name: 'Remove column'})[0]);
  1091. await tick();
  1092. expect(screen.getByText('issue')).toBeInTheDocument();
  1093. expect(screen.queryByText('assignee')).not.toBeInTheDocument();
  1094. expect(screen.queryByText('title')).not.toBeInTheDocument();
  1095. expect(screen.queryAllByRole('button', {name: 'Remove column'}).length).toEqual(0);
  1096. expect(screen.queryAllByRole('button', {name: 'Drag to reorder'}).length).toEqual(
  1097. 0
  1098. );
  1099. wrapper.unmount();
  1100. });
  1101. });
  1102. describe('Metrics Widgets', function () {
  1103. it('renders the dataset selector', async function () {
  1104. initialData.organization.features = [
  1105. 'performance-view',
  1106. 'discover-query',
  1107. 'dashboards-metrics',
  1108. ];
  1109. const wrapper = mountModalWithRtl({
  1110. initialData,
  1111. onAddWidget: () => undefined,
  1112. onUpdateWidget: () => undefined,
  1113. source: types.DashboardWidgetSource.DASHBOARDS,
  1114. });
  1115. await tick();
  1116. expect(screen.getByText('Data Set')).toBeInTheDocument();
  1117. expect(
  1118. screen.getByText('All Events (Errors and Transactions)')
  1119. ).toBeInTheDocument();
  1120. expect(
  1121. screen.queryByText('Issues (States, Assignment, Time, etc.)')
  1122. ).not.toBeInTheDocument();
  1123. expect(screen.getByText('Metrics (Release Health)')).toBeInTheDocument();
  1124. wrapper.unmount();
  1125. });
  1126. it('maintains the selected dataset when display type is changed', async function () {
  1127. initialData.organization.features = [
  1128. 'performance-view',
  1129. 'discover-query',
  1130. 'dashboards-metrics',
  1131. ];
  1132. const wrapper = mountModalWithRtl({
  1133. initialData,
  1134. onAddWidget: () => undefined,
  1135. onUpdateWidget: () => undefined,
  1136. source: types.DashboardWidgetSource.DASHBOARDS,
  1137. });
  1138. await tick();
  1139. const metricsDataset = screen.getByLabelText('Metrics (Release Health)');
  1140. expect(metricsDataset).not.toBeChecked();
  1141. await act(async () =>
  1142. userEvent.click(screen.getByLabelText('Metrics (Release Health)'))
  1143. );
  1144. expect(metricsDataset).toBeChecked();
  1145. userEvent.click(screen.getByText('Table'));
  1146. userEvent.click(screen.getByText('Line Chart'));
  1147. expect(metricsDataset).toBeChecked();
  1148. wrapper.unmount();
  1149. });
  1150. it('displays metrics tags', async function () {
  1151. initialData.organization.features = [
  1152. 'performance-view',
  1153. 'discover-query',
  1154. 'dashboards-metrics',
  1155. ];
  1156. const wrapper = mountModalWithRtl({
  1157. initialData,
  1158. onAddWidget: () => undefined,
  1159. onUpdateWidget: () => undefined,
  1160. source: types.DashboardWidgetSource.DASHBOARDS,
  1161. });
  1162. await tick();
  1163. await act(async () =>
  1164. userEvent.click(screen.getByLabelText('Metrics (Release Health)'))
  1165. );
  1166. expect(screen.getByText('sum(…)')).toBeInTheDocument();
  1167. expect(screen.getByText('sentry.sessions.session')).toBeInTheDocument();
  1168. userEvent.click(screen.getByText('sum(…)'));
  1169. expect(screen.getByText('count_unique(…)')).toBeInTheDocument();
  1170. expect(screen.getByText('release')).toBeInTheDocument();
  1171. expect(screen.getByText('environment')).toBeInTheDocument();
  1172. expect(screen.getByText('session.status')).toBeInTheDocument();
  1173. userEvent.click(screen.getByText('count_unique(…)'));
  1174. expect(screen.getByText('sentry.sessions.user')).toBeInTheDocument();
  1175. userEvent.click(screen.getByText('sentry.sessions.user'));
  1176. // Ensure METRICS_FIELDS_ALLOW_LIST is honoured
  1177. expect(screen.queryByText('not.on.allow.list')).not.toBeInTheDocument();
  1178. wrapper.unmount();
  1179. });
  1180. it('displays the correct options for area chart', async function () {
  1181. initialData.organization.features = [
  1182. 'performance-view',
  1183. 'discover-query',
  1184. 'dashboards-metrics',
  1185. ];
  1186. const wrapper = mountModalWithRtl({
  1187. initialData,
  1188. onAddWidget: () => undefined,
  1189. onUpdateWidget: () => undefined,
  1190. source: types.DashboardWidgetSource.DASHBOARDS,
  1191. });
  1192. await tick();
  1193. await act(async () =>
  1194. userEvent.click(screen.getByLabelText('Metrics (Release Health)'))
  1195. );
  1196. userEvent.click(screen.getByText('Table'));
  1197. userEvent.click(screen.getByText('Line Chart'));
  1198. expect(screen.getByText('sum(…)')).toBeInTheDocument();
  1199. expect(screen.getByText('sentry.sessions.session')).toBeInTheDocument();
  1200. userEvent.click(screen.getByText('sum(…)'));
  1201. expect(screen.getByText('count_unique(…)')).toBeInTheDocument();
  1202. userEvent.click(screen.getByText('count_unique(…)'));
  1203. expect(screen.getByText('sentry.sessions.user')).toBeInTheDocument();
  1204. wrapper.unmount();
  1205. });
  1206. it('makes the appropriate metrics call', async function () {
  1207. initialData.organization.features = [
  1208. 'performance-view',
  1209. 'discover-query',
  1210. 'dashboards-metrics',
  1211. ];
  1212. const wrapper = mountModalWithRtl({
  1213. initialData,
  1214. onAddWidget: () => undefined,
  1215. onUpdateWidget: () => undefined,
  1216. source: types.DashboardWidgetSource.DASHBOARDS,
  1217. });
  1218. await act(async () =>
  1219. userEvent.click(screen.getByLabelText('Metrics (Release Health)'))
  1220. );
  1221. userEvent.click(screen.getByText('Table'));
  1222. userEvent.click(screen.getByText('Line Chart'));
  1223. expect(metricsDataMock).toHaveBeenCalledWith(
  1224. `/organizations/org-slug/metrics/data/`,
  1225. expect.objectContaining({
  1226. query: {
  1227. environment: [],
  1228. field: ['sum(sentry.sessions.session)'],
  1229. interval: '30m',
  1230. project: [],
  1231. statsPeriod: '14d',
  1232. },
  1233. })
  1234. );
  1235. wrapper.unmount();
  1236. });
  1237. });
  1238. });