addToDashboardModal.spec.tsx 14 KB

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