detail.spec.tsx 48 KB

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