queryList.spec.tsx 20 KB

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