detail.spec.tsx 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547
  1. import {browserHistory} from 'react-router';
  2. import {DashboardFixture} from 'sentry-fixture/dashboard';
  3. import {LocationFixture} from 'sentry-fixture/locationFixture';
  4. import {OrganizationFixture} from 'sentry-fixture/organization';
  5. import {ProjectFixture} from 'sentry-fixture/project';
  6. import {ReleaseFixture} from 'sentry-fixture/release';
  7. import {RouteComponentPropsFixture} from 'sentry-fixture/routeComponentPropsFixture';
  8. import {WidgetFixture} from 'sentry-fixture/widget';
  9. import {initializeOrg} from 'sentry-test/initializeOrg';
  10. import {
  11. act,
  12. render,
  13. screen,
  14. userEvent,
  15. waitFor,
  16. within,
  17. } from 'sentry-test/reactTestingLibrary';
  18. import * as modals from 'sentry/actionCreators/modal';
  19. import ProjectsStore from 'sentry/stores/projectsStore';
  20. import CreateDashboard from 'sentry/views/dashboards/create';
  21. import * as types from 'sentry/views/dashboards/types';
  22. import ViewEditDashboard from 'sentry/views/dashboards/view';
  23. import {OrganizationContext} from 'sentry/views/organizationContext';
  24. describe('Dashboards > Detail', function () {
  25. const organization = OrganizationFixture({
  26. features: ['global-views', 'dashboards-basic', 'dashboards-edit', 'discover-query'],
  27. });
  28. const projects = [ProjectFixture()];
  29. describe('prebuilt dashboards', function () {
  30. let initialData;
  31. beforeEach(function () {
  32. act(() => ProjectsStore.loadInitialData(projects));
  33. initialData = initializeOrg({organization});
  34. MockApiClient.addMockResponse({
  35. url: '/organizations/org-slug/tags/',
  36. body: [],
  37. });
  38. MockApiClient.addMockResponse({
  39. url: '/organizations/org-slug/projects/',
  40. body: [ProjectFixture()],
  41. });
  42. MockApiClient.addMockResponse({
  43. url: '/organizations/org-slug/dashboards/',
  44. body: [
  45. DashboardFixture([], {id: 'default-overview', title: 'Default'}),
  46. DashboardFixture([], {id: '1', title: 'Custom Errors'}),
  47. ],
  48. });
  49. MockApiClient.addMockResponse({
  50. url: '/organizations/org-slug/dashboards/default-overview/',
  51. body: DashboardFixture([], {id: 'default-overview', title: 'Default'}),
  52. });
  53. MockApiClient.addMockResponse({
  54. url: '/organizations/org-slug/dashboards/1/visit/',
  55. method: 'POST',
  56. body: [],
  57. statusCode: 200,
  58. });
  59. MockApiClient.addMockResponse({
  60. url: '/organizations/org-slug/users/',
  61. method: 'GET',
  62. body: [],
  63. });
  64. MockApiClient.addMockResponse({
  65. url: '/organizations/org-slug/sdk-updates/',
  66. body: [],
  67. });
  68. MockApiClient.addMockResponse({
  69. url: '/organizations/org-slug/prompts-activity/',
  70. body: {},
  71. });
  72. MockApiClient.addMockResponse({
  73. url: '/organizations/org-slug/events/',
  74. method: 'GET',
  75. body: [],
  76. });
  77. MockApiClient.addMockResponse({
  78. url: '/organizations/org-slug/events-stats/',
  79. body: {data: []},
  80. });
  81. MockApiClient.addMockResponse({
  82. method: 'GET',
  83. url: '/organizations/org-slug/issues/',
  84. body: [],
  85. });
  86. MockApiClient.addMockResponse({
  87. url: '/organizations/org-slug/releases/',
  88. body: [],
  89. });
  90. });
  91. afterEach(function () {
  92. MockApiClient.clearMockResponses();
  93. });
  94. it('assigns unique IDs to all widgets so grid keys are unique', async function () {
  95. MockApiClient.addMockResponse({
  96. url: '/organizations/org-slug/events-stats/',
  97. body: {data: []},
  98. });
  99. MockApiClient.addMockResponse({
  100. url: '/organizations/org-slug/dashboards/default-overview/',
  101. body: DashboardFixture(
  102. [
  103. WidgetFixture({
  104. queries: [
  105. {
  106. name: '',
  107. conditions: 'event.type:error',
  108. fields: ['count()'],
  109. aggregates: ['count()'],
  110. columns: [],
  111. orderby: '-count()',
  112. },
  113. ],
  114. title: 'Default Widget 1',
  115. interval: '1d',
  116. }),
  117. WidgetFixture({
  118. queries: [
  119. {
  120. name: '',
  121. conditions: 'event.type:transaction',
  122. fields: ['count()'],
  123. aggregates: ['count()'],
  124. columns: [],
  125. orderby: '-count()',
  126. },
  127. ],
  128. title: 'Default Widget 2',
  129. interval: '1d',
  130. }),
  131. ],
  132. {id: 'default-overview', title: 'Default'}
  133. ),
  134. });
  135. initialData = initializeOrg({
  136. organization: OrganizationFixture({
  137. features: ['global-views', 'dashboards-basic', 'discover-query'],
  138. projects: [ProjectFixture()],
  139. }),
  140. });
  141. render(
  142. <OrganizationContext.Provider value={initialData.organization}>
  143. <ViewEditDashboard
  144. {...RouteComponentPropsFixture()}
  145. organization={initialData.organization}
  146. params={{orgId: 'org-slug', dashboardId: 'default-overview'}}
  147. router={initialData.router}
  148. location={initialData.router.location}
  149. >
  150. {null}
  151. </ViewEditDashboard>
  152. </OrganizationContext.Provider>,
  153. {context: initialData.routerContext}
  154. );
  155. expect(await screen.findByText('Default Widget 1')).toBeInTheDocument();
  156. expect(screen.getByText('Default Widget 2')).toBeInTheDocument();
  157. });
  158. it('opens the widget viewer modal in a prebuilt dashboard using the widget id specified in the url', async () => {
  159. const openWidgetViewerModal = jest.spyOn(modals, 'openWidgetViewerModal');
  160. render(
  161. <CreateDashboard
  162. {...RouteComponentPropsFixture()}
  163. organization={initialData.organization}
  164. params={{templateId: 'default-template', widgetId: '2'}}
  165. router={initialData.router}
  166. location={{...initialData.router.location, pathname: '/widget/2/'}}
  167. >
  168. {null}
  169. </CreateDashboard>,
  170. {context: initialData.routerContext, organization: initialData.organization}
  171. );
  172. await waitFor(() => {
  173. expect(openWidgetViewerModal).toHaveBeenCalledWith(
  174. expect.objectContaining({
  175. organization: initialData.organization,
  176. widget: expect.objectContaining({
  177. displayType: 'line',
  178. interval: '5m',
  179. queries: [
  180. {
  181. aggregates: ['count()'],
  182. columns: [],
  183. conditions: '!event.type:transaction',
  184. fields: ['count()'],
  185. name: 'Events',
  186. orderby: 'count()',
  187. },
  188. ],
  189. title: 'Events',
  190. widgetType: types.WidgetType.DISCOVER,
  191. }),
  192. onClose: expect.anything(),
  193. })
  194. );
  195. });
  196. });
  197. });
  198. describe('custom dashboards', function () {
  199. let initialData, widgets, mockVisit, mockPut;
  200. beforeEach(function () {
  201. window.confirm = jest.fn();
  202. initialData = initializeOrg({
  203. organization,
  204. router: {
  205. location: LocationFixture(),
  206. },
  207. });
  208. widgets = [
  209. WidgetFixture({
  210. queries: [
  211. {
  212. name: '',
  213. conditions: 'event.type:error',
  214. fields: ['count()'],
  215. columns: [],
  216. aggregates: ['count()'],
  217. orderby: '-count()',
  218. },
  219. ],
  220. title: 'Errors',
  221. interval: '1d',
  222. widgetType: types.WidgetType.DISCOVER,
  223. id: '1',
  224. }),
  225. WidgetFixture({
  226. queries: [
  227. {
  228. name: '',
  229. conditions: 'event.type:transaction',
  230. fields: ['count()'],
  231. columns: [],
  232. aggregates: ['count()'],
  233. orderby: '-count()',
  234. },
  235. ],
  236. title: 'Transactions',
  237. interval: '1d',
  238. widgetType: types.WidgetType.DISCOVER,
  239. id: '2',
  240. }),
  241. WidgetFixture({
  242. queries: [
  243. {
  244. name: '',
  245. conditions: 'event.type:transaction transaction:/api/cats',
  246. fields: ['p50()'],
  247. columns: [],
  248. aggregates: ['p50()'],
  249. orderby: '-p50()',
  250. },
  251. ],
  252. title: 'p50 of /api/cats',
  253. interval: '1d',
  254. id: '3',
  255. }),
  256. ];
  257. mockVisit = MockApiClient.addMockResponse({
  258. url: '/organizations/org-slug/dashboards/1/visit/',
  259. method: 'POST',
  260. body: [],
  261. statusCode: 200,
  262. });
  263. MockApiClient.addMockResponse({
  264. url: '/organizations/org-slug/tags/',
  265. body: [],
  266. });
  267. MockApiClient.addMockResponse({
  268. url: '/organizations/org-slug/projects/',
  269. body: [ProjectFixture()],
  270. });
  271. MockApiClient.addMockResponse({
  272. url: '/organizations/org-slug/dashboards/',
  273. body: [
  274. {
  275. ...DashboardFixture([], {
  276. id: 'default-overview',
  277. title: 'Default',
  278. }),
  279. widgetDisplay: ['area'],
  280. },
  281. {
  282. ...DashboardFixture([], {
  283. id: '1',
  284. title: 'Custom Errors',
  285. }),
  286. widgetDisplay: ['area'],
  287. },
  288. ],
  289. });
  290. MockApiClient.addMockResponse({
  291. url: '/organizations/org-slug/dashboards/1/',
  292. body: DashboardFixture(widgets, {
  293. id: '1',
  294. title: 'Custom Errors',
  295. filters: {},
  296. }),
  297. });
  298. mockPut = MockApiClient.addMockResponse({
  299. url: '/organizations/org-slug/dashboards/1/',
  300. method: 'PUT',
  301. body: DashboardFixture(widgets, {id: '1', title: 'Custom Errors'}),
  302. });
  303. MockApiClient.addMockResponse({
  304. url: '/organizations/org-slug/events-stats/',
  305. body: {data: []},
  306. });
  307. MockApiClient.addMockResponse({
  308. method: 'POST',
  309. url: '/organizations/org-slug/dashboards/widgets/',
  310. body: [],
  311. });
  312. MockApiClient.addMockResponse({
  313. method: 'GET',
  314. url: '/organizations/org-slug/recent-searches/',
  315. body: [],
  316. });
  317. MockApiClient.addMockResponse({
  318. method: 'GET',
  319. url: '/organizations/org-slug/issues/',
  320. body: [],
  321. });
  322. MockApiClient.addMockResponse({
  323. url: '/organizations/org-slug/events/',
  324. method: 'GET',
  325. body: [],
  326. });
  327. MockApiClient.addMockResponse({
  328. url: '/organizations/org-slug/users/',
  329. method: 'GET',
  330. body: [],
  331. });
  332. MockApiClient.addMockResponse({
  333. url: '/organizations/org-slug/releases/',
  334. body: [],
  335. });
  336. MockApiClient.addMockResponse({
  337. url: '/organizations/org-slug/sdk-updates/',
  338. body: [],
  339. });
  340. MockApiClient.addMockResponse({
  341. url: '/organizations/org-slug/prompts-activity/',
  342. body: {},
  343. });
  344. });
  345. afterEach(function () {
  346. MockApiClient.clearMockResponses();
  347. jest.clearAllMocks();
  348. });
  349. it('can remove widgets', async function () {
  350. const updateMock = MockApiClient.addMockResponse({
  351. url: '/organizations/org-slug/dashboards/1/',
  352. method: 'PUT',
  353. body: DashboardFixture([widgets[0]], {id: '1', title: 'Custom Errors'}),
  354. });
  355. render(
  356. <OrganizationContext.Provider value={initialData.organization}>
  357. <ViewEditDashboard
  358. {...RouteComponentPropsFixture()}
  359. organization={initialData.organization}
  360. params={{orgId: 'org-slug', dashboardId: '1'}}
  361. router={initialData.router}
  362. location={initialData.router.location}
  363. >
  364. {null}
  365. </ViewEditDashboard>
  366. </OrganizationContext.Provider>,
  367. {context: initialData.routerContext}
  368. );
  369. await waitFor(() => expect(mockVisit).toHaveBeenCalledTimes(1));
  370. // Enter edit mode.
  371. await userEvent.click(screen.getByRole('button', {name: 'Edit Dashboard'}));
  372. // Remove the second and third widgets
  373. await userEvent.click(screen.getAllByRole('button', {name: 'Delete Widget'})[1]);
  374. await userEvent.click(screen.getAllByRole('button', {name: 'Delete Widget'})[1]);
  375. // Save changes
  376. await userEvent.click(screen.getByRole('button', {name: 'Save and Finish'}));
  377. expect(updateMock).toHaveBeenCalled();
  378. expect(updateMock).toHaveBeenCalledWith(
  379. '/organizations/org-slug/dashboards/1/',
  380. expect.objectContaining({
  381. data: expect.objectContaining({
  382. title: 'Custom Errors',
  383. widgets: [expect.objectContaining(widgets[0])],
  384. }),
  385. })
  386. );
  387. // Visit should not be called again on dashboard update
  388. expect(mockVisit).toHaveBeenCalledTimes(1);
  389. });
  390. it('appends dashboard-level filters to series request', async function () {
  391. MockApiClient.addMockResponse({
  392. url: '/organizations/org-slug/dashboards/1/',
  393. body: DashboardFixture(widgets, {
  394. id: '1',
  395. title: 'Custom Errors',
  396. filters: {release: ['abc@1.2.0']},
  397. }),
  398. });
  399. const mock = MockApiClient.addMockResponse({
  400. url: '/organizations/org-slug/events-stats/',
  401. body: [],
  402. });
  403. render(
  404. <OrganizationContext.Provider value={initialData.organization}>
  405. <ViewEditDashboard
  406. {...RouteComponentPropsFixture()}
  407. organization={initialData.organization}
  408. params={{orgId: 'org-slug', dashboardId: '1'}}
  409. router={initialData.router}
  410. location={initialData.router.location}
  411. >
  412. {null}
  413. </ViewEditDashboard>
  414. </OrganizationContext.Provider>,
  415. {context: initialData.routerContext}
  416. );
  417. await waitFor(() =>
  418. expect(mock).toHaveBeenLastCalledWith(
  419. '/organizations/org-slug/events-stats/',
  420. expect.objectContaining({
  421. query: expect.objectContaining({
  422. query: 'event.type:transaction transaction:/api/cats release:"abc@1.2.0" ',
  423. }),
  424. })
  425. )
  426. );
  427. });
  428. it('shows add widget option', async function () {
  429. render(
  430. <OrganizationContext.Provider value={initialData.organization}>
  431. <ViewEditDashboard
  432. {...RouteComponentPropsFixture()}
  433. organization={initialData.organization}
  434. params={{orgId: 'org-slug', dashboardId: '1'}}
  435. router={initialData.router}
  436. location={initialData.router.location}
  437. >
  438. {null}
  439. </ViewEditDashboard>
  440. </OrganizationContext.Provider>,
  441. {context: initialData.routerContext}
  442. );
  443. // Enter edit mode.
  444. await userEvent.click(screen.getByRole('button', {name: 'Edit Dashboard'}));
  445. expect(await screen.findByRole('button', {name: 'Add widget'})).toBeInTheDocument();
  446. });
  447. it('shows top level release filter', async function () {
  448. const mockReleases = MockApiClient.addMockResponse({
  449. url: '/organizations/org-slug/releases/',
  450. body: [ReleaseFixture()],
  451. });
  452. initialData = initializeOrg({
  453. organization: OrganizationFixture({
  454. features: [
  455. 'global-views',
  456. 'dashboards-basic',
  457. 'dashboards-edit',
  458. 'discover-query',
  459. ],
  460. projects: [ProjectFixture()],
  461. }),
  462. });
  463. render(
  464. <OrganizationContext.Provider value={initialData.organization}>
  465. <ViewEditDashboard
  466. {...RouteComponentPropsFixture()}
  467. organization={initialData.organization}
  468. params={{orgId: 'org-slug', dashboardId: '1'}}
  469. router={initialData.router}
  470. location={initialData.router.location}
  471. >
  472. {null}
  473. </ViewEditDashboard>
  474. </OrganizationContext.Provider>,
  475. {context: initialData.routerContext}
  476. );
  477. expect(await screen.findByText('All Releases')).toBeInTheDocument();
  478. expect(mockReleases).toHaveBeenCalledTimes(1);
  479. });
  480. it('hides add widget option', async function () {
  481. // @ts-expect-error this is assigning to readonly property...
  482. types.MAX_WIDGETS = 1;
  483. render(
  484. <OrganizationContext.Provider value={initialData.organization}>
  485. <ViewEditDashboard
  486. {...RouteComponentPropsFixture()}
  487. organization={initialData.organization}
  488. params={{orgId: 'org-slug', dashboardId: '1'}}
  489. router={initialData.router}
  490. location={initialData.router.location}
  491. >
  492. {null}
  493. </ViewEditDashboard>
  494. </OrganizationContext.Provider>,
  495. {context: initialData.routerContext}
  496. );
  497. // Enter edit mode.
  498. await userEvent.click(await screen.findByRole('button', {name: 'Edit Dashboard'}));
  499. expect(screen.queryByRole('button', {name: 'Add widget'})).not.toBeInTheDocument();
  500. });
  501. it('renders successfully if more widgets than stored layouts', async function () {
  502. // A case where someone has async added widgets to a dashboard
  503. MockApiClient.addMockResponse({
  504. url: '/organizations/org-slug/dashboards/1/',
  505. body: DashboardFixture(
  506. [
  507. WidgetFixture({
  508. queries: [
  509. {
  510. name: '',
  511. conditions: 'event.type:error',
  512. fields: ['count()'],
  513. aggregates: ['count()'],
  514. columns: [],
  515. orderby: '-count()',
  516. },
  517. ],
  518. title: 'First Widget',
  519. interval: '1d',
  520. id: '1',
  521. layout: {x: 0, y: 0, w: 2, h: 6, minH: 0},
  522. }),
  523. WidgetFixture({
  524. queries: [
  525. {
  526. name: '',
  527. conditions: 'event.type:error',
  528. fields: ['count()'],
  529. aggregates: ['count()'],
  530. columns: [],
  531. orderby: '-count()',
  532. },
  533. ],
  534. title: 'Second Widget',
  535. interval: '1d',
  536. id: '2',
  537. }),
  538. ],
  539. {id: '1', title: 'Custom Errors'}
  540. ),
  541. });
  542. render(
  543. <ViewEditDashboard
  544. {...RouteComponentPropsFixture()}
  545. organization={initialData.organization}
  546. params={{orgId: 'org-slug', dashboardId: '1'}}
  547. router={initialData.router}
  548. location={initialData.router.location}
  549. >
  550. {null}
  551. </ViewEditDashboard>,
  552. {context: initialData.routerContext, organization: initialData.organization}
  553. );
  554. expect(await screen.findByText('First Widget')).toBeInTheDocument();
  555. expect(await screen.findByText('Second Widget')).toBeInTheDocument();
  556. });
  557. it('does not trigger request if layout not updated', async () => {
  558. MockApiClient.addMockResponse({
  559. url: '/organizations/org-slug/dashboards/1/',
  560. body: DashboardFixture(
  561. [
  562. WidgetFixture({
  563. queries: [
  564. {
  565. name: '',
  566. conditions: 'event.type:error',
  567. fields: ['count()'],
  568. aggregates: ['count()'],
  569. columns: [],
  570. orderby: '-count()',
  571. },
  572. ],
  573. title: 'First Widget',
  574. interval: '1d',
  575. id: '1',
  576. layout: {x: 0, y: 0, w: 2, h: 6, minH: 0},
  577. }),
  578. ],
  579. {id: '1', title: 'Custom Errors'}
  580. ),
  581. });
  582. render(
  583. <ViewEditDashboard
  584. {...RouteComponentPropsFixture()}
  585. organization={initialData.organization}
  586. params={{orgId: 'org-slug', dashboardId: '1'}}
  587. router={initialData.router}
  588. location={initialData.router.location}
  589. >
  590. {null}
  591. </ViewEditDashboard>,
  592. {context: initialData.routerContext, organization: initialData.organization}
  593. );
  594. await userEvent.click(await screen.findByText('Edit Dashboard'));
  595. await userEvent.click(await screen.findByText('Save and Finish'));
  596. expect(screen.getByText('Edit Dashboard')).toBeInTheDocument();
  597. expect(mockPut).not.toHaveBeenCalled();
  598. });
  599. it('renders the custom resize handler for a widget', async () => {
  600. MockApiClient.addMockResponse({
  601. url: '/organizations/org-slug/dashboards/1/',
  602. body: DashboardFixture(
  603. [
  604. WidgetFixture({
  605. queries: [
  606. {
  607. name: '',
  608. conditions: 'event.type:error',
  609. fields: ['count()'],
  610. aggregates: ['count()'],
  611. columns: [],
  612. orderby: '-count()',
  613. },
  614. ],
  615. title: 'First Widget',
  616. interval: '1d',
  617. id: '1',
  618. layout: {x: 0, y: 0, w: 2, h: 6, minH: 0},
  619. }),
  620. ],
  621. {id: '1', title: 'Custom Errors'}
  622. ),
  623. });
  624. render(
  625. <ViewEditDashboard
  626. {...RouteComponentPropsFixture()}
  627. organization={initialData.organization}
  628. params={{orgId: 'org-slug', dashboardId: '1'}}
  629. router={initialData.router}
  630. location={initialData.router.location}
  631. >
  632. {null}
  633. </ViewEditDashboard>,
  634. {context: initialData.routerContext, organization: initialData.organization}
  635. );
  636. await userEvent.click(await screen.findByText('Edit Dashboard'));
  637. const widget = screen
  638. .getByText('First Widget')
  639. .closest('.react-grid-item') as HTMLElement;
  640. const resizeHandle = within(widget).getByTestId('custom-resize-handle');
  641. expect(resizeHandle).toBeVisible();
  642. });
  643. it('does not trigger an alert when the widgets have no layout and user cancels without changes', async () => {
  644. MockApiClient.addMockResponse({
  645. url: '/organizations/org-slug/dashboards/1/',
  646. body: DashboardFixture(
  647. [
  648. WidgetFixture({
  649. queries: [
  650. {
  651. name: '',
  652. conditions: 'event.type:error',
  653. fields: ['count()'],
  654. aggregates: ['count()'],
  655. columns: [],
  656. orderby: '-count()',
  657. },
  658. ],
  659. title: 'First Widget',
  660. interval: '1d',
  661. id: '1',
  662. layout: null,
  663. }),
  664. ],
  665. {id: '1', title: 'Custom Errors'}
  666. ),
  667. });
  668. render(
  669. <ViewEditDashboard
  670. {...RouteComponentPropsFixture()}
  671. organization={initialData.organization}
  672. params={{orgId: 'org-slug', dashboardId: '1'}}
  673. router={initialData.router}
  674. location={initialData.router.location}
  675. >
  676. {null}
  677. </ViewEditDashboard>,
  678. {context: initialData.routerContext, organization: initialData.organization}
  679. );
  680. await userEvent.click(await screen.findByText('Edit Dashboard'));
  681. await userEvent.click(await screen.findByText('Cancel'));
  682. expect(window.confirm).not.toHaveBeenCalled();
  683. });
  684. it('opens the widget viewer modal using the widget id specified in the url', async () => {
  685. const openWidgetViewerModal = jest.spyOn(modals, 'openWidgetViewerModal');
  686. const widget = WidgetFixture({
  687. queries: [
  688. {
  689. name: '',
  690. conditions: 'event.type:error',
  691. fields: ['count()'],
  692. aggregates: ['count()'],
  693. columns: [],
  694. orderby: '',
  695. },
  696. ],
  697. title: 'First Widget',
  698. interval: '1d',
  699. id: '1',
  700. layout: null,
  701. });
  702. MockApiClient.addMockResponse({
  703. url: '/organizations/org-slug/dashboards/1/',
  704. body: DashboardFixture([widget], {id: '1', title: 'Custom Errors'}),
  705. });
  706. render(
  707. <ViewEditDashboard
  708. {...RouteComponentPropsFixture()}
  709. organization={initialData.organization}
  710. params={{orgId: 'org-slug', dashboardId: '1', widgetId: 1}}
  711. router={initialData.router}
  712. location={{...initialData.router.location, pathname: '/widget/123/'}}
  713. >
  714. {null}
  715. </ViewEditDashboard>,
  716. {context: initialData.routerContext, organization: initialData.organization}
  717. );
  718. await waitFor(() => {
  719. expect(openWidgetViewerModal).toHaveBeenCalledWith(
  720. expect.objectContaining({
  721. organization: initialData.organization,
  722. widget,
  723. onClose: expect.anything(),
  724. })
  725. );
  726. });
  727. });
  728. it('redirects user to dashboard url if widget is not found', async () => {
  729. const openWidgetViewerModal = jest.spyOn(modals, 'openWidgetViewerModal');
  730. MockApiClient.addMockResponse({
  731. url: '/organizations/org-slug/dashboards/1/',
  732. body: DashboardFixture([], {id: '1', title: 'Custom Errors'}),
  733. });
  734. render(
  735. <ViewEditDashboard
  736. {...RouteComponentPropsFixture()}
  737. organization={initialData.organization}
  738. params={{orgId: 'org-slug', dashboardId: '1', widgetId: 123}}
  739. router={initialData.router}
  740. location={{...initialData.router.location, pathname: '/widget/123/'}}
  741. >
  742. {null}
  743. </ViewEditDashboard>,
  744. {context: initialData.routerContext, organization: initialData.organization}
  745. );
  746. expect(await screen.findByText('All Releases')).toBeInTheDocument();
  747. expect(openWidgetViewerModal).not.toHaveBeenCalled();
  748. expect(initialData.router.replace).toHaveBeenCalledWith(
  749. expect.objectContaining({
  750. pathname: '/organizations/org-slug/dashboard/1/',
  751. query: {},
  752. })
  753. );
  754. });
  755. it('saves a new dashboard with the page filters', async () => {
  756. const mockPOST = MockApiClient.addMockResponse({
  757. url: '/organizations/org-slug/dashboards/',
  758. method: 'POST',
  759. body: [],
  760. });
  761. render(
  762. <CreateDashboard
  763. {...RouteComponentPropsFixture()}
  764. organization={initialData.organization}
  765. params={{templateId: undefined}}
  766. router={initialData.router}
  767. location={{
  768. ...initialData.router.location,
  769. query: {
  770. ...initialData.router.location.query,
  771. statsPeriod: '7d',
  772. project: [2],
  773. environment: ['alpha', 'beta'],
  774. },
  775. }}
  776. >
  777. {null}
  778. </CreateDashboard>,
  779. {
  780. context: initialData.routerContext,
  781. organization: initialData.organization,
  782. }
  783. );
  784. await userEvent.click(await screen.findByText('Save and Finish'));
  785. expect(mockPOST).toHaveBeenCalledWith(
  786. '/organizations/org-slug/dashboards/',
  787. expect.objectContaining({
  788. data: expect.objectContaining({
  789. projects: [2],
  790. environment: ['alpha', 'beta'],
  791. period: '7d',
  792. }),
  793. })
  794. );
  795. });
  796. it('saves a template with the page filters', async () => {
  797. const mockPOST = MockApiClient.addMockResponse({
  798. url: '/organizations/org-slug/dashboards/',
  799. method: 'POST',
  800. body: [],
  801. });
  802. render(
  803. <CreateDashboard
  804. {...RouteComponentPropsFixture()}
  805. organization={initialData.organization}
  806. params={{templateId: 'default-template'}}
  807. router={initialData.router}
  808. location={{
  809. ...initialData.router.location,
  810. query: {
  811. ...initialData.router.location.query,
  812. statsPeriod: '7d',
  813. project: [2],
  814. environment: ['alpha', 'beta'],
  815. },
  816. }}
  817. >
  818. {null}
  819. </CreateDashboard>,
  820. {
  821. context: initialData.routerContext,
  822. organization: initialData.organization,
  823. }
  824. );
  825. await userEvent.click(await screen.findByText('Add Dashboard'));
  826. expect(mockPOST).toHaveBeenCalledWith(
  827. '/organizations/org-slug/dashboards/',
  828. expect.objectContaining({
  829. data: expect.objectContaining({
  830. projects: [2],
  831. environment: ['alpha', 'beta'],
  832. period: '7d',
  833. }),
  834. })
  835. );
  836. });
  837. it('does not render save and cancel buttons on templates', async () => {
  838. MockApiClient.addMockResponse({
  839. url: '/organizations/org-slug/releases/',
  840. body: [
  841. ReleaseFixture({
  842. shortVersion: 'sentry-android-shop@1.2.0',
  843. version: 'sentry-android-shop@1.2.0',
  844. }),
  845. ],
  846. });
  847. render(
  848. <CreateDashboard
  849. {...RouteComponentPropsFixture()}
  850. organization={initialData.organization}
  851. params={{templateId: 'default-template'}}
  852. router={initialData.router}
  853. location={initialData.router.location}
  854. >
  855. {null}
  856. </CreateDashboard>,
  857. {
  858. context: initialData.routerContext,
  859. organization: initialData.organization,
  860. }
  861. );
  862. await userEvent.click(await screen.findByText('24H'));
  863. await userEvent.click(screen.getByText('Last 7 days'));
  864. await screen.findByText('7D');
  865. expect(screen.queryByText('Cancel')).not.toBeInTheDocument();
  866. expect(screen.queryByText('Save')).not.toBeInTheDocument();
  867. });
  868. it('opens the widget viewer with saved dashboard filters', async () => {
  869. const openWidgetViewerModal = jest.spyOn(modals, 'openWidgetViewerModal');
  870. MockApiClient.addMockResponse({
  871. url: '/organizations/org-slug/dashboards/1/',
  872. body: DashboardFixture(widgets, {
  873. id: '1',
  874. filters: {release: ['sentry-android-shop@1.2.0']},
  875. }),
  876. });
  877. render(
  878. <ViewEditDashboard
  879. {...RouteComponentPropsFixture()}
  880. organization={initialData.organization}
  881. params={{orgId: 'org-slug', dashboardId: '1', widgetId: 1}}
  882. router={initialData.router}
  883. location={{...initialData.router.location, pathname: '/widget/1/'}}
  884. >
  885. {null}
  886. </ViewEditDashboard>,
  887. {context: initialData.routerContext, organization: initialData.organization}
  888. );
  889. await waitFor(() => {
  890. expect(openWidgetViewerModal).toHaveBeenCalledWith(
  891. expect.objectContaining({
  892. dashboardFilters: {release: ['sentry-android-shop@1.2.0']},
  893. })
  894. );
  895. });
  896. });
  897. it('opens the widget viewer with unsaved dashboard filters', async () => {
  898. const openWidgetViewerModal = jest.spyOn(modals, 'openWidgetViewerModal');
  899. MockApiClient.addMockResponse({
  900. url: '/organizations/org-slug/dashboards/1/',
  901. body: DashboardFixture(widgets, {
  902. id: '1',
  903. filters: {release: ['sentry-android-shop@1.2.0']},
  904. }),
  905. });
  906. render(
  907. <ViewEditDashboard
  908. {...RouteComponentPropsFixture()}
  909. organization={initialData.organization}
  910. params={{orgId: 'org-slug', dashboardId: '1', widgetId: 1}}
  911. router={initialData.router}
  912. location={{
  913. ...initialData.router.location,
  914. pathname: '/widget/1/',
  915. query: {release: ['unsaved-release-filter@1.2.0']},
  916. }}
  917. >
  918. {null}
  919. </ViewEditDashboard>,
  920. {context: initialData.routerContext, organization: initialData.organization}
  921. );
  922. await waitFor(() => {
  923. expect(openWidgetViewerModal).toHaveBeenCalledWith(
  924. expect.objectContaining({
  925. dashboardFilters: {release: ['unsaved-release-filter@1.2.0']},
  926. })
  927. );
  928. });
  929. });
  930. it('can save dashboard filters in existing dashboard', async () => {
  931. MockApiClient.addMockResponse({
  932. url: '/organizations/org-slug/releases/',
  933. body: [
  934. ReleaseFixture({
  935. shortVersion: 'sentry-android-shop@1.2.0',
  936. version: 'sentry-android-shop@1.2.0',
  937. }),
  938. ],
  939. });
  940. const testData = initializeOrg({
  941. organization: OrganizationFixture({
  942. features: [
  943. 'global-views',
  944. 'dashboards-basic',
  945. 'dashboards-edit',
  946. 'discover-query',
  947. ],
  948. }),
  949. router: {
  950. location: {
  951. ...LocationFixture(),
  952. query: {
  953. statsPeriod: '7d',
  954. release: ['sentry-android-shop@1.2.0'],
  955. },
  956. },
  957. },
  958. });
  959. render(
  960. <ViewEditDashboard
  961. {...RouteComponentPropsFixture()}
  962. organization={testData.organization}
  963. params={{orgId: 'org-slug', dashboardId: '1'}}
  964. router={testData.router}
  965. location={testData.router.location}
  966. >
  967. {null}
  968. </ViewEditDashboard>,
  969. {context: testData.routerContext, organization: testData.organization}
  970. );
  971. await userEvent.click(await screen.findByText('Save'));
  972. expect(mockPut).toHaveBeenCalledWith(
  973. '/organizations/org-slug/dashboards/1/',
  974. expect.objectContaining({
  975. data: expect.objectContaining({
  976. period: '7d',
  977. filters: {release: ['sentry-android-shop@1.2.0']},
  978. }),
  979. })
  980. );
  981. });
  982. it('can clear dashboard filters in compact select', async () => {
  983. MockApiClient.addMockResponse({
  984. url: '/organizations/org-slug/dashboards/1/',
  985. body: DashboardFixture(widgets, {
  986. id: '1',
  987. title: 'Custom Errors',
  988. filters: {release: ['sentry-android-shop@1.2.0']},
  989. }),
  990. });
  991. MockApiClient.addMockResponse({
  992. url: '/organizations/org-slug/releases/',
  993. body: [
  994. ReleaseFixture({
  995. shortVersion: 'sentry-android-shop@1.2.0',
  996. version: 'sentry-android-shop@1.2.0',
  997. }),
  998. ],
  999. });
  1000. const testData = initializeOrg({
  1001. organization: OrganizationFixture({
  1002. features: [
  1003. 'global-views',
  1004. 'dashboards-basic',
  1005. 'dashboards-edit',
  1006. 'discover-query',
  1007. ],
  1008. }),
  1009. router: {
  1010. location: {
  1011. ...LocationFixture(),
  1012. query: {
  1013. statsPeriod: '7d',
  1014. },
  1015. },
  1016. },
  1017. });
  1018. render(
  1019. <ViewEditDashboard
  1020. {...RouteComponentPropsFixture()}
  1021. organization={testData.organization}
  1022. params={{orgId: 'org-slug', dashboardId: '1'}}
  1023. router={testData.router}
  1024. location={testData.router.location}
  1025. >
  1026. {null}
  1027. </ViewEditDashboard>,
  1028. {context: testData.routerContext, organization: testData.organization}
  1029. );
  1030. await screen.findByText('7D');
  1031. await userEvent.click(await screen.findByText('sentry-android-shop@1.2.0'));
  1032. await userEvent.click(screen.getAllByText('Clear')[0]);
  1033. screen.getByText('All Releases');
  1034. await userEvent.click(document.body);
  1035. expect(browserHistory.push).toHaveBeenCalledWith(
  1036. expect.objectContaining({
  1037. query: expect.objectContaining({
  1038. release: '',
  1039. }),
  1040. })
  1041. );
  1042. });
  1043. it('can save absolute time range in existing dashboard', async () => {
  1044. const testData = initializeOrg({
  1045. organization: OrganizationFixture({
  1046. features: [
  1047. 'global-views',
  1048. 'dashboards-basic',
  1049. 'dashboards-edit',
  1050. 'discover-query',
  1051. ],
  1052. }),
  1053. router: {
  1054. location: {
  1055. ...LocationFixture(),
  1056. query: {
  1057. start: '2022-07-14T07:00:00',
  1058. end: '2022-07-19T23:59:59',
  1059. utc: 'true',
  1060. },
  1061. },
  1062. },
  1063. });
  1064. render(
  1065. <ViewEditDashboard
  1066. {...RouteComponentPropsFixture()}
  1067. organization={testData.organization}
  1068. params={{orgId: 'org-slug', dashboardId: '1'}}
  1069. router={testData.router}
  1070. location={testData.router.location}
  1071. >
  1072. {null}
  1073. </ViewEditDashboard>,
  1074. {context: testData.routerContext, organization: testData.organization}
  1075. );
  1076. await userEvent.click(await screen.findByText('Save'));
  1077. expect(mockPut).toHaveBeenCalledWith(
  1078. '/organizations/org-slug/dashboards/1/',
  1079. expect.objectContaining({
  1080. data: expect.objectContaining({
  1081. start: '2022-07-14T07:00:00.000',
  1082. end: '2022-07-19T23:59:59.000',
  1083. utc: true,
  1084. }),
  1085. })
  1086. );
  1087. });
  1088. it('can clear dashboard filters in existing dashboard', async () => {
  1089. MockApiClient.addMockResponse({
  1090. url: '/organizations/org-slug/releases/',
  1091. body: [
  1092. ReleaseFixture({
  1093. shortVersion: 'sentry-android-shop@1.2.0',
  1094. version: 'sentry-android-shop@1.2.0',
  1095. }),
  1096. ],
  1097. });
  1098. const testData = initializeOrg({
  1099. organization: OrganizationFixture({
  1100. features: [
  1101. 'global-views',
  1102. 'dashboards-basic',
  1103. 'dashboards-edit',
  1104. 'discover-query',
  1105. ],
  1106. }),
  1107. router: {
  1108. location: {
  1109. ...LocationFixture(),
  1110. query: {
  1111. statsPeriod: '7d',
  1112. environment: ['alpha', 'beta'],
  1113. },
  1114. },
  1115. },
  1116. });
  1117. render(
  1118. <ViewEditDashboard
  1119. {...RouteComponentPropsFixture()}
  1120. organization={testData.organization}
  1121. params={{orgId: 'org-slug', dashboardId: '1'}}
  1122. router={testData.router}
  1123. location={testData.router.location}
  1124. >
  1125. {null}
  1126. </ViewEditDashboard>,
  1127. {context: testData.routerContext, organization: testData.organization}
  1128. );
  1129. await screen.findByText('7D');
  1130. await userEvent.click(await screen.findByText('All Releases'));
  1131. await userEvent.click(screen.getByText('sentry-android-shop@1.2.0'));
  1132. await userEvent.keyboard('{Escape}');
  1133. await userEvent.click(screen.getByText('Cancel'));
  1134. screen.getByText('All Releases');
  1135. expect(browserHistory.replace).toHaveBeenCalledWith(
  1136. expect.objectContaining({
  1137. query: expect.objectContaining({
  1138. project: undefined,
  1139. statsPeriod: undefined,
  1140. environment: undefined,
  1141. }),
  1142. })
  1143. );
  1144. });
  1145. it('disables the Edit Dashboard button when there are unsaved filters', async () => {
  1146. MockApiClient.addMockResponse({
  1147. url: '/organizations/org-slug/releases/',
  1148. body: [
  1149. ReleaseFixture({
  1150. shortVersion: 'sentry-android-shop@1.2.0',
  1151. version: 'sentry-android-shop@1.2.0',
  1152. }),
  1153. ],
  1154. });
  1155. const testData = initializeOrg({
  1156. organization: OrganizationFixture({
  1157. features: [
  1158. 'global-views',
  1159. 'dashboards-basic',
  1160. 'dashboards-edit',
  1161. 'discover-basic',
  1162. 'discover-query',
  1163. ],
  1164. }),
  1165. router: {
  1166. location: {
  1167. ...LocationFixture(),
  1168. query: {
  1169. statsPeriod: '7d',
  1170. environment: ['alpha', 'beta'],
  1171. },
  1172. },
  1173. },
  1174. });
  1175. render(
  1176. <ViewEditDashboard
  1177. {...RouteComponentPropsFixture()}
  1178. organization={testData.organization}
  1179. params={{orgId: 'org-slug', dashboardId: '1'}}
  1180. router={testData.router}
  1181. location={testData.router.location}
  1182. >
  1183. {null}
  1184. </ViewEditDashboard>,
  1185. {context: testData.routerContext, organization: testData.organization}
  1186. );
  1187. expect(await screen.findByText('Save')).toBeInTheDocument();
  1188. expect(screen.getByText('Cancel')).toBeInTheDocument();
  1189. expect(screen.getByRole('button', {name: 'Edit Dashboard'})).toBeDisabled();
  1190. });
  1191. it('ignores the order of selection of page filters to render unsaved filters', async () => {
  1192. const testProjects = [
  1193. ProjectFixture({id: '1', name: 'first', environments: ['alpha', 'beta']}),
  1194. ProjectFixture({id: '2', name: 'second', environments: ['alpha', 'beta']}),
  1195. ];
  1196. act(() => ProjectsStore.loadInitialData(testProjects));
  1197. MockApiClient.addMockResponse({
  1198. url: '/organizations/org-slug/projects/',
  1199. body: testProjects,
  1200. });
  1201. MockApiClient.addMockResponse({
  1202. url: '/organizations/org-slug/dashboards/1/',
  1203. body: DashboardFixture(widgets, {
  1204. id: '1',
  1205. title: 'Custom Errors',
  1206. filters: {},
  1207. environment: ['alpha', 'beta'],
  1208. }),
  1209. });
  1210. const testData = initializeOrg({
  1211. organization: OrganizationFixture({
  1212. features: [
  1213. 'global-views',
  1214. 'dashboards-basic',
  1215. 'dashboards-edit',
  1216. 'discover-query',
  1217. ],
  1218. }),
  1219. router: {
  1220. location: {
  1221. ...LocationFixture(),
  1222. query: {
  1223. environment: ['beta', 'alpha'], // Reversed order from saved dashboard
  1224. },
  1225. },
  1226. },
  1227. });
  1228. render(
  1229. <ViewEditDashboard
  1230. {...RouteComponentPropsFixture()}
  1231. organization={testData.organization}
  1232. params={{orgId: 'org-slug', dashboardId: '1'}}
  1233. router={testData.router}
  1234. location={testData.router.location}
  1235. >
  1236. {null}
  1237. </ViewEditDashboard>,
  1238. {context: testData.routerContext, organization: testData.organization}
  1239. );
  1240. await waitFor(() => expect(screen.queryAllByText('Loading\u2026')).toEqual([]));
  1241. await userEvent.click(screen.getByRole('button', {name: 'All Envs'}));
  1242. expect(screen.getByRole('row', {name: 'alpha'})).toHaveAttribute(
  1243. 'aria-selected',
  1244. 'true'
  1245. );
  1246. expect(screen.getByRole('row', {name: 'beta'})).toHaveAttribute(
  1247. 'aria-selected',
  1248. 'true'
  1249. );
  1250. // Save and Cancel should not appear because alpha, beta is the same as beta, alpha
  1251. expect(screen.queryByText('Save')).not.toBeInTheDocument();
  1252. expect(screen.queryByText('Cancel')).not.toBeInTheDocument();
  1253. });
  1254. it('uses releases from the URL query params', async function () {
  1255. const testData = initializeOrg({
  1256. organization: OrganizationFixture({
  1257. features: [
  1258. 'global-views',
  1259. 'dashboards-basic',
  1260. 'dashboards-edit',
  1261. 'discover-query',
  1262. ],
  1263. }),
  1264. router: {
  1265. location: {
  1266. ...LocationFixture(),
  1267. query: {
  1268. release: ['not-selected-1'],
  1269. },
  1270. },
  1271. },
  1272. });
  1273. render(
  1274. <ViewEditDashboard
  1275. {...RouteComponentPropsFixture()}
  1276. organization={testData.organization}
  1277. params={{orgId: 'org-slug', dashboardId: '1'}}
  1278. router={testData.router}
  1279. location={testData.router.location}
  1280. >
  1281. {null}
  1282. </ViewEditDashboard>,
  1283. {context: testData.routerContext, organization: testData.organization}
  1284. );
  1285. await screen.findByText(/not-selected-1/);
  1286. screen.getByText('Save');
  1287. screen.getByText('Cancel');
  1288. });
  1289. it('resets release in URL params', async function () {
  1290. MockApiClient.addMockResponse({
  1291. url: '/organizations/org-slug/dashboards/1/',
  1292. body: DashboardFixture(widgets, {
  1293. id: '1',
  1294. title: 'Custom Errors',
  1295. filters: {
  1296. release: ['abc'],
  1297. },
  1298. }),
  1299. });
  1300. const testData = initializeOrg({
  1301. organization: OrganizationFixture({
  1302. features: [
  1303. 'global-views',
  1304. 'dashboards-basic',
  1305. 'dashboards-edit',
  1306. 'discover-query',
  1307. ],
  1308. }),
  1309. router: {
  1310. location: {
  1311. ...LocationFixture(),
  1312. query: {
  1313. release: ['not-selected-1'],
  1314. },
  1315. },
  1316. },
  1317. });
  1318. render(
  1319. <ViewEditDashboard
  1320. {...RouteComponentPropsFixture()}
  1321. organization={testData.organization}
  1322. params={{orgId: 'org-slug', dashboardId: '1'}}
  1323. router={testData.router}
  1324. location={testData.router.location}
  1325. >
  1326. {null}
  1327. </ViewEditDashboard>,
  1328. {context: testData.routerContext, organization: testData.organization}
  1329. );
  1330. await screen.findByText(/not-selected-1/);
  1331. await userEvent.click(screen.getByText('Cancel'));
  1332. // release isn't used in the redirect
  1333. expect(browserHistory.replace).toHaveBeenCalledWith(
  1334. expect.objectContaining({
  1335. query: {
  1336. end: undefined,
  1337. environment: undefined,
  1338. project: undefined,
  1339. start: undefined,
  1340. statsPeriod: undefined,
  1341. utc: undefined,
  1342. },
  1343. })
  1344. );
  1345. });
  1346. it('reflects selections in the release filter in the query params', async function () {
  1347. MockApiClient.addMockResponse({
  1348. url: '/organizations/org-slug/releases/',
  1349. body: [
  1350. ReleaseFixture({
  1351. shortVersion: 'sentry-android-shop@1.2.0',
  1352. version: 'sentry-android-shop@1.2.0',
  1353. }),
  1354. ],
  1355. });
  1356. const testData = initializeOrg({
  1357. organization: OrganizationFixture({
  1358. features: [
  1359. 'global-views',
  1360. 'dashboards-basic',
  1361. 'dashboards-edit',
  1362. 'discover-query',
  1363. ],
  1364. }),
  1365. router: {
  1366. location: LocationFixture(),
  1367. },
  1368. });
  1369. render(
  1370. <ViewEditDashboard
  1371. {...RouteComponentPropsFixture()}
  1372. organization={testData.organization}
  1373. params={{orgId: 'org-slug', dashboardId: '1'}}
  1374. router={testData.router}
  1375. location={testData.router.location}
  1376. >
  1377. {null}
  1378. </ViewEditDashboard>,
  1379. {context: testData.routerContext, organization: testData.organization}
  1380. );
  1381. await userEvent.click(await screen.findByText('All Releases'));
  1382. await userEvent.click(screen.getByText('sentry-android-shop@1.2.0'));
  1383. await userEvent.click(document.body);
  1384. expect(browserHistory.push).toHaveBeenCalledWith(
  1385. expect.objectContaining({
  1386. query: expect.objectContaining({
  1387. release: ['sentry-android-shop@1.2.0'],
  1388. }),
  1389. })
  1390. );
  1391. });
  1392. it('persists release selections made during search requests that do not appear in default query', async function () {
  1393. // Default response
  1394. MockApiClient.addMockResponse({
  1395. url: '/organizations/org-slug/releases/',
  1396. body: [
  1397. ReleaseFixture({
  1398. shortVersion: 'sentry-android-shop@1.2.0',
  1399. version: 'sentry-android-shop@1.2.0',
  1400. }),
  1401. ],
  1402. });
  1403. // Mocked search results
  1404. MockApiClient.addMockResponse({
  1405. url: '/organizations/org-slug/releases/',
  1406. body: [
  1407. ReleaseFixture({
  1408. id: '9',
  1409. shortVersion: 'search-result',
  1410. version: 'search-result',
  1411. }),
  1412. ],
  1413. match: [MockApiClient.matchData({query: 's'})],
  1414. });
  1415. const testData = initializeOrg({
  1416. organization: OrganizationFixture({
  1417. features: [
  1418. 'global-views',
  1419. 'dashboards-basic',
  1420. 'dashboards-edit',
  1421. 'discover-basic',
  1422. 'discover-query',
  1423. ],
  1424. }),
  1425. router: {
  1426. location: LocationFixture(),
  1427. },
  1428. });
  1429. render(
  1430. <ViewEditDashboard
  1431. {...RouteComponentPropsFixture()}
  1432. organization={testData.organization}
  1433. params={{orgId: 'org-slug', dashboardId: '1'}}
  1434. router={testData.router}
  1435. location={testData.router.location}
  1436. >
  1437. {null}
  1438. </ViewEditDashboard>,
  1439. {context: testData.routerContext, organization: testData.organization}
  1440. );
  1441. await userEvent.click(await screen.findByText('All Releases'));
  1442. await userEvent.type(screen.getAllByPlaceholderText('Search\u2026')[2], 's');
  1443. await userEvent.click(await screen.findByRole('option', {name: 'search-result'}));
  1444. // Validate that after search is cleared, search result still appears
  1445. expect(await screen.findByText('Latest Release(s)')).toBeInTheDocument();
  1446. expect(screen.getByRole('option', {name: 'search-result'})).toBeInTheDocument();
  1447. });
  1448. });
  1449. });