detail.spec.jsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827
  1. import {enforceActOnUseLegacyStoreHook, mountWithTheme} from 'sentry-test/enzyme';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {mountGlobalModal} from 'sentry-test/modal';
  4. import {act, render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
  5. import * as modals from 'sentry/actionCreators/modal';
  6. import ProjectsStore from 'sentry/stores/projectsStore';
  7. import CreateDashboard from 'sentry/views/dashboardsV2/create';
  8. import {constructGridItemKey} from 'sentry/views/dashboardsV2/layoutUtils';
  9. import {DashboardWidgetSource} from 'sentry/views/dashboardsV2/types';
  10. import * as types from 'sentry/views/dashboardsV2/types';
  11. import ViewEditDashboard from 'sentry/views/dashboardsV2/view';
  12. describe('Dashboards > Detail', function () {
  13. enforceActOnUseLegacyStoreHook();
  14. const organization = TestStubs.Organization({
  15. features: [
  16. 'global-views',
  17. 'dashboards-basic',
  18. 'dashboards-edit',
  19. 'discover-query',
  20. 'dashboard-grid-layout',
  21. ],
  22. });
  23. const projects = [TestStubs.Project()];
  24. describe('prebuilt dashboards', function () {
  25. let wrapper, initialData;
  26. beforeEach(function () {
  27. act(() => ProjectsStore.loadInitialData(projects));
  28. initialData = initializeOrg({organization});
  29. MockApiClient.addMockResponse({
  30. url: '/organizations/org-slug/tags/',
  31. body: [],
  32. });
  33. MockApiClient.addMockResponse({
  34. url: '/organizations/org-slug/projects/',
  35. body: [TestStubs.Project()],
  36. });
  37. MockApiClient.addMockResponse({
  38. url: '/organizations/org-slug/dashboards/',
  39. body: [
  40. TestStubs.Dashboard([], {id: 'default-overview', title: 'Default'}),
  41. TestStubs.Dashboard([], {id: '1', title: 'Custom Errors'}),
  42. ],
  43. });
  44. MockApiClient.addMockResponse({
  45. url: '/organizations/org-slug/dashboards/default-overview/',
  46. body: TestStubs.Dashboard([], {id: 'default-overview', title: 'Default'}),
  47. });
  48. MockApiClient.addMockResponse({
  49. url: '/organizations/org-slug/dashboards/1/visit/',
  50. method: 'POST',
  51. body: [],
  52. statusCode: 200,
  53. });
  54. MockApiClient.addMockResponse({
  55. url: '/organizations/org-slug/users/',
  56. method: 'GET',
  57. body: [],
  58. });
  59. });
  60. afterEach(function () {
  61. MockApiClient.clearMockResponses();
  62. if (wrapper) {
  63. wrapper.unmount();
  64. }
  65. });
  66. it('assigns unique IDs to all widgets so grid keys are unique', async function () {
  67. MockApiClient.addMockResponse({
  68. url: '/organizations/org-slug/events-stats/',
  69. body: {data: []},
  70. });
  71. MockApiClient.addMockResponse({
  72. url: '/organizations/org-slug/dashboards/default-overview/',
  73. body: TestStubs.Dashboard(
  74. [
  75. TestStubs.Widget(
  76. [
  77. {
  78. name: '',
  79. conditions: 'event.type:error',
  80. fields: ['count()'],
  81. aggregates: ['count()'],
  82. columns: [],
  83. },
  84. ],
  85. {
  86. title: 'Default Widget 1',
  87. interval: '1d',
  88. }
  89. ),
  90. TestStubs.Widget(
  91. [
  92. {
  93. name: '',
  94. conditions: 'event.type:transaction',
  95. fields: ['count()'],
  96. aggregates: ['count()'],
  97. columns: [],
  98. },
  99. ],
  100. {
  101. title: 'Default Widget 2',
  102. interval: '1d',
  103. }
  104. ),
  105. ],
  106. {id: 'default-overview', title: 'Default'}
  107. ),
  108. });
  109. initialData = initializeOrg({
  110. organization: TestStubs.Organization({
  111. features: [
  112. 'global-views',
  113. 'dashboards-basic',
  114. 'discover-query',
  115. 'dashboard-grid-layout',
  116. ],
  117. projects: [TestStubs.Project()],
  118. }),
  119. });
  120. wrapper = mountWithTheme(
  121. <ViewEditDashboard
  122. organization={initialData.organization}
  123. params={{orgId: 'org-slug', dashboardId: 'default-overview'}}
  124. router={initialData.router}
  125. location={initialData.router.location}
  126. />,
  127. initialData.routerContext
  128. );
  129. await tick();
  130. wrapper.update();
  131. const dashboardInstance = wrapper.find('Dashboard').instance();
  132. const assignedIds = new Set(
  133. dashboardInstance.props.dashboard.widgets.map(constructGridItemKey)
  134. );
  135. expect(assignedIds.size).toBe(dashboardInstance.props.dashboard.widgets.length);
  136. });
  137. });
  138. describe('custom dashboards', function () {
  139. let wrapper, initialData, widgets, mockVisit, mockPut;
  140. const openEditModal = jest.spyOn(modals, 'openAddDashboardWidgetModal');
  141. beforeEach(function () {
  142. window.confirm = jest.fn();
  143. initialData = initializeOrg({organization});
  144. widgets = [
  145. TestStubs.Widget(
  146. [
  147. {
  148. name: '',
  149. conditions: 'event.type:error',
  150. fields: ['count()'],
  151. columns: [],
  152. aggregates: ['count()'],
  153. },
  154. ],
  155. {
  156. title: 'Errors',
  157. interval: '1d',
  158. id: '1',
  159. }
  160. ),
  161. TestStubs.Widget(
  162. [
  163. {
  164. name: '',
  165. conditions: 'event.type:transaction',
  166. fields: ['count()'],
  167. columns: [],
  168. aggregates: ['count()'],
  169. },
  170. ],
  171. {
  172. title: 'Transactions',
  173. interval: '1d',
  174. id: '2',
  175. }
  176. ),
  177. TestStubs.Widget(
  178. [
  179. {
  180. name: '',
  181. conditions: 'event.type:transaction transaction:/api/cats',
  182. fields: ['p50()'],
  183. columns: [],
  184. aggregates: ['p50()'],
  185. },
  186. ],
  187. {
  188. title: 'p50 of /api/cats',
  189. interval: '1d',
  190. id: '3',
  191. }
  192. ),
  193. ];
  194. mockVisit = MockApiClient.addMockResponse({
  195. url: '/organizations/org-slug/dashboards/1/visit/',
  196. method: 'POST',
  197. body: [],
  198. statusCode: 200,
  199. });
  200. MockApiClient.addMockResponse({
  201. url: '/organizations/org-slug/tags/',
  202. body: [],
  203. });
  204. MockApiClient.addMockResponse({
  205. url: '/organizations/org-slug/projects/',
  206. body: [TestStubs.Project()],
  207. });
  208. MockApiClient.addMockResponse({
  209. url: '/organizations/org-slug/dashboards/',
  210. body: [
  211. TestStubs.Dashboard([], {
  212. id: 'default-overview',
  213. title: 'Default',
  214. widgetDisplay: ['area'],
  215. }),
  216. TestStubs.Dashboard([], {
  217. id: '1',
  218. title: 'Custom Errors',
  219. widgetDisplay: ['area'],
  220. }),
  221. ],
  222. });
  223. MockApiClient.addMockResponse({
  224. url: '/organizations/org-slug/dashboards/1/',
  225. body: TestStubs.Dashboard(widgets, {id: '1', title: 'Custom Errors'}),
  226. });
  227. mockPut = MockApiClient.addMockResponse({
  228. url: '/organizations/org-slug/dashboards/1/',
  229. method: 'PUT',
  230. body: TestStubs.Dashboard(widgets, {id: '1', title: 'Custom Errors'}),
  231. });
  232. MockApiClient.addMockResponse({
  233. url: '/organizations/org-slug/events-stats/',
  234. body: {data: []},
  235. });
  236. MockApiClient.addMockResponse({
  237. method: 'POST',
  238. url: '/organizations/org-slug/dashboards/widgets/',
  239. body: [],
  240. });
  241. MockApiClient.addMockResponse({
  242. method: 'GET',
  243. url: '/organizations/org-slug/recent-searches/',
  244. body: [],
  245. });
  246. MockApiClient.addMockResponse({
  247. method: 'GET',
  248. url: '/organizations/org-slug/issues/',
  249. body: [],
  250. });
  251. MockApiClient.addMockResponse({
  252. url: '/organizations/org-slug/eventsv2/',
  253. method: 'GET',
  254. body: [],
  255. });
  256. MockApiClient.addMockResponse({
  257. url: '/organizations/org-slug/users/',
  258. method: 'GET',
  259. body: [],
  260. });
  261. });
  262. afterEach(function () {
  263. MockApiClient.clearMockResponses();
  264. jest.clearAllMocks();
  265. if (wrapper) {
  266. wrapper.unmount();
  267. wrapper = null;
  268. }
  269. });
  270. it('can remove widgets', async function () {
  271. const updateMock = MockApiClient.addMockResponse({
  272. url: '/organizations/org-slug/dashboards/1/',
  273. method: 'PUT',
  274. body: TestStubs.Dashboard([widgets[0]], {id: '1', title: 'Custom Errors'}),
  275. });
  276. wrapper = mountWithTheme(
  277. <ViewEditDashboard
  278. organization={initialData.organization}
  279. params={{orgId: 'org-slug', dashboardId: '1'}}
  280. router={initialData.router}
  281. location={initialData.router.location}
  282. />,
  283. initialData.routerContext
  284. );
  285. await tick();
  286. wrapper.update();
  287. expect(mockVisit).toHaveBeenCalledTimes(1);
  288. // Enter edit mode.
  289. wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
  290. // Remove the second and third widgets
  291. wrapper
  292. .find('WidgetCard')
  293. .at(1)
  294. .find('Button[data-test-id="widget-delete"]')
  295. .simulate('click');
  296. wrapper
  297. .find('WidgetCard')
  298. .at(1)
  299. .find('Button[data-test-id="widget-delete"]')
  300. .simulate('click');
  301. // Save changes
  302. wrapper.find('Controls Button[data-test-id="dashboard-commit"]').simulate('click');
  303. await tick();
  304. expect(updateMock).toHaveBeenCalled();
  305. expect(updateMock).toHaveBeenCalledWith(
  306. '/organizations/org-slug/dashboards/1/',
  307. expect.objectContaining({
  308. data: expect.objectContaining({
  309. title: 'Custom Errors',
  310. widgets: [expect.objectContaining(widgets[0])],
  311. }),
  312. })
  313. );
  314. // Visit should not be called again on dashboard update
  315. expect(mockVisit).toHaveBeenCalledTimes(1);
  316. });
  317. it('can enter edit mode for widgets', async function () {
  318. wrapper = mountWithTheme(
  319. <ViewEditDashboard
  320. organization={initialData.organization}
  321. params={{orgId: 'org-slug', dashboardId: '1'}}
  322. router={initialData.router}
  323. location={initialData.router.location}
  324. />,
  325. initialData.routerContext
  326. );
  327. await tick();
  328. wrapper.update();
  329. // Enter edit mode.
  330. wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
  331. wrapper.update();
  332. const card = wrapper.find('WidgetCard').first();
  333. card.find('StyledPanel').simulate('mouseOver');
  334. // Edit the first widget
  335. wrapper
  336. .find('WidgetCard')
  337. .first()
  338. .find('Button[data-test-id="widget-edit"]')
  339. .simulate('click');
  340. expect(openEditModal).toHaveBeenCalledTimes(1);
  341. expect(openEditModal).toHaveBeenCalledWith(
  342. expect.objectContaining({
  343. widget: {
  344. id: '1',
  345. interval: '1d',
  346. layout: {h: 2, minH: 2, w: 2, x: 0, y: 0},
  347. queries: [
  348. {
  349. conditions: 'event.type:error',
  350. fields: ['count()'],
  351. aggregates: ['count()'],
  352. columns: [],
  353. name: '',
  354. },
  355. ],
  356. title: 'Errors',
  357. type: 'line',
  358. },
  359. })
  360. );
  361. });
  362. it('shows add widget option', async function () {
  363. wrapper = mountWithTheme(
  364. <ViewEditDashboard
  365. organization={initialData.organization}
  366. params={{orgId: 'org-slug', dashboardId: '1'}}
  367. router={initialData.router}
  368. location={initialData.router.location}
  369. />,
  370. initialData.routerContext
  371. );
  372. await tick();
  373. wrapper.update();
  374. // Enter edit mode.
  375. wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
  376. wrapper.update();
  377. expect(wrapper.find('AddWidget').exists()).toBe(true);
  378. });
  379. it('opens custom modal when add widget option is clicked', async function () {
  380. wrapper = mountWithTheme(
  381. <ViewEditDashboard
  382. organization={initialData.organization}
  383. params={{orgId: 'org-slug', dashboardId: '1'}}
  384. router={initialData.router}
  385. location={initialData.router.location}
  386. />,
  387. initialData.routerContext
  388. );
  389. await tick();
  390. wrapper.update();
  391. // Enter edit mode.
  392. wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
  393. wrapper.update();
  394. wrapper.find('AddButton[data-test-id="widget-add"]').simulate('click');
  395. expect(openEditModal).toHaveBeenCalledTimes(1);
  396. });
  397. it('opens widget library when add widget option is clicked', async function () {
  398. initialData = initializeOrg({
  399. organization: TestStubs.Organization({
  400. features: [
  401. 'global-views',
  402. 'dashboards-basic',
  403. 'dashboards-edit',
  404. 'discover-query',
  405. 'widget-library',
  406. ],
  407. projects: [TestStubs.Project()],
  408. }),
  409. });
  410. wrapper = mountWithTheme(
  411. <ViewEditDashboard
  412. organization={initialData.organization}
  413. params={{orgId: 'org-slug', dashboardId: '1'}}
  414. router={initialData.router}
  415. location={initialData.router.location}
  416. />,
  417. initialData.routerContext
  418. );
  419. await tick();
  420. wrapper.update();
  421. // Enter edit mode.
  422. wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
  423. wrapper.update();
  424. wrapper.find('AddButton[data-test-id="widget-add"]').simulate('click');
  425. expect(openEditModal).toHaveBeenCalledTimes(1);
  426. expect(openEditModal).toHaveBeenCalledWith(
  427. expect.objectContaining({
  428. source: types.DashboardWidgetSource.LIBRARY,
  429. })
  430. );
  431. });
  432. it('hides add widget option', async function () {
  433. types.MAX_WIDGETS = 1;
  434. wrapper = mountWithTheme(
  435. <ViewEditDashboard
  436. organization={initialData.organization}
  437. params={{orgId: 'org-slug', dashboardId: '1'}}
  438. router={initialData.router}
  439. location={initialData.router.location}
  440. />,
  441. initialData.routerContext
  442. );
  443. await tick();
  444. wrapper.update();
  445. // Enter edit mode.
  446. wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
  447. wrapper.update();
  448. expect(wrapper.find('AddWidget').exists()).toBe(false);
  449. });
  450. it('renders successfully if more widgets than stored layouts', async function () {
  451. // A case where someone has async added widgets to a dashboard
  452. MockApiClient.addMockResponse({
  453. url: '/organizations/org-slug/dashboards/1/',
  454. body: TestStubs.Dashboard(
  455. [
  456. TestStubs.Widget(
  457. [
  458. {
  459. name: '',
  460. conditions: 'event.type:error',
  461. fields: ['count()'],
  462. aggregates: ['count()'],
  463. columns: [],
  464. },
  465. ],
  466. {
  467. title: 'First Widget',
  468. interval: '1d',
  469. id: '1',
  470. layout: {i: 'grid-item-1', x: 0, y: 0, w: 2, h: 6},
  471. }
  472. ),
  473. TestStubs.Widget(
  474. [
  475. {
  476. name: '',
  477. conditions: 'event.type:error',
  478. fields: ['count()'],
  479. aggregates: ['count()'],
  480. columns: [],
  481. },
  482. ],
  483. {
  484. title: 'Second Widget',
  485. interval: '1d',
  486. id: '2',
  487. }
  488. ),
  489. ],
  490. {id: '1', title: 'Custom Errors'}
  491. ),
  492. });
  493. render(
  494. <ViewEditDashboard
  495. organization={initialData.organization}
  496. params={{orgId: 'org-slug', dashboardId: '1'}}
  497. router={initialData.router}
  498. location={initialData.router.location}
  499. />,
  500. {context: initialData.routerContext}
  501. );
  502. await tick();
  503. await screen.findByText('First Widget');
  504. await screen.findByText('Second Widget');
  505. });
  506. it('does not trigger request if layout not updated', async () => {
  507. MockApiClient.addMockResponse({
  508. url: '/organizations/org-slug/dashboards/1/',
  509. body: TestStubs.Dashboard(
  510. [
  511. TestStubs.Widget(
  512. [
  513. {
  514. name: '',
  515. conditions: 'event.type:error',
  516. fields: ['count()'],
  517. aggregates: ['count()'],
  518. columns: [],
  519. },
  520. ],
  521. {
  522. title: 'First Widget',
  523. interval: '1d',
  524. id: '1',
  525. layout: {i: 'grid-item-1', x: 0, y: 0, w: 2, h: 6},
  526. }
  527. ),
  528. ],
  529. {id: '1', title: 'Custom Errors'}
  530. ),
  531. });
  532. render(
  533. <ViewEditDashboard
  534. organization={initialData.organization}
  535. params={{orgId: 'org-slug', dashboardId: '1'}}
  536. router={initialData.router}
  537. location={initialData.router.location}
  538. />,
  539. {context: initialData.routerContext}
  540. );
  541. await tick();
  542. userEvent.click(screen.getByText('Edit Dashboard'));
  543. userEvent.click(screen.getByText('Save and Finish'));
  544. await tick();
  545. expect(screen.getByText('Edit Dashboard')).toBeInTheDocument();
  546. expect(mockPut).not.toHaveBeenCalled();
  547. });
  548. it('renders the custom resize handler for a widget', async () => {
  549. MockApiClient.addMockResponse({
  550. url: '/organizations/org-slug/dashboards/1/',
  551. body: TestStubs.Dashboard(
  552. [
  553. TestStubs.Widget(
  554. [
  555. {
  556. name: '',
  557. conditions: 'event.type:error',
  558. fields: ['count()'],
  559. aggregates: ['count()'],
  560. columns: [],
  561. },
  562. ],
  563. {
  564. title: 'First Widget',
  565. interval: '1d',
  566. id: '1',
  567. layout: {i: 'grid-item-1', x: 0, y: 0, w: 2, h: 6},
  568. }
  569. ),
  570. ],
  571. {id: '1', title: 'Custom Errors'}
  572. ),
  573. });
  574. render(
  575. <ViewEditDashboard
  576. organization={initialData.organization}
  577. params={{orgId: 'org-slug', dashboardId: '1'}}
  578. router={initialData.router}
  579. location={initialData.router.location}
  580. />,
  581. {context: initialData.routerContext}
  582. );
  583. await tick();
  584. userEvent.click(await screen.findByText('Edit Dashboard'));
  585. const widget = screen.getByText('First Widget').closest('.react-grid-item');
  586. const resizeHandle = within(widget).getByTestId('custom-resize-handle');
  587. expect(resizeHandle).toBeVisible();
  588. });
  589. it('does not trigger an alert when the widgets have no layout and user cancels without changes', async () => {
  590. MockApiClient.addMockResponse({
  591. url: '/organizations/org-slug/dashboards/1/',
  592. body: TestStubs.Dashboard(
  593. [
  594. TestStubs.Widget(
  595. [
  596. {
  597. name: '',
  598. conditions: 'event.type:error',
  599. fields: ['count()'],
  600. aggregates: ['count()'],
  601. columns: [],
  602. },
  603. ],
  604. {
  605. title: 'First Widget',
  606. interval: '1d',
  607. id: '1',
  608. layout: null,
  609. }
  610. ),
  611. ],
  612. {id: '1', title: 'Custom Errors'}
  613. ),
  614. });
  615. render(
  616. <ViewEditDashboard
  617. organization={initialData.organization}
  618. params={{orgId: 'org-slug', dashboardId: '1'}}
  619. router={initialData.router}
  620. location={initialData.router.location}
  621. />,
  622. {context: initialData.routerContext}
  623. );
  624. await tick();
  625. userEvent.click(await screen.findByText('Edit Dashboard'));
  626. userEvent.click(await screen.findByText('Cancel'));
  627. expect(window.confirm).not.toHaveBeenCalled();
  628. });
  629. it('opens the widget viewer modal using the widget id specified in the url', () => {
  630. const openWidgetViewerModal = jest.spyOn(modals, 'openWidgetViewerModal');
  631. const widget = TestStubs.Widget(
  632. [
  633. {
  634. name: '',
  635. conditions: 'event.type:error',
  636. fields: ['count()'],
  637. aggregates: ['count()'],
  638. columns: [],
  639. },
  640. ],
  641. {
  642. title: 'First Widget',
  643. interval: '1d',
  644. id: '1',
  645. layout: null,
  646. }
  647. );
  648. MockApiClient.addMockResponse({
  649. url: '/organizations/org-slug/dashboards/1/',
  650. body: TestStubs.Dashboard([widget], {id: '1', title: 'Custom Errors'}),
  651. });
  652. render(
  653. <ViewEditDashboard
  654. organization={initialData.organization}
  655. params={{orgId: 'org-slug', dashboardId: '1', widgetId: '1'}}
  656. router={initialData.router}
  657. location={{...initialData.router.location, pathname: '/widget/123/'}}
  658. />,
  659. {context: initialData.routerContext}
  660. );
  661. expect(openWidgetViewerModal).toHaveBeenCalledWith(
  662. expect.objectContaining({
  663. organization: initialData.organization,
  664. widget,
  665. onClose: expect.anything(),
  666. })
  667. );
  668. });
  669. it('redirects user to dashboard url if widget is not found', () => {
  670. const openWidgetViewerModal = jest.spyOn(modals, 'openWidgetViewerModal');
  671. MockApiClient.addMockResponse({
  672. url: '/organizations/org-slug/dashboards/1/',
  673. body: TestStubs.Dashboard([], {id: '1', title: 'Custom Errors'}),
  674. });
  675. render(
  676. <ViewEditDashboard
  677. organization={initialData.organization}
  678. params={{orgId: 'org-slug', dashboardId: '1', widgetId: '123'}}
  679. router={initialData.router}
  680. location={{...initialData.router.location, pathname: '/widget/123/'}}
  681. />,
  682. {context: initialData.routerContext}
  683. );
  684. expect(openWidgetViewerModal).not.toHaveBeenCalled();
  685. expect(initialData.router.replace).toHaveBeenCalledWith(
  686. expect.objectContaining({
  687. pathname: '/organizations/org-slug/dashboard/1/',
  688. query: {},
  689. })
  690. );
  691. });
  692. it('opens the edit widget modal when clicking the edit button', async () => {
  693. const widget = TestStubs.Widget(
  694. [
  695. {
  696. name: '',
  697. conditions: 'event.type:error',
  698. fields: ['count()'],
  699. columns: [],
  700. aggregates: ['count()'],
  701. },
  702. ],
  703. {
  704. title: 'First Widget',
  705. interval: '1d',
  706. id: '1',
  707. layout: null,
  708. }
  709. );
  710. MockApiClient.addMockResponse({
  711. url: '/organizations/org-slug/dashboards/1/',
  712. body: TestStubs.Dashboard([widget], {id: '1', title: 'Custom Errors'}),
  713. });
  714. render(
  715. <ViewEditDashboard
  716. organization={initialData.organization}
  717. params={{orgId: 'org-slug', dashboardId: '1', widgetId: '1'}}
  718. router={initialData.router}
  719. location={{...initialData.router.location, pathname: '/widget/123/'}}
  720. />,
  721. {context: initialData.routerContext}
  722. );
  723. await mountGlobalModal(initialData.routerContext);
  724. userEvent.click(screen.getByRole('button', {name: 'Edit Widget'}));
  725. expect(openEditModal).toHaveBeenCalledWith(
  726. expect.objectContaining({
  727. widget,
  728. organization: initialData.organization,
  729. source: DashboardWidgetSource.DASHBOARDS,
  730. })
  731. );
  732. });
  733. it('opens the widget viewer modal in a prebuilt dashboard using the widget id specified in the url', () => {
  734. const openWidgetViewerModal = jest.spyOn(modals, 'openWidgetViewerModal');
  735. render(
  736. <CreateDashboard
  737. organization={initialData.organization}
  738. params={{orgId: 'org-slug', templateId: 'default-template', widgetId: '2'}}
  739. router={initialData.router}
  740. location={{...initialData.router.location, pathname: '/widget/2/'}}
  741. />,
  742. {context: initialData.routerContext}
  743. );
  744. expect(openWidgetViewerModal).toHaveBeenCalledWith(
  745. expect.objectContaining({
  746. organization: initialData.organization,
  747. widget: expect.objectContaining({
  748. displayType: 'line',
  749. interval: '5m',
  750. queries: [
  751. {
  752. aggregates: ['count()'],
  753. columns: [],
  754. conditions: '!event.type:transaction',
  755. fields: ['count()'],
  756. name: 'Events',
  757. orderby: 'count()',
  758. },
  759. ],
  760. title: 'Events',
  761. widgetType: 'discover',
  762. }),
  763. onClose: expect.anything(),
  764. })
  765. );
  766. });
  767. });
  768. });