detail.spec.tsx 48 KB

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