detail.spec.jsx 47 KB

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