detail.spec.tsx 48 KB

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