queryList.spec.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. import {DiscoverSavedQueryFixture} from 'sentry-fixture/discover';
  2. import {OrganizationFixture} from 'sentry-fixture/organization';
  3. import {RouterFixture} from 'sentry-fixture/routerFixture';
  4. import {initializeOrg} from 'sentry-test/initializeOrg';
  5. import {
  6. render,
  7. screen,
  8. userEvent,
  9. waitFor,
  10. within,
  11. } from 'sentry-test/reactTestingLibrary';
  12. import {openAddToDashboardModal} from 'sentry/actionCreators/modal';
  13. import {browserHistory} from 'sentry/utils/browserHistory';
  14. import {DisplayModes, SavedQueryDatasets} from 'sentry/utils/discover/types';
  15. import {DashboardWidgetSource, DisplayType} from 'sentry/views/dashboards/types';
  16. import QueryList from 'sentry/views/discover/queryList';
  17. jest.mock('sentry/actionCreators/modal');
  18. describe('Discover > QueryList', function () {
  19. let location,
  20. savedQueries,
  21. organization,
  22. deleteMock,
  23. duplicateMock,
  24. queryChangeMock,
  25. updateHomepageMock,
  26. eventsStatsMock,
  27. wrapper;
  28. const {router} = initializeOrg();
  29. beforeAll(async function () {
  30. await import('sentry/components/modals/widgetBuilder/addToDashboardModal');
  31. });
  32. beforeEach(function () {
  33. organization = OrganizationFixture({
  34. features: ['discover-basic', 'discover-query'],
  35. });
  36. savedQueries = [
  37. DiscoverSavedQueryFixture(),
  38. DiscoverSavedQueryFixture({name: 'saved query 2', id: '2'}),
  39. ];
  40. eventsStatsMock = MockApiClient.addMockResponse({
  41. url: '/organizations/org-slug/events-stats/',
  42. method: 'GET',
  43. statusCode: 200,
  44. body: {data: []},
  45. });
  46. deleteMock = MockApiClient.addMockResponse({
  47. url: '/organizations/org-slug/discover/saved/2/',
  48. method: 'DELETE',
  49. statusCode: 200,
  50. body: {},
  51. });
  52. duplicateMock = MockApiClient.addMockResponse({
  53. url: '/organizations/org-slug/discover/saved/',
  54. method: 'POST',
  55. body: {
  56. id: '3',
  57. name: 'Saved query copy',
  58. },
  59. });
  60. updateHomepageMock = MockApiClient.addMockResponse({
  61. url: '/organizations/org-slug/discover/homepage/',
  62. method: 'PUT',
  63. statusCode: 204,
  64. });
  65. location = {
  66. pathname: '/organizations/org-slug/discover/queries/',
  67. query: {cursor: '0:1:1', statsPeriod: '14d'},
  68. };
  69. queryChangeMock = jest.fn();
  70. });
  71. afterEach(() => {
  72. jest.clearAllMocks();
  73. wrapper?.unmount();
  74. wrapper = null;
  75. });
  76. it('renders an empty list', function () {
  77. render(
  78. <QueryList
  79. router={RouterFixture()}
  80. organization={organization}
  81. savedQueries={[]}
  82. savedQuerySearchQuery="no matches"
  83. pageLinks=""
  84. renderPrebuilt={false}
  85. onQueryChange={queryChangeMock}
  86. location={location}
  87. />
  88. );
  89. expect(screen.getByText('No saved queries match that filter')).toBeInTheDocument();
  90. });
  91. it('renders pre-built queries and saved ones', async function () {
  92. render(
  93. <QueryList
  94. savedQuerySearchQuery=""
  95. router={RouterFixture()}
  96. organization={organization}
  97. savedQueries={savedQueries}
  98. renderPrebuilt
  99. pageLinks=""
  100. onQueryChange={queryChangeMock}
  101. location={location}
  102. />
  103. );
  104. await waitFor(() => {
  105. expect(screen.getAllByTestId(/card-.*/)).toHaveLength(5);
  106. });
  107. expect(eventsStatsMock).toHaveBeenCalledWith(
  108. '/organizations/org-slug/events-stats/',
  109. expect.objectContaining({
  110. query: {
  111. environment: [],
  112. interval: '30m',
  113. partial: '1',
  114. project: [],
  115. query: '',
  116. referrer: 'api.discover.default-chart',
  117. statsPeriod: '14d',
  118. yAxis: ['count()'],
  119. },
  120. })
  121. );
  122. });
  123. it('passes dataset to the query if flag is enabled', async function () {
  124. const org = OrganizationFixture({
  125. features: [
  126. 'discover-basic',
  127. 'discover-query',
  128. 'performance-discover-dataset-selector',
  129. ],
  130. });
  131. render(
  132. <QueryList
  133. savedQuerySearchQuery=""
  134. router={RouterFixture()}
  135. organization={org}
  136. savedQueries={savedQueries}
  137. renderPrebuilt
  138. pageLinks=""
  139. onQueryChange={queryChangeMock}
  140. location={location}
  141. />
  142. );
  143. await waitFor(() => {
  144. expect(screen.getAllByTestId(/card-.*/)).toHaveLength(5);
  145. });
  146. expect(eventsStatsMock).toHaveBeenCalledWith(
  147. '/organizations/org-slug/events-stats/',
  148. expect.objectContaining({
  149. query: {
  150. environment: [],
  151. interval: '30m',
  152. partial: '1',
  153. project: [],
  154. query: '',
  155. referrer: 'api.discover.default-chart',
  156. statsPeriod: '14d',
  157. yAxis: ['count()'],
  158. dataset: 'transactions',
  159. },
  160. })
  161. );
  162. });
  163. it('can duplicate and trigger change callback', async function () {
  164. render(
  165. <QueryList
  166. savedQuerySearchQuery=""
  167. router={RouterFixture()}
  168. organization={organization}
  169. savedQueries={savedQueries}
  170. pageLinks=""
  171. renderPrebuilt={false}
  172. onQueryChange={queryChangeMock}
  173. location={location}
  174. />
  175. );
  176. const card = screen.getAllByTestId(/card-*/).at(0)!;
  177. const withinCard = within(card!);
  178. expect(withinCard.getByText('Saved query #1')).toBeInTheDocument();
  179. await userEvent.click(withinCard.getByTestId('menu-trigger'));
  180. await userEvent.click(withinCard.getByText('Duplicate Query'));
  181. await waitFor(() => {
  182. expect(browserHistory.push).toHaveBeenCalledWith({
  183. pathname: location.pathname,
  184. query: {},
  185. });
  186. });
  187. expect(duplicateMock).toHaveBeenCalled();
  188. expect(queryChangeMock).toHaveBeenCalled();
  189. });
  190. it('can delete and trigger change callback', async function () {
  191. render(
  192. <QueryList
  193. savedQuerySearchQuery=""
  194. renderPrebuilt={false}
  195. router={RouterFixture()}
  196. organization={organization}
  197. savedQueries={savedQueries}
  198. pageLinks=""
  199. onQueryChange={queryChangeMock}
  200. location={location}
  201. />
  202. );
  203. const card = screen.getAllByTestId(/card-*/).at(1);
  204. const withinCard = within(card!);
  205. await userEvent.click(withinCard.getByTestId('menu-trigger'));
  206. await userEvent.click(withinCard.getByText('Delete Query'));
  207. await waitFor(() => {
  208. expect(queryChangeMock).toHaveBeenCalled();
  209. });
  210. expect(deleteMock).toHaveBeenCalled();
  211. });
  212. it('redirects to Discover on card click', async function () {
  213. render(
  214. <QueryList
  215. savedQuerySearchQuery=""
  216. router={RouterFixture()}
  217. organization={organization}
  218. savedQueries={savedQueries}
  219. pageLinks=""
  220. renderPrebuilt={false}
  221. onQueryChange={queryChangeMock}
  222. location={location}
  223. />,
  224. {router}
  225. );
  226. await userEvent.click(screen.getAllByTestId(/card-*/).at(0)!);
  227. expect(router.push).toHaveBeenLastCalledWith({
  228. pathname: '/organizations/org-slug/discover/results/',
  229. query: {id: '1', statsPeriod: '14d'},
  230. });
  231. });
  232. it('can redirect on last query deletion', async function () {
  233. render(
  234. <QueryList
  235. savedQuerySearchQuery=""
  236. router={RouterFixture()}
  237. organization={organization}
  238. savedQueries={savedQueries.slice(1)}
  239. renderPrebuilt={false}
  240. pageLinks=""
  241. onQueryChange={queryChangeMock}
  242. location={location}
  243. />,
  244. {router}
  245. );
  246. const card = screen.getAllByTestId(/card-*/).at(0)!;
  247. const withinCard = within(card!);
  248. await userEvent.click(withinCard.getByTestId('menu-trigger'));
  249. await userEvent.click(withinCard.getByText('Delete Query'));
  250. expect(deleteMock).toHaveBeenCalled();
  251. expect(queryChangeMock).not.toHaveBeenCalled();
  252. await waitFor(() => {
  253. expect(browserHistory.push).toHaveBeenCalledWith({
  254. pathname: location.pathname,
  255. query: {cursor: undefined, statsPeriod: '14d'},
  256. });
  257. });
  258. });
  259. it('renders Add to Dashboard in context menu', async function () {
  260. const featuredOrganization = OrganizationFixture({
  261. features: ['dashboards-edit'],
  262. });
  263. render(
  264. <QueryList
  265. savedQuerySearchQuery=""
  266. router={RouterFixture()}
  267. organization={featuredOrganization}
  268. savedQueries={savedQueries.slice(1)}
  269. pageLinks=""
  270. onQueryChange={queryChangeMock}
  271. renderPrebuilt={false}
  272. location={location}
  273. />
  274. );
  275. const card = screen.getAllByTestId(/card-*/).at(0)!;
  276. const withinCard = within(card!);
  277. await userEvent.click(withinCard.getByTestId('menu-trigger'));
  278. expect(
  279. screen.getByRole('menuitemradio', {name: 'Add to Dashboard'})
  280. ).toBeInTheDocument();
  281. expect(
  282. screen.getByRole('menuitemradio', {name: 'Set as Default'})
  283. ).toBeInTheDocument();
  284. expect(
  285. screen.getByRole('menuitemradio', {name: 'Duplicate Query'})
  286. ).toBeInTheDocument();
  287. expect(screen.getByRole('menuitemradio', {name: 'Delete Query'})).toBeInTheDocument();
  288. });
  289. it('only renders Delete Query and Duplicate Query in context menu', async function () {
  290. render(
  291. <QueryList
  292. savedQuerySearchQuery=""
  293. router={RouterFixture()}
  294. organization={organization}
  295. savedQueries={savedQueries.slice(1)}
  296. pageLinks=""
  297. renderPrebuilt={false}
  298. onQueryChange={queryChangeMock}
  299. location={location}
  300. />
  301. );
  302. const card = screen.getAllByTestId(/card-*/).at(0)!;
  303. const withinCard = within(card!);
  304. await userEvent.click(withinCard.getByTestId('menu-trigger'));
  305. expect(
  306. screen.queryByRole('menuitemradio', {name: 'Add to Dashboard'})
  307. ).not.toBeInTheDocument();
  308. expect(
  309. screen.getByRole('menuitemradio', {name: 'Set as Default'})
  310. ).toBeInTheDocument();
  311. expect(
  312. screen.getByRole('menuitemradio', {name: 'Duplicate Query'})
  313. ).toBeInTheDocument();
  314. expect(screen.getByRole('menuitemradio', {name: 'Delete Query'})).toBeInTheDocument();
  315. });
  316. it('passes yAxis from the savedQuery to MiniGraph', async function () {
  317. const featuredOrganization = OrganizationFixture({
  318. features: ['dashboards-edit'],
  319. });
  320. const yAxis = ['count()', 'failure_count()'];
  321. const savedQueryWithMultiYAxis = {
  322. ...savedQueries.slice(1)[0],
  323. yAxis,
  324. };
  325. render(
  326. <QueryList
  327. savedQuerySearchQuery=""
  328. router={RouterFixture()}
  329. organization={featuredOrganization}
  330. savedQueries={[savedQueryWithMultiYAxis]}
  331. pageLinks=""
  332. renderPrebuilt={false}
  333. onQueryChange={queryChangeMock}
  334. location={location}
  335. />
  336. );
  337. const chart = await screen.findByTestId('area-chart');
  338. expect(chart).toBeInTheDocument();
  339. expect(eventsStatsMock).toHaveBeenCalledWith(
  340. '/organizations/org-slug/events-stats/',
  341. expect.objectContaining({
  342. query: expect.objectContaining({
  343. yAxis: ['count()', 'failure_count()'],
  344. }),
  345. })
  346. );
  347. });
  348. it('Set as Default updates the homepage query', async function () {
  349. render(
  350. <QueryList
  351. savedQuerySearchQuery=""
  352. router={RouterFixture()}
  353. organization={organization}
  354. savedQueries={savedQueries.slice(1)}
  355. renderPrebuilt={false}
  356. pageLinks=""
  357. onQueryChange={queryChangeMock}
  358. location={location}
  359. />
  360. );
  361. await userEvent.click(screen.getByTestId('menu-trigger'));
  362. await userEvent.click(screen.getByText('Set as Default'));
  363. expect(updateHomepageMock).toHaveBeenCalledWith(
  364. '/organizations/org-slug/discover/homepage/',
  365. expect.objectContaining({
  366. data: expect.objectContaining({fields: ['test'], range: '14d'}),
  367. })
  368. );
  369. });
  370. describe('Add to Dashboard modal', () => {
  371. it('opens a modal with the correct params for Top 5 chart', async function () {
  372. const featuredOrganization = OrganizationFixture({
  373. features: ['dashboards-edit'],
  374. });
  375. render(
  376. <QueryList
  377. savedQuerySearchQuery=""
  378. router={RouterFixture()}
  379. organization={featuredOrganization}
  380. renderPrebuilt={false}
  381. savedQueries={[
  382. DiscoverSavedQueryFixture({
  383. display: DisplayModes.TOP5,
  384. orderby: 'test',
  385. fields: ['test', 'count()'],
  386. yAxis: ['count()'],
  387. }),
  388. ]}
  389. pageLinks=""
  390. onQueryChange={queryChangeMock}
  391. location={location}
  392. />
  393. );
  394. const contextMenu = await screen.findByTestId('menu-trigger');
  395. expect(contextMenu).toBeInTheDocument();
  396. expect(screen.queryByTestId('add-to-dashboard')).not.toBeInTheDocument();
  397. await userEvent.click(contextMenu);
  398. const addToDashboardMenuItem = await screen.findByTestId('add-to-dashboard');
  399. await userEvent.click(addToDashboardMenuItem);
  400. await waitFor(() => {
  401. expect(openAddToDashboardModal).toHaveBeenCalledWith(
  402. expect.objectContaining({
  403. widget: {
  404. title: 'Saved query #1',
  405. displayType: DisplayType.AREA,
  406. limit: 5,
  407. queries: [
  408. {
  409. aggregates: ['count()'],
  410. columns: ['test'],
  411. conditions: '',
  412. fields: ['test', 'count()', 'count()'],
  413. name: '',
  414. orderby: 'test',
  415. },
  416. ],
  417. },
  418. widgetAsQueryParams: expect.objectContaining({
  419. defaultTableColumns: ['test', 'count()'],
  420. defaultTitle: 'Saved query #1',
  421. defaultWidgetQuery:
  422. 'name=&aggregates=count()&columns=test&fields=test%2Ccount()%2Ccount()&conditions=&orderby=test',
  423. displayType: DisplayType.AREA,
  424. source: DashboardWidgetSource.DISCOVERV2,
  425. }),
  426. })
  427. );
  428. });
  429. });
  430. it('opens a modal with the correct params for other chart', async function () {
  431. const featuredOrganization = OrganizationFixture({
  432. features: ['dashboards-edit'],
  433. });
  434. render(
  435. <QueryList
  436. savedQuerySearchQuery=""
  437. router={RouterFixture()}
  438. renderPrebuilt={false}
  439. organization={featuredOrganization}
  440. savedQueries={[
  441. DiscoverSavedQueryFixture({
  442. display: DisplayModes.DEFAULT,
  443. orderby: 'count()',
  444. fields: ['test', 'count()'],
  445. yAxis: ['count()'],
  446. queryDataset: SavedQueryDatasets.TRANSACTIONS,
  447. }),
  448. ]}
  449. pageLinks=""
  450. onQueryChange={queryChangeMock}
  451. location={location}
  452. />
  453. );
  454. const contextMenu = await screen.findByTestId('menu-trigger');
  455. expect(contextMenu).toBeInTheDocument();
  456. expect(screen.queryByTestId('add-to-dashboard')).not.toBeInTheDocument();
  457. await userEvent.click(contextMenu);
  458. const addToDashboardMenuItem = await screen.findByTestId('add-to-dashboard');
  459. await userEvent.click(addToDashboardMenuItem);
  460. await waitFor(() => {
  461. expect(openAddToDashboardModal).toHaveBeenCalledWith(
  462. expect.objectContaining({
  463. widget: {
  464. title: 'Saved query #1',
  465. displayType: DisplayType.LINE,
  466. queries: [
  467. {
  468. aggregates: ['count()'],
  469. columns: [],
  470. conditions: '',
  471. fields: ['count()'],
  472. name: '',
  473. // Orderby gets dropped because ordering only applies to
  474. // Top-N and tables
  475. orderby: '',
  476. },
  477. ],
  478. },
  479. widgetAsQueryParams: expect.objectContaining({
  480. defaultTableColumns: ['test', 'count()'],
  481. defaultTitle: 'Saved query #1',
  482. defaultWidgetQuery:
  483. 'name=&aggregates=count()&columns=&fields=count()&conditions=&orderby=',
  484. displayType: DisplayType.LINE,
  485. source: DashboardWidgetSource.DISCOVERV2,
  486. }),
  487. })
  488. );
  489. });
  490. });
  491. });
  492. it('passes dataset to open modal', async function () {
  493. const featuredOrganization = OrganizationFixture({
  494. features: ['dashboards-edit', 'performance-discover-dataset-selector'],
  495. });
  496. render(
  497. <QueryList
  498. savedQuerySearchQuery=""
  499. router={RouterFixture()}
  500. renderPrebuilt={false}
  501. organization={featuredOrganization}
  502. savedQueries={[
  503. DiscoverSavedQueryFixture({
  504. display: DisplayModes.DEFAULT,
  505. orderby: 'count()',
  506. fields: ['test', 'count()'],
  507. yAxis: ['count()'],
  508. queryDataset: SavedQueryDatasets.TRANSACTIONS,
  509. }),
  510. ]}
  511. pageLinks=""
  512. onQueryChange={queryChangeMock}
  513. location={location}
  514. />
  515. );
  516. const contextMenu = await screen.findByTestId('menu-trigger');
  517. expect(contextMenu).toBeInTheDocument();
  518. expect(screen.queryByTestId('add-to-dashboard')).not.toBeInTheDocument();
  519. await userEvent.click(contextMenu);
  520. const addToDashboardMenuItem = await screen.findByTestId('add-to-dashboard');
  521. await userEvent.click(addToDashboardMenuItem);
  522. await waitFor(() => {
  523. expect(openAddToDashboardModal).toHaveBeenCalledWith(
  524. expect.objectContaining({
  525. widget: {
  526. displayType: 'line',
  527. interval: undefined,
  528. limit: undefined,
  529. queries: [
  530. {
  531. aggregates: ['count()'],
  532. columns: [],
  533. conditions: '',
  534. fields: ['count()'],
  535. name: '',
  536. orderby: '',
  537. },
  538. ],
  539. title: 'Saved query #1',
  540. widgetType: 'transaction-like',
  541. },
  542. widgetAsQueryParams: expect.objectContaining({
  543. cursor: '0:1:1',
  544. dataset: 'transaction-like',
  545. defaultTableColumns: ['test', 'count()'],
  546. defaultTitle: 'Saved query #1',
  547. defaultWidgetQuery:
  548. 'name=&aggregates=count()&columns=&fields=count()&conditions=&orderby=',
  549. displayType: 'line',
  550. source: 'discoverv2',
  551. statsPeriod: '14d',
  552. }),
  553. })
  554. );
  555. });
  556. });
  557. });