queryList.spec.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692
  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('renders pre-built queries with dataset', async function () {
  124. organization = OrganizationFixture({
  125. features: [
  126. 'discover-basic',
  127. 'discover-query',
  128. 'performance-view',
  129. 'performance-discover-dataset-selector',
  130. ],
  131. });
  132. render(
  133. <QueryList
  134. savedQuerySearchQuery=""
  135. router={RouterFixture()}
  136. organization={organization}
  137. savedQueries={[]}
  138. renderPrebuilt
  139. pageLinks=""
  140. onQueryChange={queryChangeMock}
  141. location={location}
  142. />,
  143. {router}
  144. );
  145. await waitFor(() => {
  146. expect(screen.getAllByTestId(/card-.*/)).toHaveLength(5);
  147. });
  148. expect(eventsStatsMock).toHaveBeenCalledWith(
  149. '/organizations/org-slug/events-stats/',
  150. expect.objectContaining({
  151. query: expect.objectContaining({
  152. dataset: 'transactions',
  153. query: '',
  154. referrer: 'api.discover.homepage.prebuilt',
  155. statsPeriod: '24h',
  156. yAxis: 'count()',
  157. }),
  158. })
  159. );
  160. expect(eventsStatsMock).toHaveBeenCalledWith(
  161. '/organizations/org-slug/events-stats/',
  162. expect.objectContaining({
  163. query: expect.objectContaining({
  164. dataset: 'errors',
  165. environment: [],
  166. field: ['url', 'count()', 'count_unique(issue)'],
  167. query: 'has:url',
  168. referrer: 'api.discover.homepage.prebuilt',
  169. statsPeriod: '24h',
  170. topEvents: 5,
  171. yAxis: 'count()',
  172. }),
  173. })
  174. );
  175. await userEvent.click(screen.getAllByTestId(/card-*/).at(0)!);
  176. expect(router.push).toHaveBeenLastCalledWith({
  177. pathname: '/organizations/org-slug/discover/results/',
  178. query: expect.objectContaining({queryDataset: 'error-events'}),
  179. });
  180. });
  181. it('passes dataset to the query if flag is enabled', async function () {
  182. const org = OrganizationFixture({
  183. features: [
  184. 'discover-basic',
  185. 'discover-query',
  186. 'performance-discover-dataset-selector',
  187. ],
  188. });
  189. render(
  190. <QueryList
  191. savedQuerySearchQuery=""
  192. router={RouterFixture()}
  193. organization={org}
  194. savedQueries={savedQueries}
  195. renderPrebuilt
  196. pageLinks=""
  197. onQueryChange={queryChangeMock}
  198. location={location}
  199. />
  200. );
  201. await waitFor(() => {
  202. expect(screen.getAllByTestId(/card-.*/)).toHaveLength(5);
  203. });
  204. expect(eventsStatsMock).toHaveBeenCalledWith(
  205. '/organizations/org-slug/events-stats/',
  206. expect.objectContaining({
  207. query: {
  208. environment: [],
  209. interval: '30m',
  210. partial: '1',
  211. project: [],
  212. query: '',
  213. referrer: 'api.discover.default-chart',
  214. statsPeriod: '14d',
  215. yAxis: ['count()'],
  216. dataset: 'transactions',
  217. },
  218. })
  219. );
  220. });
  221. it('can duplicate and trigger change callback', async function () {
  222. render(
  223. <QueryList
  224. savedQuerySearchQuery=""
  225. router={RouterFixture()}
  226. organization={organization}
  227. savedQueries={savedQueries}
  228. pageLinks=""
  229. renderPrebuilt={false}
  230. onQueryChange={queryChangeMock}
  231. location={location}
  232. />
  233. );
  234. const card = screen.getAllByTestId(/card-*/).at(0)!;
  235. const withinCard = within(card!);
  236. expect(withinCard.getByText('Saved query #1')).toBeInTheDocument();
  237. await userEvent.click(withinCard.getByTestId('menu-trigger'));
  238. await userEvent.click(withinCard.getByText('Duplicate Query'));
  239. await waitFor(() => {
  240. expect(browserHistory.push).toHaveBeenCalledWith({
  241. pathname: location.pathname,
  242. query: {},
  243. });
  244. });
  245. expect(duplicateMock).toHaveBeenCalled();
  246. expect(queryChangeMock).toHaveBeenCalled();
  247. });
  248. it('can delete and trigger change callback', async function () {
  249. render(
  250. <QueryList
  251. savedQuerySearchQuery=""
  252. renderPrebuilt={false}
  253. router={RouterFixture()}
  254. organization={organization}
  255. savedQueries={savedQueries}
  256. pageLinks=""
  257. onQueryChange={queryChangeMock}
  258. location={location}
  259. />
  260. );
  261. const card = screen.getAllByTestId(/card-*/).at(1);
  262. const withinCard = within(card!);
  263. await userEvent.click(withinCard.getByTestId('menu-trigger'));
  264. await userEvent.click(withinCard.getByText('Delete Query'));
  265. await waitFor(() => {
  266. expect(queryChangeMock).toHaveBeenCalled();
  267. });
  268. expect(deleteMock).toHaveBeenCalled();
  269. });
  270. it('redirects to Discover on card click', async function () {
  271. render(
  272. <QueryList
  273. savedQuerySearchQuery=""
  274. router={RouterFixture()}
  275. organization={organization}
  276. savedQueries={savedQueries}
  277. pageLinks=""
  278. renderPrebuilt={false}
  279. onQueryChange={queryChangeMock}
  280. location={location}
  281. />,
  282. {router}
  283. );
  284. await userEvent.click(screen.getAllByTestId(/card-*/).at(0)!);
  285. expect(router.push).toHaveBeenLastCalledWith({
  286. pathname: '/organizations/org-slug/discover/results/',
  287. query: {id: '1', statsPeriod: '14d'},
  288. });
  289. });
  290. it('can redirect on last query deletion', async function () {
  291. render(
  292. <QueryList
  293. savedQuerySearchQuery=""
  294. router={RouterFixture()}
  295. organization={organization}
  296. savedQueries={savedQueries.slice(1)}
  297. renderPrebuilt={false}
  298. pageLinks=""
  299. onQueryChange={queryChangeMock}
  300. location={location}
  301. />,
  302. {router}
  303. );
  304. const card = screen.getAllByTestId(/card-*/).at(0)!;
  305. const withinCard = within(card!);
  306. await userEvent.click(withinCard.getByTestId('menu-trigger'));
  307. await userEvent.click(withinCard.getByText('Delete Query'));
  308. expect(deleteMock).toHaveBeenCalled();
  309. expect(queryChangeMock).not.toHaveBeenCalled();
  310. await waitFor(() => {
  311. expect(browserHistory.push).toHaveBeenCalledWith({
  312. pathname: location.pathname,
  313. query: {cursor: undefined, statsPeriod: '14d'},
  314. });
  315. });
  316. });
  317. it('renders Add to Dashboard in context menu', async function () {
  318. const featuredOrganization = OrganizationFixture({
  319. features: ['dashboards-edit'],
  320. });
  321. render(
  322. <QueryList
  323. savedQuerySearchQuery=""
  324. router={RouterFixture()}
  325. organization={featuredOrganization}
  326. savedQueries={savedQueries.slice(1)}
  327. pageLinks=""
  328. onQueryChange={queryChangeMock}
  329. renderPrebuilt={false}
  330. location={location}
  331. />
  332. );
  333. const card = screen.getAllByTestId(/card-*/).at(0)!;
  334. const withinCard = within(card!);
  335. await userEvent.click(withinCard.getByTestId('menu-trigger'));
  336. expect(
  337. screen.getByRole('menuitemradio', {name: 'Add to Dashboard'})
  338. ).toBeInTheDocument();
  339. expect(
  340. screen.getByRole('menuitemradio', {name: 'Set as Default'})
  341. ).toBeInTheDocument();
  342. expect(
  343. screen.getByRole('menuitemradio', {name: 'Duplicate Query'})
  344. ).toBeInTheDocument();
  345. expect(screen.getByRole('menuitemradio', {name: 'Delete Query'})).toBeInTheDocument();
  346. });
  347. it('only renders Delete Query and Duplicate Query in context menu', async function () {
  348. render(
  349. <QueryList
  350. savedQuerySearchQuery=""
  351. router={RouterFixture()}
  352. organization={organization}
  353. savedQueries={savedQueries.slice(1)}
  354. pageLinks=""
  355. renderPrebuilt={false}
  356. onQueryChange={queryChangeMock}
  357. location={location}
  358. />
  359. );
  360. const card = screen.getAllByTestId(/card-*/).at(0)!;
  361. const withinCard = within(card!);
  362. await userEvent.click(withinCard.getByTestId('menu-trigger'));
  363. expect(
  364. screen.queryByRole('menuitemradio', {name: 'Add to Dashboard'})
  365. ).not.toBeInTheDocument();
  366. expect(
  367. screen.getByRole('menuitemradio', {name: 'Set as Default'})
  368. ).toBeInTheDocument();
  369. expect(
  370. screen.getByRole('menuitemradio', {name: 'Duplicate Query'})
  371. ).toBeInTheDocument();
  372. expect(screen.getByRole('menuitemradio', {name: 'Delete Query'})).toBeInTheDocument();
  373. });
  374. it('passes yAxis from the savedQuery to MiniGraph', async function () {
  375. const featuredOrganization = OrganizationFixture({
  376. features: ['dashboards-edit'],
  377. });
  378. const yAxis = ['count()', 'failure_count()'];
  379. const savedQueryWithMultiYAxis = {
  380. ...savedQueries.slice(1)[0],
  381. yAxis,
  382. };
  383. render(
  384. <QueryList
  385. savedQuerySearchQuery=""
  386. router={RouterFixture()}
  387. organization={featuredOrganization}
  388. savedQueries={[savedQueryWithMultiYAxis]}
  389. pageLinks=""
  390. renderPrebuilt={false}
  391. onQueryChange={queryChangeMock}
  392. location={location}
  393. />
  394. );
  395. const chart = await screen.findByTestId('area-chart');
  396. expect(chart).toBeInTheDocument();
  397. expect(eventsStatsMock).toHaveBeenCalledWith(
  398. '/organizations/org-slug/events-stats/',
  399. expect.objectContaining({
  400. query: expect.objectContaining({
  401. yAxis: ['count()', 'failure_count()'],
  402. }),
  403. })
  404. );
  405. });
  406. it('Set as Default updates the homepage query', async function () {
  407. render(
  408. <QueryList
  409. savedQuerySearchQuery=""
  410. router={RouterFixture()}
  411. organization={organization}
  412. savedQueries={savedQueries.slice(1)}
  413. renderPrebuilt={false}
  414. pageLinks=""
  415. onQueryChange={queryChangeMock}
  416. location={location}
  417. />
  418. );
  419. await userEvent.click(screen.getByTestId('menu-trigger'));
  420. await userEvent.click(screen.getByText('Set as Default'));
  421. expect(updateHomepageMock).toHaveBeenCalledWith(
  422. '/organizations/org-slug/discover/homepage/',
  423. expect.objectContaining({
  424. data: expect.objectContaining({fields: ['test'], range: '14d'}),
  425. })
  426. );
  427. });
  428. describe('Add to Dashboard modal', () => {
  429. it('opens a modal with the correct params for Top 5 chart', async function () {
  430. const featuredOrganization = OrganizationFixture({
  431. features: ['dashboards-edit'],
  432. });
  433. render(
  434. <QueryList
  435. savedQuerySearchQuery=""
  436. router={RouterFixture()}
  437. organization={featuredOrganization}
  438. renderPrebuilt={false}
  439. savedQueries={[
  440. DiscoverSavedQueryFixture({
  441. display: DisplayModes.TOP5,
  442. orderby: 'test',
  443. fields: ['test', 'count()'],
  444. yAxis: ['count()'],
  445. }),
  446. ]}
  447. pageLinks=""
  448. onQueryChange={queryChangeMock}
  449. location={location}
  450. />
  451. );
  452. const contextMenu = await screen.findByTestId('menu-trigger');
  453. expect(contextMenu).toBeInTheDocument();
  454. expect(screen.queryByTestId('add-to-dashboard')).not.toBeInTheDocument();
  455. await userEvent.click(contextMenu);
  456. const addToDashboardMenuItem = await screen.findByTestId('add-to-dashboard');
  457. await userEvent.click(addToDashboardMenuItem);
  458. await waitFor(() => {
  459. expect(openAddToDashboardModal).toHaveBeenCalledWith(
  460. expect.objectContaining({
  461. widget: {
  462. title: 'Saved query #1',
  463. displayType: DisplayType.AREA,
  464. limit: 5,
  465. queries: [
  466. {
  467. aggregates: ['count()'],
  468. columns: ['test'],
  469. conditions: '',
  470. fields: ['test', 'count()', 'count()'],
  471. name: '',
  472. orderby: 'test',
  473. },
  474. ],
  475. },
  476. widgetAsQueryParams: expect.objectContaining({
  477. defaultTableColumns: ['test', 'count()'],
  478. defaultTitle: 'Saved query #1',
  479. defaultWidgetQuery:
  480. 'name=&aggregates=count()&columns=test&fields=test%2Ccount()%2Ccount()&conditions=&orderby=test',
  481. displayType: DisplayType.AREA,
  482. source: DashboardWidgetSource.DISCOVERV2,
  483. }),
  484. })
  485. );
  486. });
  487. });
  488. it('opens a modal with the correct params for other chart', async function () {
  489. const featuredOrganization = OrganizationFixture({
  490. features: ['dashboards-edit'],
  491. });
  492. render(
  493. <QueryList
  494. savedQuerySearchQuery=""
  495. router={RouterFixture()}
  496. renderPrebuilt={false}
  497. organization={featuredOrganization}
  498. savedQueries={[
  499. DiscoverSavedQueryFixture({
  500. display: DisplayModes.DEFAULT,
  501. orderby: 'count()',
  502. fields: ['test', 'count()'],
  503. yAxis: ['count()'],
  504. queryDataset: SavedQueryDatasets.TRANSACTIONS,
  505. }),
  506. ]}
  507. pageLinks=""
  508. onQueryChange={queryChangeMock}
  509. location={location}
  510. />
  511. );
  512. const contextMenu = await screen.findByTestId('menu-trigger');
  513. expect(contextMenu).toBeInTheDocument();
  514. expect(screen.queryByTestId('add-to-dashboard')).not.toBeInTheDocument();
  515. await userEvent.click(contextMenu);
  516. const addToDashboardMenuItem = await screen.findByTestId('add-to-dashboard');
  517. await userEvent.click(addToDashboardMenuItem);
  518. await waitFor(() => {
  519. expect(openAddToDashboardModal).toHaveBeenCalledWith(
  520. expect.objectContaining({
  521. widget: {
  522. title: 'Saved query #1',
  523. displayType: DisplayType.LINE,
  524. queries: [
  525. {
  526. aggregates: ['count()'],
  527. columns: [],
  528. conditions: '',
  529. fields: ['count()'],
  530. name: '',
  531. // Orderby gets dropped because ordering only applies to
  532. // Top-N and tables
  533. orderby: '',
  534. },
  535. ],
  536. },
  537. widgetAsQueryParams: expect.objectContaining({
  538. defaultTableColumns: ['test', 'count()'],
  539. defaultTitle: 'Saved query #1',
  540. defaultWidgetQuery:
  541. 'name=&aggregates=count()&columns=&fields=count()&conditions=&orderby=',
  542. displayType: DisplayType.LINE,
  543. source: DashboardWidgetSource.DISCOVERV2,
  544. }),
  545. })
  546. );
  547. });
  548. });
  549. });
  550. it('passes dataset to open modal', async function () {
  551. const featuredOrganization = OrganizationFixture({
  552. features: ['dashboards-edit', 'performance-discover-dataset-selector'],
  553. });
  554. render(
  555. <QueryList
  556. savedQuerySearchQuery=""
  557. router={RouterFixture()}
  558. renderPrebuilt={false}
  559. organization={featuredOrganization}
  560. savedQueries={[
  561. DiscoverSavedQueryFixture({
  562. display: DisplayModes.DEFAULT,
  563. orderby: 'count()',
  564. fields: ['test', 'count()'],
  565. yAxis: ['count()'],
  566. queryDataset: SavedQueryDatasets.TRANSACTIONS,
  567. }),
  568. ]}
  569. pageLinks=""
  570. onQueryChange={queryChangeMock}
  571. location={location}
  572. />
  573. );
  574. const contextMenu = await screen.findByTestId('menu-trigger');
  575. expect(contextMenu).toBeInTheDocument();
  576. expect(screen.queryByTestId('add-to-dashboard')).not.toBeInTheDocument();
  577. await userEvent.click(contextMenu);
  578. const addToDashboardMenuItem = await screen.findByTestId('add-to-dashboard');
  579. await userEvent.click(addToDashboardMenuItem);
  580. await waitFor(() => {
  581. expect(openAddToDashboardModal).toHaveBeenCalledWith(
  582. expect.objectContaining({
  583. widget: {
  584. displayType: 'line',
  585. interval: undefined,
  586. limit: undefined,
  587. queries: [
  588. {
  589. aggregates: ['count()'],
  590. columns: [],
  591. conditions: '',
  592. fields: ['count()'],
  593. name: '',
  594. orderby: '',
  595. },
  596. ],
  597. title: 'Saved query #1',
  598. widgetType: 'transaction-like',
  599. },
  600. widgetAsQueryParams: expect.objectContaining({
  601. cursor: '0:1:1',
  602. dataset: 'transaction-like',
  603. defaultTableColumns: ['test', 'count()'],
  604. defaultTitle: 'Saved query #1',
  605. defaultWidgetQuery:
  606. 'name=&aggregates=count()&columns=&fields=count()&conditions=&orderby=',
  607. displayType: 'line',
  608. source: 'discoverv2',
  609. statsPeriod: '14d',
  610. }),
  611. })
  612. );
  613. });
  614. });
  615. });