addToDashboardModal.spec.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. import {LocationFixture} from 'sentry-fixture/locationFixture';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  4. import selectEvent from 'sentry-test/selectEvent';
  5. import type {ModalRenderProps} from 'sentry/actionCreators/modal';
  6. import AddToDashboardModal from 'sentry/components/modals/widgetBuilder/addToDashboardModal';
  7. import type {DashboardDetails, DashboardListItem} from 'sentry/views/dashboards/types';
  8. import {DashboardWidgetSource, DisplayType} from 'sentry/views/dashboards/types';
  9. const stubEl = (props: {children?: React.ReactNode}) => <div>{props.children}</div>;
  10. const mockWidgetAsQueryParams = {
  11. defaultTableColumns: ['field1', 'field2'],
  12. defaultTitle: 'Default title',
  13. defaultWidgetQuery: '',
  14. displayType: DisplayType.LINE,
  15. end: undefined,
  16. environment: [],
  17. project: [1],
  18. source: DashboardWidgetSource.DISCOVERV2,
  19. start: undefined,
  20. statsPeriod: '1h',
  21. utc: undefined,
  22. };
  23. describe('add to dashboard modal', () => {
  24. let eventsStatsMock;
  25. let initialData;
  26. const testDashboardListItem: DashboardListItem = {
  27. id: '1',
  28. title: 'Test Dashboard',
  29. createdBy: undefined,
  30. dateCreated: '2020-01-01T00:00:00.000Z',
  31. widgetDisplay: [DisplayType.AREA],
  32. widgetPreview: [],
  33. };
  34. const testDashboard: DashboardDetails = {
  35. id: '1',
  36. title: 'Test Dashboard',
  37. createdBy: undefined,
  38. dateCreated: '2020-01-01T00:00:00.000Z',
  39. widgets: [],
  40. environment: [],
  41. projects: [1],
  42. period: '1h',
  43. filters: {release: ['abc@v1.2.0']},
  44. };
  45. let widget = {
  46. title: 'Test title',
  47. description: 'Test description',
  48. displayType: DisplayType.LINE,
  49. interval: '5m',
  50. queries: [
  51. {
  52. conditions: '',
  53. fields: ['count()'],
  54. aggregates: ['count()'],
  55. fieldAliases: [],
  56. columns: [] as string[],
  57. orderby: '',
  58. name: '',
  59. },
  60. ],
  61. };
  62. const defaultSelection = {
  63. projects: [],
  64. environments: [],
  65. datetime: {
  66. start: null,
  67. end: null,
  68. period: '24h',
  69. utc: false,
  70. },
  71. };
  72. beforeEach(() => {
  73. initialData = initializeOrg();
  74. MockApiClient.addMockResponse({
  75. url: '/organizations/org-slug/dashboards/',
  76. body: [{...testDashboardListItem, widgetDisplay: [DisplayType.AREA]}],
  77. });
  78. MockApiClient.addMockResponse({
  79. url: '/organizations/org-slug/dashboards/1/',
  80. body: testDashboard,
  81. });
  82. eventsStatsMock = MockApiClient.addMockResponse({
  83. url: '/organizations/org-slug/events-stats/',
  84. body: [],
  85. });
  86. });
  87. afterEach(() => {
  88. MockApiClient.clearMockResponses();
  89. jest.clearAllMocks();
  90. });
  91. it('renders with the widget title and description', async function () {
  92. render(
  93. <AddToDashboardModal
  94. Header={stubEl}
  95. Footer={stubEl as ModalRenderProps['Footer']}
  96. Body={stubEl as ModalRenderProps['Body']}
  97. CloseButton={stubEl}
  98. closeModal={() => undefined}
  99. organization={initialData.organization}
  100. widget={widget}
  101. selection={defaultSelection}
  102. router={initialData.router}
  103. widgetAsQueryParams={mockWidgetAsQueryParams}
  104. location={LocationFixture()}
  105. />
  106. );
  107. await waitFor(() => {
  108. expect(screen.getByText('Select Dashboard')).toBeEnabled();
  109. });
  110. expect(screen.getByText('Test title')).toBeInTheDocument();
  111. expect(screen.getByText('Select Dashboard')).toBeInTheDocument();
  112. expect(
  113. screen.getByText(
  114. /This is a preview of how the widget will appear in your dashboard./
  115. )
  116. ).toBeInTheDocument();
  117. expect(screen.getByRole('button', {name: 'Add + Stay on this Page'})).toBeDisabled();
  118. expect(screen.getByRole('button', {name: 'Open in Widget Builder'})).toBeDisabled();
  119. });
  120. it('enables the buttons when a dashboard is selected', async function () {
  121. render(
  122. <AddToDashboardModal
  123. Header={stubEl}
  124. Footer={stubEl as ModalRenderProps['Footer']}
  125. Body={stubEl as ModalRenderProps['Body']}
  126. CloseButton={stubEl}
  127. closeModal={() => undefined}
  128. organization={initialData.organization}
  129. widget={widget}
  130. selection={defaultSelection}
  131. router={initialData.router}
  132. widgetAsQueryParams={mockWidgetAsQueryParams}
  133. location={LocationFixture()}
  134. />
  135. );
  136. await waitFor(() => {
  137. expect(screen.getByText('Select Dashboard')).toBeEnabled();
  138. });
  139. expect(screen.getByRole('button', {name: 'Add + Stay on this Page'})).toBeDisabled();
  140. expect(screen.getByRole('button', {name: 'Open in Widget Builder'})).toBeDisabled();
  141. await selectEvent.select(screen.getByText('Select Dashboard'), 'Test Dashboard');
  142. expect(screen.getByRole('button', {name: 'Add + Stay on this Page'})).toBeEnabled();
  143. expect(screen.getByRole('button', {name: 'Open in Widget Builder'})).toBeEnabled();
  144. });
  145. it('includes a New Dashboard option in the selector with saved dashboards', async function () {
  146. render(
  147. <AddToDashboardModal
  148. Header={stubEl}
  149. Footer={stubEl as ModalRenderProps['Footer']}
  150. Body={stubEl as ModalRenderProps['Body']}
  151. CloseButton={stubEl}
  152. closeModal={() => undefined}
  153. organization={initialData.organization}
  154. widget={widget}
  155. selection={defaultSelection}
  156. router={initialData.router}
  157. widgetAsQueryParams={mockWidgetAsQueryParams}
  158. location={LocationFixture()}
  159. />
  160. );
  161. await waitFor(() => {
  162. expect(screen.getByText('Select Dashboard')).toBeEnabled();
  163. });
  164. await selectEvent.openMenu(screen.getByText('Select Dashboard'));
  165. expect(screen.getByText('+ Create New Dashboard')).toBeInTheDocument();
  166. expect(screen.getByText('Test Dashboard')).toBeInTheDocument();
  167. });
  168. it('applies dashboard saved filters to visualization', async function () {
  169. render(
  170. <AddToDashboardModal
  171. Header={stubEl}
  172. Footer={stubEl as ModalRenderProps['Footer']}
  173. Body={stubEl as ModalRenderProps['Body']}
  174. CloseButton={stubEl}
  175. closeModal={() => undefined}
  176. organization={initialData.organization}
  177. widget={widget}
  178. selection={defaultSelection}
  179. router={initialData.router}
  180. widgetAsQueryParams={mockWidgetAsQueryParams}
  181. location={LocationFixture()}
  182. />
  183. );
  184. await waitFor(() => {
  185. expect(screen.getByText('Select Dashboard')).toBeEnabled();
  186. });
  187. expect(eventsStatsMock).toHaveBeenCalledWith(
  188. '/organizations/org-slug/events-stats/',
  189. expect.objectContaining({
  190. query: expect.objectContaining({
  191. environment: [],
  192. project: [],
  193. interval: '5m',
  194. orderby: '',
  195. statsPeriod: '24h',
  196. yAxis: ['count()'],
  197. }),
  198. })
  199. );
  200. await selectEvent.select(screen.getByText('Select Dashboard'), 'Test Dashboard');
  201. expect(eventsStatsMock).toHaveBeenLastCalledWith(
  202. '/organizations/org-slug/events-stats/',
  203. expect.objectContaining({
  204. query: expect.objectContaining({
  205. environment: [],
  206. interval: '1m',
  207. orderby: '',
  208. partial: '1',
  209. project: [1],
  210. query: ' release:"abc@v1.2.0" ',
  211. statsPeriod: '1h',
  212. yAxis: ['count()'],
  213. }),
  214. })
  215. );
  216. });
  217. it('calls the events stats endpoint with the query and selection values', async function () {
  218. render(
  219. <AddToDashboardModal
  220. Header={stubEl}
  221. Footer={stubEl as ModalRenderProps['Footer']}
  222. Body={stubEl as ModalRenderProps['Body']}
  223. CloseButton={stubEl}
  224. closeModal={() => undefined}
  225. organization={initialData.organization}
  226. widget={widget}
  227. selection={defaultSelection}
  228. router={initialData.router}
  229. widgetAsQueryParams={mockWidgetAsQueryParams}
  230. location={LocationFixture()}
  231. />
  232. );
  233. await waitFor(() => {
  234. expect(screen.getByText('Select Dashboard')).toBeEnabled();
  235. });
  236. expect(eventsStatsMock).toHaveBeenCalledWith(
  237. '/organizations/org-slug/events-stats/',
  238. expect.objectContaining({
  239. query: expect.objectContaining({
  240. environment: [],
  241. project: [],
  242. interval: '5m',
  243. orderby: '',
  244. statsPeriod: '24h',
  245. yAxis: ['count()'],
  246. }),
  247. })
  248. );
  249. });
  250. it('navigates to the widget builder when clicking Open in Widget Builder', async () => {
  251. render(
  252. <AddToDashboardModal
  253. Header={stubEl}
  254. Footer={stubEl as ModalRenderProps['Footer']}
  255. Body={stubEl as ModalRenderProps['Body']}
  256. CloseButton={stubEl}
  257. closeModal={() => undefined}
  258. organization={initialData.organization}
  259. widget={widget}
  260. selection={defaultSelection}
  261. router={initialData.router}
  262. widgetAsQueryParams={mockWidgetAsQueryParams}
  263. location={LocationFixture()}
  264. />
  265. );
  266. await waitFor(() => {
  267. expect(screen.getByText('Select Dashboard')).toBeEnabled();
  268. });
  269. await selectEvent.select(screen.getByText('Select Dashboard'), 'Test Dashboard');
  270. await userEvent.click(screen.getByText('Open in Widget Builder'));
  271. expect(initialData.router.push).toHaveBeenCalledWith({
  272. pathname: '/organizations/org-slug/dashboard/1/widget/new/',
  273. query: mockWidgetAsQueryParams,
  274. });
  275. });
  276. it('navigates to the widget builder with saved filters', async () => {
  277. render(
  278. <AddToDashboardModal
  279. Header={stubEl}
  280. Footer={stubEl as ModalRenderProps['Footer']}
  281. Body={stubEl as ModalRenderProps['Body']}
  282. CloseButton={stubEl}
  283. closeModal={() => undefined}
  284. organization={initialData.organization}
  285. widget={widget}
  286. selection={defaultSelection}
  287. router={initialData.router}
  288. widgetAsQueryParams={mockWidgetAsQueryParams}
  289. location={LocationFixture()}
  290. />
  291. );
  292. await waitFor(() => {
  293. expect(screen.getByText('Select Dashboard')).toBeEnabled();
  294. });
  295. await selectEvent.select(screen.getByText('Select Dashboard'), 'Test Dashboard');
  296. await userEvent.click(screen.getByText('Open in Widget Builder'));
  297. expect(initialData.router.push).toHaveBeenCalledWith({
  298. pathname: '/organizations/org-slug/dashboard/1/widget/new/',
  299. query: expect.objectContaining({
  300. defaultTableColumns: ['field1', 'field2'],
  301. defaultTitle: 'Default title',
  302. defaultWidgetQuery: '',
  303. displayType: DisplayType.LINE,
  304. project: [1],
  305. source: DashboardWidgetSource.DISCOVERV2,
  306. statsPeriod: '1h',
  307. }),
  308. });
  309. });
  310. it('updates the selected dashboard with the widget when clicking Add + Stay in Discover', async () => {
  311. const dashboardDetailGetMock = MockApiClient.addMockResponse({
  312. url: '/organizations/org-slug/dashboards/1/',
  313. body: {id: '1', widgets: []},
  314. });
  315. const dashboardDetailPutMock = MockApiClient.addMockResponse({
  316. url: '/organizations/org-slug/dashboards/1/',
  317. method: 'PUT',
  318. body: {},
  319. });
  320. render(
  321. <AddToDashboardModal
  322. Header={stubEl}
  323. Footer={stubEl as ModalRenderProps['Footer']}
  324. Body={stubEl as ModalRenderProps['Body']}
  325. CloseButton={stubEl}
  326. closeModal={() => undefined}
  327. organization={initialData.organization}
  328. widget={widget}
  329. selection={defaultSelection}
  330. router={initialData.router}
  331. widgetAsQueryParams={mockWidgetAsQueryParams}
  332. location={LocationFixture()}
  333. />
  334. );
  335. await waitFor(() => {
  336. expect(screen.getByText('Select Dashboard')).toBeEnabled();
  337. });
  338. await selectEvent.select(screen.getByText('Select Dashboard'), 'Test Dashboard');
  339. await userEvent.click(screen.getByText('Add + Stay on this Page'));
  340. expect(dashboardDetailGetMock).toHaveBeenCalled();
  341. // mocked widgets response is an empty array, assert this new widget
  342. // is sent as an update to the dashboard
  343. await waitFor(() => {
  344. expect(dashboardDetailPutMock).toHaveBeenCalledWith(
  345. '/organizations/org-slug/dashboards/1/',
  346. expect.objectContaining({data: expect.objectContaining({widgets: [widget]})})
  347. );
  348. });
  349. });
  350. it('clears sort when clicking Add + Stay in Discover with line chart', async () => {
  351. const dashboardDetailGetMock = MockApiClient.addMockResponse({
  352. url: '/organizations/org-slug/dashboards/1/',
  353. body: {id: '1', widgets: []},
  354. });
  355. const dashboardDetailPutMock = MockApiClient.addMockResponse({
  356. url: '/organizations/org-slug/dashboards/1/',
  357. method: 'PUT',
  358. body: {},
  359. });
  360. widget = {
  361. ...widget,
  362. queries: [
  363. {
  364. conditions: '',
  365. fields: ['count()'],
  366. aggregates: ['count()'],
  367. fieldAliases: [],
  368. columns: [],
  369. orderby: '-project',
  370. name: '',
  371. },
  372. ],
  373. };
  374. render(
  375. <AddToDashboardModal
  376. Header={stubEl}
  377. Footer={stubEl as ModalRenderProps['Footer']}
  378. Body={stubEl as ModalRenderProps['Body']}
  379. CloseButton={stubEl}
  380. closeModal={() => undefined}
  381. organization={initialData.organization}
  382. widget={widget}
  383. selection={defaultSelection}
  384. router={initialData.router}
  385. widgetAsQueryParams={mockWidgetAsQueryParams}
  386. location={LocationFixture()}
  387. />
  388. );
  389. await waitFor(() => {
  390. expect(screen.getByText('Select Dashboard')).toBeEnabled();
  391. });
  392. await selectEvent.select(screen.getByText('Select Dashboard'), 'Test Dashboard');
  393. await userEvent.click(screen.getByText('Add + Stay on this Page'));
  394. expect(dashboardDetailGetMock).toHaveBeenCalled();
  395. // mocked widgets response is an empty array, assert this new widget
  396. // is sent as an update to the dashboard
  397. await waitFor(() => {
  398. expect(dashboardDetailPutMock).toHaveBeenCalledWith(
  399. '/organizations/org-slug/dashboards/1/',
  400. expect.objectContaining({
  401. data: expect.objectContaining({
  402. widgets: [
  403. {
  404. description: 'Test description',
  405. displayType: 'line',
  406. interval: '5m',
  407. queries: [
  408. {
  409. aggregates: ['count()'],
  410. columns: [],
  411. conditions: '',
  412. fieldAliases: [],
  413. fields: ['count()'],
  414. name: '',
  415. orderby: '',
  416. },
  417. ],
  418. title: 'Test title',
  419. },
  420. ],
  421. }),
  422. })
  423. );
  424. });
  425. });
  426. it('saves sort when clicking Add + Stay in Discover with top period chart', async () => {
  427. const dashboardDetailGetMock = MockApiClient.addMockResponse({
  428. url: '/organizations/org-slug/dashboards/1/',
  429. body: {id: '1', widgets: []},
  430. });
  431. const dashboardDetailPutMock = MockApiClient.addMockResponse({
  432. url: '/organizations/org-slug/dashboards/1/',
  433. method: 'PUT',
  434. body: {},
  435. });
  436. widget = {
  437. ...widget,
  438. displayType: DisplayType.TOP_N,
  439. queries: [
  440. {
  441. conditions: '',
  442. fields: ['count()'],
  443. aggregates: ['count()'],
  444. fieldAliases: [],
  445. columns: ['project'],
  446. orderby: '-project',
  447. name: '',
  448. },
  449. ],
  450. };
  451. render(
  452. <AddToDashboardModal
  453. Header={stubEl}
  454. Footer={stubEl as ModalRenderProps['Footer']}
  455. Body={stubEl as ModalRenderProps['Body']}
  456. CloseButton={stubEl}
  457. closeModal={() => undefined}
  458. organization={initialData.organization}
  459. widget={widget}
  460. selection={defaultSelection}
  461. router={initialData.router}
  462. widgetAsQueryParams={mockWidgetAsQueryParams}
  463. location={LocationFixture()}
  464. />
  465. );
  466. await waitFor(() => {
  467. expect(screen.getByText('Select Dashboard')).toBeEnabled();
  468. });
  469. await selectEvent.select(screen.getByText('Select Dashboard'), 'Test Dashboard');
  470. await userEvent.click(screen.getByText('Add + Stay on this Page'));
  471. expect(dashboardDetailGetMock).toHaveBeenCalled();
  472. // mocked widgets response is an empty array, assert this new widget
  473. // is sent as an update to the dashboard
  474. await waitFor(() => {
  475. expect(dashboardDetailPutMock).toHaveBeenCalledWith(
  476. '/organizations/org-slug/dashboards/1/',
  477. expect.objectContaining({
  478. data: expect.objectContaining({
  479. widgets: [
  480. {
  481. description: 'Test description',
  482. displayType: 'top_n',
  483. interval: '5m',
  484. limit: 5,
  485. queries: [
  486. {
  487. aggregates: ['count()'],
  488. columns: ['project'],
  489. conditions: '',
  490. fieldAliases: [],
  491. fields: ['count()'],
  492. name: '',
  493. orderby: '-project',
  494. },
  495. ],
  496. title: 'Test title',
  497. },
  498. ],
  499. }),
  500. })
  501. );
  502. });
  503. });
  504. it('disables Add + Stay in Discover when a new dashboard is selected', async () => {
  505. render(
  506. <AddToDashboardModal
  507. Header={stubEl}
  508. Footer={stubEl as ModalRenderProps['Footer']}
  509. Body={stubEl as ModalRenderProps['Body']}
  510. CloseButton={stubEl}
  511. closeModal={() => undefined}
  512. organization={initialData.organization}
  513. widget={widget}
  514. selection={defaultSelection}
  515. router={initialData.router}
  516. widgetAsQueryParams={mockWidgetAsQueryParams}
  517. location={LocationFixture()}
  518. />
  519. );
  520. await waitFor(() => {
  521. expect(screen.getByText('Select Dashboard')).toBeEnabled();
  522. });
  523. await selectEvent.select(
  524. screen.getByText('Select Dashboard'),
  525. '+ Create New Dashboard'
  526. );
  527. expect(screen.getByRole('button', {name: 'Add + Stay on this Page'})).toBeDisabled();
  528. expect(screen.getByRole('button', {name: 'Open in Widget Builder'})).toBeEnabled();
  529. });
  530. });