addToDashboardModal.spec.tsx 18 KB

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