addDashboardWidgetModal.spec.jsx 49 KB

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