addToDashboardModal.spec.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  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/dashboards/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. end: undefined,
  19. environment: [],
  20. project: [1],
  21. source: DashboardWidgetSource.DISCOVERV2,
  22. start: undefined,
  23. statsPeriod: '1h',
  24. utc: undefined,
  25. };
  26. describe('add to dashboard modal', () => {
  27. let eventsStatsMock;
  28. let initialData;
  29. const testDashboardListItem: DashboardListItem = {
  30. id: '1',
  31. title: 'Test Dashboard',
  32. createdBy: undefined,
  33. dateCreated: '2020-01-01T00:00:00.000Z',
  34. widgetDisplay: [DisplayType.AREA],
  35. widgetPreview: [],
  36. };
  37. const testDashboard: DashboardDetails = {
  38. id: '1',
  39. title: 'Test Dashboard',
  40. createdBy: undefined,
  41. dateCreated: '2020-01-01T00:00:00.000Z',
  42. widgets: [],
  43. environment: [],
  44. projects: [1],
  45. period: '1h',
  46. filters: {release: ['abc@v1.2.0']},
  47. };
  48. let widget = {
  49. title: 'Test title',
  50. description: 'Test description',
  51. displayType: DisplayType.LINE,
  52. interval: '5m',
  53. queries: [
  54. {
  55. conditions: '',
  56. fields: ['count()'],
  57. aggregates: ['count()'],
  58. fieldAliases: [],
  59. columns: [] as string[],
  60. orderby: '',
  61. name: '',
  62. },
  63. ],
  64. };
  65. const defaultSelection = {
  66. projects: [],
  67. environments: [],
  68. datetime: {
  69. start: null,
  70. end: null,
  71. period: '24h',
  72. utc: false,
  73. },
  74. };
  75. beforeEach(() => {
  76. initialData = initializeOrg();
  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. afterEach(() => {
  91. MockApiClient.clearMockResponses();
  92. jest.clearAllMocks();
  93. });
  94. it('renders with the widget title and description', async function () {
  95. render(
  96. <AddToDashboardModal
  97. Header={stubEl}
  98. Footer={stubEl as ModalRenderProps['Footer']}
  99. Body={stubEl as ModalRenderProps['Body']}
  100. CloseButton={stubEl}
  101. closeModal={() => undefined}
  102. organization={initialData.organization}
  103. widget={widget}
  104. selection={defaultSelection}
  105. router={initialData.router}
  106. widgetAsQueryParams={mockWidgetAsQueryParams}
  107. location={TestStubs.location()}
  108. />
  109. );
  110. await waitFor(() => {
  111. expect(screen.getByText('Select Dashboard')).toBeEnabled();
  112. });
  113. expect(screen.getByText('Test title')).toBeInTheDocument();
  114. expect(screen.getByText('Select Dashboard')).toBeInTheDocument();
  115. expect(
  116. screen.getByText(
  117. /This is a preview of how the widget will appear in your dashboard./
  118. )
  119. ).toBeInTheDocument();
  120. expect(screen.getByRole('button', {name: 'Add + Stay in Discover'})).toBeDisabled();
  121. expect(screen.getByRole('button', {name: 'Open in Widget Builder'})).toBeDisabled();
  122. });
  123. it('enables the buttons when a dashboard is selected', async function () {
  124. render(
  125. <AddToDashboardModal
  126. Header={stubEl}
  127. Footer={stubEl as ModalRenderProps['Footer']}
  128. Body={stubEl as ModalRenderProps['Body']}
  129. CloseButton={stubEl}
  130. closeModal={() => undefined}
  131. organization={initialData.organization}
  132. widget={widget}
  133. selection={defaultSelection}
  134. router={initialData.router}
  135. widgetAsQueryParams={mockWidgetAsQueryParams}
  136. location={TestStubs.location()}
  137. />
  138. );
  139. await waitFor(() => {
  140. expect(screen.getByText('Select Dashboard')).toBeEnabled();
  141. });
  142. expect(screen.getByRole('button', {name: 'Add + Stay in Discover'})).toBeDisabled();
  143. expect(screen.getByRole('button', {name: 'Open in Widget Builder'})).toBeDisabled();
  144. await selectEvent.select(screen.getByText('Select Dashboard'), 'Test Dashboard');
  145. expect(screen.getByRole('button', {name: 'Add + Stay in Discover'})).toBeEnabled();
  146. expect(screen.getByRole('button', {name: 'Open in Widget Builder'})).toBeEnabled();
  147. });
  148. it('includes a New Dashboard option in the selector with saved dashboards', async function () {
  149. render(
  150. <AddToDashboardModal
  151. Header={stubEl}
  152. Footer={stubEl as ModalRenderProps['Footer']}
  153. Body={stubEl as ModalRenderProps['Body']}
  154. CloseButton={stubEl}
  155. closeModal={() => undefined}
  156. organization={initialData.organization}
  157. widget={widget}
  158. selection={defaultSelection}
  159. router={initialData.router}
  160. widgetAsQueryParams={mockWidgetAsQueryParams}
  161. location={TestStubs.location()}
  162. />
  163. );
  164. await waitFor(() => {
  165. expect(screen.getByText('Select Dashboard')).toBeEnabled();
  166. });
  167. selectEvent.openMenu(screen.getByText('Select Dashboard'));
  168. expect(screen.getByText('+ Create New Dashboard')).toBeInTheDocument();
  169. expect(screen.getByText('Test Dashboard')).toBeInTheDocument();
  170. });
  171. it('applies dashboard saved filters to visualization', async function () {
  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. await 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. render(
  281. <AddToDashboardModal
  282. Header={stubEl}
  283. Footer={stubEl as ModalRenderProps['Footer']}
  284. Body={stubEl as ModalRenderProps['Body']}
  285. CloseButton={stubEl}
  286. closeModal={() => undefined}
  287. organization={initialData.organization}
  288. widget={widget}
  289. selection={defaultSelection}
  290. router={initialData.router}
  291. widgetAsQueryParams={mockWidgetAsQueryParams}
  292. location={TestStubs.location()}
  293. />
  294. );
  295. await waitFor(() => {
  296. expect(screen.getByText('Select Dashboard')).toBeEnabled();
  297. });
  298. await selectEvent.select(screen.getByText('Select Dashboard'), 'Test Dashboard');
  299. await userEvent.click(screen.getByText('Open in Widget Builder'));
  300. expect(initialData.router.push).toHaveBeenCalledWith({
  301. pathname: '/organizations/org-slug/dashboard/1/widget/new/',
  302. query: expect.objectContaining({
  303. defaultTableColumns: ['field1', 'field2'],
  304. defaultTitle: 'Default title',
  305. defaultWidgetQuery: '',
  306. displayType: DisplayType.LINE,
  307. project: [1],
  308. source: DashboardWidgetSource.DISCOVERV2,
  309. statsPeriod: '1h',
  310. }),
  311. });
  312. });
  313. it('updates the selected dashboard with the widget when clicking Add + Stay in Discover', async () => {
  314. const dashboardDetailGetMock = MockApiClient.addMockResponse({
  315. url: '/organizations/org-slug/dashboards/1/',
  316. body: {id: '1', widgets: []},
  317. });
  318. const dashboardDetailPutMock = MockApiClient.addMockResponse({
  319. url: '/organizations/org-slug/dashboards/1/',
  320. method: 'PUT',
  321. body: {},
  322. });
  323. render(
  324. <AddToDashboardModal
  325. Header={stubEl}
  326. Footer={stubEl as ModalRenderProps['Footer']}
  327. Body={stubEl as ModalRenderProps['Body']}
  328. CloseButton={stubEl}
  329. closeModal={() => undefined}
  330. organization={initialData.organization}
  331. widget={widget}
  332. selection={defaultSelection}
  333. router={initialData.router}
  334. widgetAsQueryParams={mockWidgetAsQueryParams}
  335. location={TestStubs.location()}
  336. />
  337. );
  338. await waitFor(() => {
  339. expect(screen.getByText('Select Dashboard')).toBeEnabled();
  340. });
  341. await selectEvent.select(screen.getByText('Select Dashboard'), 'Test Dashboard');
  342. await userEvent.click(screen.getByText('Add + Stay in Discover'));
  343. expect(dashboardDetailGetMock).toHaveBeenCalled();
  344. // mocked widgets response is an empty array, assert this new widget
  345. // is sent as an update to the dashboard
  346. await waitFor(() => {
  347. expect(dashboardDetailPutMock).toHaveBeenCalledWith(
  348. '/organizations/org-slug/dashboards/1/',
  349. expect.objectContaining({data: expect.objectContaining({widgets: [widget]})})
  350. );
  351. });
  352. });
  353. it('clears sort when clicking Add + Stay in Discover with line chart', async () => {
  354. const dashboardDetailGetMock = MockApiClient.addMockResponse({
  355. url: '/organizations/org-slug/dashboards/1/',
  356. body: {id: '1', widgets: []},
  357. });
  358. const dashboardDetailPutMock = MockApiClient.addMockResponse({
  359. url: '/organizations/org-slug/dashboards/1/',
  360. method: 'PUT',
  361. body: {},
  362. });
  363. widget = {
  364. ...widget,
  365. queries: [
  366. {
  367. conditions: '',
  368. fields: ['count()'],
  369. aggregates: ['count()'],
  370. fieldAliases: [],
  371. columns: [],
  372. orderby: '-project',
  373. name: '',
  374. },
  375. ],
  376. };
  377. render(
  378. <AddToDashboardModal
  379. Header={stubEl}
  380. Footer={stubEl as ModalRenderProps['Footer']}
  381. Body={stubEl as ModalRenderProps['Body']}
  382. CloseButton={stubEl}
  383. closeModal={() => undefined}
  384. organization={initialData.organization}
  385. widget={widget}
  386. selection={defaultSelection}
  387. router={initialData.router}
  388. widgetAsQueryParams={mockWidgetAsQueryParams}
  389. location={TestStubs.location()}
  390. />
  391. );
  392. await waitFor(() => {
  393. expect(screen.getByText('Select Dashboard')).toBeEnabled();
  394. });
  395. await selectEvent.select(screen.getByText('Select Dashboard'), 'Test Dashboard');
  396. await userEvent.click(screen.getByText('Add + Stay in Discover'));
  397. expect(dashboardDetailGetMock).toHaveBeenCalled();
  398. // mocked widgets response is an empty array, assert this new widget
  399. // is sent as an update to the dashboard
  400. await waitFor(() => {
  401. expect(dashboardDetailPutMock).toHaveBeenCalledWith(
  402. '/organizations/org-slug/dashboards/1/',
  403. expect.objectContaining({
  404. data: expect.objectContaining({
  405. widgets: [
  406. {
  407. description: 'Test description',
  408. displayType: 'line',
  409. interval: '5m',
  410. queries: [
  411. {
  412. aggregates: ['count()'],
  413. columns: [],
  414. conditions: '',
  415. fieldAliases: [],
  416. fields: ['count()'],
  417. name: '',
  418. orderby: '',
  419. },
  420. ],
  421. title: 'Test title',
  422. },
  423. ],
  424. }),
  425. })
  426. );
  427. });
  428. });
  429. it('saves sort when clicking Add + Stay in Discover with top period chart', async () => {
  430. const dashboardDetailGetMock = MockApiClient.addMockResponse({
  431. url: '/organizations/org-slug/dashboards/1/',
  432. body: {id: '1', widgets: []},
  433. });
  434. const dashboardDetailPutMock = MockApiClient.addMockResponse({
  435. url: '/organizations/org-slug/dashboards/1/',
  436. method: 'PUT',
  437. body: {},
  438. });
  439. widget = {
  440. ...widget,
  441. displayType: DisplayType.TOP_N,
  442. queries: [
  443. {
  444. conditions: '',
  445. fields: ['count()'],
  446. aggregates: ['count()'],
  447. fieldAliases: [],
  448. columns: ['project'],
  449. orderby: '-project',
  450. name: '',
  451. },
  452. ],
  453. };
  454. render(
  455. <AddToDashboardModal
  456. Header={stubEl}
  457. Footer={stubEl as ModalRenderProps['Footer']}
  458. Body={stubEl as ModalRenderProps['Body']}
  459. CloseButton={stubEl}
  460. closeModal={() => undefined}
  461. organization={initialData.organization}
  462. widget={widget}
  463. selection={defaultSelection}
  464. router={initialData.router}
  465. widgetAsQueryParams={mockWidgetAsQueryParams}
  466. location={TestStubs.location()}
  467. />
  468. );
  469. await waitFor(() => {
  470. expect(screen.getByText('Select Dashboard')).toBeEnabled();
  471. });
  472. await selectEvent.select(screen.getByText('Select Dashboard'), 'Test Dashboard');
  473. await userEvent.click(screen.getByText('Add + Stay in Discover'));
  474. expect(dashboardDetailGetMock).toHaveBeenCalled();
  475. // mocked widgets response is an empty array, assert this new widget
  476. // is sent as an update to the dashboard
  477. await waitFor(() => {
  478. expect(dashboardDetailPutMock).toHaveBeenCalledWith(
  479. '/organizations/org-slug/dashboards/1/',
  480. expect.objectContaining({
  481. data: expect.objectContaining({
  482. widgets: [
  483. {
  484. description: 'Test description',
  485. displayType: 'top_n',
  486. interval: '5m',
  487. limit: 5,
  488. queries: [
  489. {
  490. aggregates: ['count()'],
  491. columns: ['project'],
  492. conditions: '',
  493. fieldAliases: [],
  494. fields: ['count()'],
  495. name: '',
  496. orderby: '-project',
  497. },
  498. ],
  499. title: 'Test title',
  500. },
  501. ],
  502. }),
  503. })
  504. );
  505. });
  506. });
  507. it('disables Add + Stay in Discover when a new dashboard is selected', async () => {
  508. render(
  509. <AddToDashboardModal
  510. Header={stubEl}
  511. Footer={stubEl as ModalRenderProps['Footer']}
  512. Body={stubEl as ModalRenderProps['Body']}
  513. CloseButton={stubEl}
  514. closeModal={() => undefined}
  515. organization={initialData.organization}
  516. widget={widget}
  517. selection={defaultSelection}
  518. router={initialData.router}
  519. widgetAsQueryParams={mockWidgetAsQueryParams}
  520. location={TestStubs.location()}
  521. />
  522. );
  523. await waitFor(() => {
  524. expect(screen.getByText('Select Dashboard')).toBeEnabled();
  525. });
  526. await selectEvent.select(
  527. screen.getByText('Select Dashboard'),
  528. '+ Create New Dashboard'
  529. );
  530. expect(screen.getByRole('button', {name: 'Add + Stay in Discover'})).toBeDisabled();
  531. expect(screen.getByRole('button', {name: 'Open in Widget Builder'})).toBeEnabled();
  532. });
  533. });