detail.spec.tsx 48 KB

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