addDashboardWidgetModal.spec.jsx 47 KB

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