widgetBuilderSortBy.spec.tsx 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984
  1. import selectEvent from 'react-select-event';
  2. import {urlEncode} from '@sentry/utils';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  5. import TagStore from 'sentry/stores/tagStore';
  6. import {
  7. DashboardDetails,
  8. DashboardWidgetSource,
  9. DisplayType,
  10. Widget,
  11. } from 'sentry/views/dashboardsV2/types';
  12. import WidgetBuilder, {WidgetBuilderProps} from 'sentry/views/dashboardsV2/widgetBuilder';
  13. const defaultOrgFeatures = [
  14. 'performance-view',
  15. 'dashboards-edit',
  16. 'global-views',
  17. 'dashboards-mep',
  18. ];
  19. // Mocking worldMapChart to avoid act warnings
  20. jest.mock('sentry/components/charts/worldMapChart');
  21. function mockDashboard(dashboard: Partial<DashboardDetails>): DashboardDetails {
  22. return {
  23. id: '1',
  24. title: 'Dashboard',
  25. createdBy: undefined,
  26. dateCreated: '2020-01-01T00:00:00.000Z',
  27. widgets: [],
  28. projects: [],
  29. filters: {},
  30. ...dashboard,
  31. };
  32. }
  33. function renderTestComponent({
  34. dashboard,
  35. query,
  36. orgFeatures,
  37. onSave,
  38. params,
  39. }: {
  40. dashboard?: WidgetBuilderProps['dashboard'];
  41. onSave?: WidgetBuilderProps['onSave'];
  42. orgFeatures?: string[];
  43. params?: Partial<WidgetBuilderProps['params']>;
  44. query?: Record<string, any>;
  45. } = {}) {
  46. const {organization, router, routerContext} = initializeOrg({
  47. ...initializeOrg(),
  48. organization: {
  49. features: orgFeatures ?? defaultOrgFeatures,
  50. },
  51. router: {
  52. location: {
  53. query: {
  54. source: DashboardWidgetSource.DASHBOARDS,
  55. ...query,
  56. },
  57. },
  58. },
  59. });
  60. render(
  61. <WidgetBuilder
  62. route={{}}
  63. router={router}
  64. routes={router.routes}
  65. routeParams={router.params}
  66. location={router.location}
  67. dashboard={{
  68. id: 'new',
  69. title: 'Dashboard',
  70. createdBy: undefined,
  71. dateCreated: '2020-01-01T00:00:00.000Z',
  72. widgets: [],
  73. projects: [],
  74. filters: {},
  75. ...dashboard,
  76. }}
  77. onSave={onSave ?? jest.fn()}
  78. params={{
  79. orgId: organization.slug,
  80. dashboardId: dashboard?.id ?? 'new',
  81. ...params,
  82. }}
  83. />,
  84. {
  85. context: routerContext,
  86. organization,
  87. }
  88. );
  89. return {router};
  90. }
  91. describe('WidgetBuilder', function () {
  92. const untitledDashboard: DashboardDetails = {
  93. id: '1',
  94. title: 'Untitled Dashboard',
  95. createdBy: undefined,
  96. dateCreated: '2020-01-01T00:00:00.000Z',
  97. widgets: [],
  98. projects: [],
  99. filters: {},
  100. };
  101. const testDashboard: DashboardDetails = {
  102. id: '2',
  103. title: 'Test Dashboard',
  104. createdBy: undefined,
  105. dateCreated: '2020-01-01T00:00:00.000Z',
  106. widgets: [],
  107. projects: [],
  108. filters: {},
  109. };
  110. let eventsStatsMock: jest.Mock | undefined;
  111. let eventsv2Mock: jest.Mock | undefined;
  112. let eventsMock: jest.Mock | undefined;
  113. beforeEach(function () {
  114. MockApiClient.addMockResponse({
  115. url: '/organizations/org-slug/dashboards/',
  116. body: [
  117. {...untitledDashboard, widgetDisplay: [DisplayType.TABLE]},
  118. {...testDashboard, widgetDisplay: [DisplayType.AREA]},
  119. ],
  120. });
  121. MockApiClient.addMockResponse({
  122. url: '/organizations/org-slug/dashboards/widgets/',
  123. method: 'POST',
  124. statusCode: 200,
  125. body: [],
  126. });
  127. eventsv2Mock = MockApiClient.addMockResponse({
  128. url: '/organizations/org-slug/eventsv2/',
  129. method: 'GET',
  130. statusCode: 200,
  131. body: {
  132. meta: {},
  133. data: [],
  134. },
  135. });
  136. eventsMock = MockApiClient.addMockResponse({
  137. url: '/organizations/org-slug/events/',
  138. method: 'GET',
  139. statusCode: 200,
  140. body: {
  141. meta: {fields: {}},
  142. data: [],
  143. },
  144. });
  145. MockApiClient.addMockResponse({
  146. url: '/organizations/org-slug/projects/',
  147. method: 'GET',
  148. body: [],
  149. });
  150. MockApiClient.addMockResponse({
  151. url: '/organizations/org-slug/recent-searches/',
  152. method: 'GET',
  153. body: [],
  154. });
  155. MockApiClient.addMockResponse({
  156. url: '/organizations/org-slug/recent-searches/',
  157. method: 'POST',
  158. body: [],
  159. });
  160. MockApiClient.addMockResponse({
  161. url: '/organizations/org-slug/issues/',
  162. method: 'GET',
  163. body: [],
  164. });
  165. eventsStatsMock = MockApiClient.addMockResponse({
  166. url: '/organizations/org-slug/events-stats/',
  167. body: [],
  168. });
  169. MockApiClient.addMockResponse({
  170. url: '/organizations/org-slug/tags/event.type/values/',
  171. body: [{count: 2, name: 'Nvidia 1080ti'}],
  172. });
  173. MockApiClient.addMockResponse({
  174. url: '/organizations/org-slug/events-geo/',
  175. body: {data: [], meta: {}},
  176. });
  177. MockApiClient.addMockResponse({
  178. url: '/organizations/org-slug/users/',
  179. body: [],
  180. });
  181. MockApiClient.addMockResponse({
  182. method: 'GET',
  183. url: '/organizations/org-slug/sessions/',
  184. body: TestStubs.SessionsField({
  185. field: `sum(session)`,
  186. }),
  187. });
  188. MockApiClient.addMockResponse({
  189. method: 'GET',
  190. url: '/organizations/org-slug/metrics/data/',
  191. body: TestStubs.MetricsField({
  192. field: 'sum(sentry.sessions.session)',
  193. }),
  194. });
  195. MockApiClient.addMockResponse({
  196. url: '/organizations/org-slug/tags/',
  197. method: 'GET',
  198. body: TestStubs.Tags(),
  199. });
  200. MockApiClient.addMockResponse({
  201. url: '/organizations/org-slug/measurements-meta/',
  202. method: 'GET',
  203. body: {},
  204. });
  205. MockApiClient.addMockResponse({
  206. url: '/organizations/org-slug/tags/is/values/',
  207. method: 'GET',
  208. body: [],
  209. });
  210. TagStore.reset();
  211. });
  212. afterEach(function () {
  213. MockApiClient.clearMockResponses();
  214. jest.clearAllMocks();
  215. jest.useRealTimers();
  216. });
  217. describe('with eventsv2 > Sort by selectors', function () {
  218. it('renders', async function () {
  219. renderTestComponent();
  220. expect(await screen.findByText('Sort by a column')).toBeInTheDocument();
  221. expect(
  222. screen.getByText("Choose one of the columns you've created to sort by.")
  223. ).toBeInTheDocument();
  224. // Selector "sortDirection"
  225. expect(screen.getByText('High to low')).toBeInTheDocument();
  226. // Selector "sortBy"
  227. expect(screen.getAllByText('count()')).toHaveLength(3);
  228. });
  229. it('ordering by column uses field form when selecting orderby', async function () {
  230. const widget: Widget = {
  231. id: '1',
  232. title: 'Test Widget',
  233. interval: '5m',
  234. displayType: DisplayType.TABLE,
  235. queries: [
  236. {
  237. name: 'errors',
  238. conditions: 'event.type:error',
  239. fields: ['count()'],
  240. aggregates: ['count()'],
  241. columns: ['project'],
  242. orderby: '-project',
  243. },
  244. ],
  245. };
  246. const dashboard = mockDashboard({widgets: [widget]});
  247. renderTestComponent({
  248. dashboard,
  249. params: {
  250. widgetIndex: '0',
  251. },
  252. });
  253. await waitFor(async () => {
  254. expect(await screen.findAllByText('project')).toHaveLength(3);
  255. });
  256. await selectEvent.select(screen.getAllByText('project')[2], 'count()');
  257. await waitFor(() => {
  258. expect(eventsv2Mock).toHaveBeenCalledWith(
  259. '/organizations/org-slug/eventsv2/',
  260. expect.objectContaining({
  261. query: expect.objectContaining({
  262. sort: ['-count()'],
  263. }),
  264. })
  265. );
  266. });
  267. });
  268. it('sortBy defaults to the first field value when changing display type to table', async function () {
  269. const widget: Widget = {
  270. id: '1',
  271. title: 'Errors over time',
  272. interval: '5m',
  273. displayType: DisplayType.LINE,
  274. queries: [
  275. {
  276. name: 'errors',
  277. conditions: 'event.type:error',
  278. fields: ['count()', 'count_unique(id)'],
  279. aggregates: ['count()', 'count_unique(id)'],
  280. columns: [],
  281. orderby: '',
  282. },
  283. {
  284. name: 'csp',
  285. conditions: 'event.type:csp',
  286. fields: ['count()', 'count_unique(id)'],
  287. aggregates: ['count()', 'count_unique(id)'],
  288. columns: [],
  289. orderby: '',
  290. },
  291. ],
  292. };
  293. const dashboard = mockDashboard({widgets: [widget]});
  294. renderTestComponent({
  295. dashboard,
  296. params: {
  297. widgetIndex: '0',
  298. },
  299. });
  300. // Click on the displayType selector
  301. userEvent.click(await screen.findByText('Line Chart'));
  302. // Choose the table visualization
  303. userEvent.click(screen.getByText('Table'));
  304. expect(await screen.findByText('Sort by a column')).toBeInTheDocument();
  305. // Selector "sortDirection"
  306. expect(screen.getByText('High to low')).toBeInTheDocument();
  307. // Selector "sortBy"
  308. expect(screen.getAllByText('count()')).toHaveLength(3);
  309. });
  310. it('can update selectors values', async function () {
  311. const handleSave = jest.fn();
  312. const widget: Widget = {
  313. id: '1',
  314. title: 'Errors over time',
  315. interval: '5m',
  316. displayType: DisplayType.TABLE,
  317. queries: [
  318. {
  319. name: '',
  320. conditions: '',
  321. fields: ['count()', 'count_unique(id)'],
  322. aggregates: ['count()', 'count_unique(id)'],
  323. columns: [],
  324. orderby: '-count()',
  325. },
  326. ],
  327. };
  328. const dashboard = mockDashboard({widgets: [widget]});
  329. renderTestComponent({
  330. dashboard,
  331. onSave: handleSave,
  332. params: {
  333. widgetIndex: '0',
  334. },
  335. });
  336. expect(await screen.findByText('Sort by a column')).toBeInTheDocument();
  337. // Selector "sortDirection"
  338. expect(screen.getByText('High to low')).toBeInTheDocument();
  339. // Selector "sortBy"
  340. expect(screen.getAllByText('count()')).toHaveLength(3);
  341. await selectEvent.select(screen.getAllByText('count()')[2], 'count_unique(id)');
  342. // Wait for the Builder update the widget values
  343. await waitFor(() => {
  344. expect(screen.getAllByText('count()')).toHaveLength(2);
  345. });
  346. // Now count_unique(id) is selected in the "sortBy" selector
  347. expect(screen.getAllByText('count_unique(id)')).toHaveLength(2);
  348. await selectEvent.select(screen.getByText('High to low'), 'Low to high');
  349. // Saves the widget
  350. userEvent.click(screen.getByText('Update Widget'));
  351. await waitFor(() => {
  352. expect(handleSave).toHaveBeenCalledWith([
  353. expect.objectContaining({
  354. queries: [expect.objectContaining({orderby: 'count_unique(id)'})],
  355. }),
  356. ]);
  357. });
  358. });
  359. it('sortBy defaults to the first field value when coming from discover', async function () {
  360. const defaultWidgetQuery = {
  361. name: '',
  362. fields: ['title', 'count()', 'count_unique(user)', 'epm()', 'count()'],
  363. columns: ['title'],
  364. aggregates: ['count()', 'count_unique(user)', 'epm()', 'count()'],
  365. conditions: 'tag:value',
  366. orderby: '',
  367. };
  368. const {router} = renderTestComponent({
  369. query: {
  370. source: DashboardWidgetSource.DISCOVERV2,
  371. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  372. displayType: DisplayType.TABLE,
  373. defaultTableColumns: ['title', 'count()', 'count_unique(user)', 'epm()'],
  374. },
  375. });
  376. expect(await screen.findByText('Sort by a column')).toBeInTheDocument();
  377. // Selector "sortDirection"
  378. expect(screen.getByText('Low to high')).toBeInTheDocument();
  379. // Selector "sortBy"
  380. expect(screen.getAllByText('title')).toHaveLength(2);
  381. // Saves the widget
  382. userEvent.click(screen.getByText('Add Widget'));
  383. await waitFor(() => {
  384. expect(router.push).toHaveBeenCalledWith(
  385. expect.objectContaining({
  386. query: expect.objectContaining({queryOrderby: 'count()'}),
  387. })
  388. );
  389. });
  390. });
  391. it('sortBy is only visible on tabular visualizations or when there is a groupBy value selected on time-series visualizations', async function () {
  392. renderTestComponent();
  393. // Sort by shall be visible on table visualization
  394. expect(await screen.findByText('Sort by a column')).toBeInTheDocument();
  395. // Update visualization to be a time-series
  396. userEvent.click(screen.getByText('Table'));
  397. userEvent.click(screen.getByText('Line Chart'));
  398. // Time-series visualizations display GroupBy step
  399. expect(await screen.findByText('Group your results')).toBeInTheDocument();
  400. // Do not show sortBy when empty columns (groupBys) are added
  401. userEvent.click(screen.getByText('Add Group'));
  402. expect(screen.getAllByText('Select group')).toHaveLength(2);
  403. // SortBy step shall not be visible
  404. expect(screen.queryByText('Sort by a y-axis')).not.toBeInTheDocument();
  405. // Select GroupBy value
  406. await selectEvent.select(screen.getAllByText('Select group')[0], 'project');
  407. // Now that at least one groupBy value is selected, the SortBy step shall be visible
  408. expect(screen.getByText('Sort by a y-axis')).toBeInTheDocument();
  409. // Remove selected GroupBy value
  410. userEvent.click(screen.getAllByLabelText('Remove group')[0]);
  411. // SortBy step shall no longer be visible
  412. expect(screen.queryByText('Sort by a y-axis')).not.toBeInTheDocument();
  413. });
  414. it('allows for sorting by a custom equation', async function () {
  415. renderTestComponent({
  416. query: {
  417. source: DashboardWidgetSource.DASHBOARDS,
  418. displayType: DisplayType.LINE,
  419. },
  420. });
  421. await selectEvent.select(await screen.findByText('Select group'), 'project');
  422. expect(screen.getAllByText('count()')).toHaveLength(2);
  423. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  424. userEvent.paste(
  425. screen.getByPlaceholderText('Enter Equation'),
  426. 'count_unique(user) * 2'
  427. );
  428. userEvent.keyboard('{enter}');
  429. await waitFor(() => {
  430. expect(eventsStatsMock).toHaveBeenCalledWith(
  431. '/organizations/org-slug/events-stats/',
  432. expect.objectContaining({
  433. query: expect.objectContaining({
  434. field: expect.arrayContaining(['equation|count_unique(user) * 2']),
  435. orderby: '-equation[0]',
  436. }),
  437. })
  438. );
  439. });
  440. }, 10000);
  441. it('persists the state when toggling between sorting options', async function () {
  442. renderTestComponent({
  443. query: {
  444. source: DashboardWidgetSource.DASHBOARDS,
  445. displayType: DisplayType.LINE,
  446. },
  447. });
  448. await selectEvent.select(await screen.findByText('Select group'), 'project');
  449. expect(screen.getAllByText('count()')).toHaveLength(2);
  450. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  451. userEvent.paste(
  452. screen.getByPlaceholderText('Enter Equation'),
  453. 'count_unique(user) * 2'
  454. );
  455. userEvent.keyboard('{enter}');
  456. // Switch away from the Custom Equation
  457. expect(screen.getByText('project')).toBeInTheDocument();
  458. await selectEvent.select(screen.getByText('Custom Equation'), 'project');
  459. expect(screen.getAllByText('project')).toHaveLength(2);
  460. // Switch back, the equation should still be visible
  461. await selectEvent.select(screen.getAllByText('project')[1], 'Custom Equation');
  462. expect(screen.getByPlaceholderText('Enter Equation')).toHaveValue(
  463. 'count_unique(user) * 2'
  464. );
  465. });
  466. it('persists the state when updating y-axes', async function () {
  467. renderTestComponent({
  468. query: {
  469. source: DashboardWidgetSource.DASHBOARDS,
  470. displayType: DisplayType.LINE,
  471. },
  472. });
  473. await selectEvent.select(await screen.findByText('Select group'), 'project');
  474. expect(screen.getAllByText('count()')).toHaveLength(2);
  475. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  476. userEvent.paste(
  477. screen.getByPlaceholderText('Enter Equation'),
  478. 'count_unique(user) * 2'
  479. );
  480. userEvent.keyboard('{enter}');
  481. // Add a y-axis
  482. userEvent.click(screen.getByText('Add Overlay'));
  483. // The equation should still be visible
  484. expect(screen.getByPlaceholderText('Enter Equation')).toHaveValue(
  485. 'count_unique(user) * 2'
  486. );
  487. });
  488. it('displays the custom equation if the widget has it saved', async function () {
  489. const widget: Widget = {
  490. id: '1',
  491. title: 'Test Widget',
  492. interval: '5m',
  493. displayType: DisplayType.LINE,
  494. queries: [
  495. {
  496. name: '',
  497. conditions: '',
  498. fields: ['count()', 'project'],
  499. aggregates: ['count()'],
  500. columns: ['project'],
  501. orderby: '-equation|count_unique(user) * 2',
  502. },
  503. ],
  504. };
  505. const dashboard = mockDashboard({widgets: [widget]});
  506. renderTestComponent({
  507. query: {
  508. source: DashboardWidgetSource.DASHBOARDS,
  509. displayType: DisplayType.LINE,
  510. },
  511. params: {
  512. widgetIndex: '0',
  513. },
  514. dashboard,
  515. });
  516. expect(await screen.findByPlaceholderText('Enter Equation')).toHaveValue(
  517. 'count_unique(user) * 2'
  518. );
  519. });
  520. it('displays Operators in the input dropdown', async function () {
  521. renderTestComponent({
  522. query: {
  523. source: DashboardWidgetSource.DASHBOARDS,
  524. displayType: DisplayType.LINE,
  525. },
  526. });
  527. await selectEvent.select(await screen.findByText('Select group'), 'project');
  528. expect(screen.getAllByText('count()')).toHaveLength(2);
  529. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  530. selectEvent.openMenu(screen.getByPlaceholderText('Enter Equation'));
  531. expect(screen.getByText('Operators')).toBeInTheDocument();
  532. expect(screen.queryByText('Fields')).not.toBeInTheDocument();
  533. });
  534. it('hides Custom Equation input and resets orderby when switching to table', async function () {
  535. renderTestComponent({
  536. query: {
  537. source: DashboardWidgetSource.DASHBOARDS,
  538. displayType: DisplayType.LINE,
  539. },
  540. });
  541. await selectEvent.select(await screen.findByText('Select group'), 'project');
  542. expect(screen.getAllByText('count()')).toHaveLength(2);
  543. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  544. userEvent.paste(
  545. screen.getByPlaceholderText('Enter Equation'),
  546. 'count_unique(user) * 2'
  547. );
  548. userEvent.keyboard('{enter}');
  549. // Switch the display type to Table
  550. userEvent.click(screen.getByText('Line Chart'));
  551. userEvent.click(screen.getByText('Table'));
  552. expect(screen.getAllByText('count()')).toHaveLength(2);
  553. expect(screen.queryByPlaceholderText('Enter Equation')).not.toBeInTheDocument();
  554. await waitFor(() => {
  555. expect(eventsv2Mock).toHaveBeenCalledWith(
  556. '/organizations/org-slug/eventsv2/',
  557. expect.objectContaining({
  558. query: expect.objectContaining({
  559. sort: ['-count()'],
  560. }),
  561. })
  562. );
  563. });
  564. });
  565. it('does not show the Custom Equation input if the only y-axis left is an empty equation', async function () {
  566. renderTestComponent({
  567. query: {
  568. source: DashboardWidgetSource.DASHBOARDS,
  569. displayType: DisplayType.LINE,
  570. },
  571. });
  572. await selectEvent.select(await screen.findByText('Select group'), 'project');
  573. userEvent.click(screen.getByText('Add an Equation'));
  574. userEvent.click(screen.getAllByLabelText('Remove this Y-Axis')[0]);
  575. expect(screen.queryByPlaceholderText('Enter Equation')).not.toBeInTheDocument();
  576. });
  577. it('persists a sort by a grouping when changing y-axes', async function () {
  578. renderTestComponent({
  579. query: {
  580. source: DashboardWidgetSource.DASHBOARDS,
  581. displayType: DisplayType.LINE,
  582. },
  583. });
  584. await selectEvent.select(await screen.findByText('Select group'), 'project');
  585. expect(screen.getAllByText('count()')).toHaveLength(2);
  586. // Change the sort option to a grouping field, and then change a y-axis
  587. await selectEvent.select(screen.getAllByText('count()')[1], 'project');
  588. await selectEvent.select(screen.getAllByText('count()')[0], /count_unique/);
  589. // project should appear in the group by field, as well as the sort field
  590. expect(screen.getAllByText('project')).toHaveLength(2);
  591. });
  592. it('persists sort by a y-axis when grouping changes', async function () {
  593. renderTestComponent({
  594. query: {
  595. source: DashboardWidgetSource.DASHBOARDS,
  596. displayType: DisplayType.LINE,
  597. },
  598. });
  599. userEvent.click(await screen.findByText('Add Overlay'));
  600. await selectEvent.select(screen.getByText('Select group'), 'project');
  601. // Change the sort by to count_unique
  602. await selectEvent.select(screen.getAllByText('count()')[1], /count_unique/);
  603. // Change the grouping
  604. await selectEvent.select(screen.getByText('project'), 'environment');
  605. // count_unique(user) should still be the sorting field
  606. expect(screen.getByText(/count_unique/)).toBeInTheDocument();
  607. expect(screen.getByText('user')).toBeInTheDocument();
  608. });
  609. it('does not remove the Custom Equation field if a grouping is updated', async function () {
  610. renderTestComponent({
  611. query: {
  612. source: DashboardWidgetSource.DASHBOARDS,
  613. displayType: DisplayType.LINE,
  614. },
  615. });
  616. await selectEvent.select(await screen.findByText('Select group'), 'project');
  617. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  618. userEvent.paste(
  619. screen.getByPlaceholderText('Enter Equation'),
  620. 'count_unique(user) * 2'
  621. );
  622. userEvent.keyboard('{enter}');
  623. userEvent.click(screen.getByText('Add Group'));
  624. expect(screen.getByPlaceholderText('Enter Equation')).toHaveValue(
  625. 'count_unique(user) * 2'
  626. );
  627. });
  628. it.each`
  629. directionPrefix | expectedOrderSelection | displayType
  630. ${'-'} | ${'High to low'} | ${DisplayType.TABLE}
  631. ${''} | ${'Low to high'} | ${DisplayType.TABLE}
  632. ${'-'} | ${'High to low'} | ${DisplayType.LINE}
  633. ${''} | ${'Low to high'} | ${DisplayType.LINE}
  634. `(
  635. `opens a widget with the '$expectedOrderSelection' sort order when the widget was saved with that direction`,
  636. async function ({directionPrefix, expectedOrderSelection}) {
  637. const widget: Widget = {
  638. id: '1',
  639. title: 'Test Widget',
  640. interval: '5m',
  641. displayType: DisplayType.LINE,
  642. queries: [
  643. {
  644. name: '',
  645. conditions: '',
  646. fields: ['count_unique(user)'],
  647. aggregates: ['count_unique(user)'],
  648. columns: ['project'],
  649. orderby: `${directionPrefix}count_unique(user)`,
  650. },
  651. ],
  652. };
  653. const dashboard = mockDashboard({widgets: [widget]});
  654. renderTestComponent({
  655. dashboard,
  656. params: {
  657. widgetIndex: '0',
  658. },
  659. });
  660. await screen.findByText(expectedOrderSelection);
  661. }
  662. );
  663. it('saved widget with aggregate alias as orderby should persist alias when y-axes change', async function () {
  664. const widget: Widget = {
  665. id: '1',
  666. title: 'Test Widget',
  667. interval: '5m',
  668. displayType: DisplayType.TABLE,
  669. queries: [
  670. {
  671. name: '',
  672. conditions: '',
  673. fields: ['project', 'count_unique(user)'],
  674. aggregates: ['count_unique(user)'],
  675. columns: ['project'],
  676. orderby: 'count_unique(user)',
  677. },
  678. ],
  679. };
  680. const dashboard = mockDashboard({widgets: [widget]});
  681. renderTestComponent({
  682. dashboard,
  683. params: {
  684. widgetIndex: '0',
  685. },
  686. });
  687. await screen.findByText('Sort by a column');
  688. // Assert for length 2 since one in the table header and one in sort by
  689. expect(screen.getAllByText('count_unique(user)')).toHaveLength(2);
  690. userEvent.click(screen.getByText('Add a Column'));
  691. // The sort by should still have count_unique(user)
  692. await waitFor(() =>
  693. expect(screen.getAllByText('count_unique(user)')).toHaveLength(2)
  694. );
  695. });
  696. it('will reset the sort field when going from line to table when sorting by a value not in fields', async function () {
  697. renderTestComponent({
  698. query: {
  699. displayType: DisplayType.LINE,
  700. },
  701. });
  702. await selectEvent.select(await screen.findByText('Select group'), 'project');
  703. expect(screen.getAllByText('count()')).toHaveLength(2);
  704. await selectEvent.select(screen.getAllByText('count()')[1], /count_unique/);
  705. userEvent.click(screen.getByText('Line Chart'));
  706. userEvent.click(screen.getByText('Table'));
  707. // 1 for table header, 1 for column selection, and 1 for sorting
  708. await waitFor(() => {
  709. expect(screen.getAllByText('count()')).toHaveLength(3);
  710. });
  711. });
  712. it('equations in y-axis appear in sort by field for grouped timeseries', async function () {
  713. renderTestComponent({
  714. query: {
  715. displayType: DisplayType.LINE,
  716. },
  717. });
  718. userEvent.click(await screen.findByText('Add an Equation'));
  719. userEvent.paste(screen.getByPlaceholderText('Equation'), 'count() * 100');
  720. userEvent.keyboard('{enter}');
  721. await selectEvent.select(screen.getByText('Select group'), 'project');
  722. expect(screen.getAllByText('count()')).toHaveLength(2);
  723. await selectEvent.select(screen.getAllByText('count()')[1], 'count() * 100');
  724. });
  725. it('does not reset the orderby when ordered by an equation in table', async function () {
  726. const widget: Widget = {
  727. id: '1',
  728. title: 'Errors over time',
  729. interval: '5m',
  730. displayType: DisplayType.TABLE,
  731. queries: [
  732. {
  733. name: '',
  734. conditions: '',
  735. fields: [
  736. 'count()',
  737. 'count_unique(id)',
  738. 'equation|count() + count_unique(id)',
  739. ],
  740. aggregates: [
  741. 'count()',
  742. 'count_unique(id)',
  743. 'equation|count() + count_unique(id)',
  744. ],
  745. columns: [],
  746. orderby: '-equation[0]',
  747. },
  748. ],
  749. };
  750. const dashboard = mockDashboard({widgets: [widget]});
  751. renderTestComponent({
  752. dashboard,
  753. params: {
  754. widgetIndex: '0',
  755. },
  756. });
  757. await screen.findByText('Sort by a column');
  758. // 1 in the column selector, 1 in the sort by field
  759. expect(screen.getAllByText('count() + count_unique(id)')).toHaveLength(2);
  760. });
  761. });
  762. describe('with events > Sort by selectors', function () {
  763. it('ordering by column uses field form when selecting orderby', async function () {
  764. const widget: Widget = {
  765. id: '1',
  766. title: 'Test Widget',
  767. interval: '5m',
  768. displayType: DisplayType.TABLE,
  769. queries: [
  770. {
  771. name: 'errors',
  772. conditions: 'event.type:error',
  773. fields: ['count()'],
  774. aggregates: ['count()'],
  775. columns: ['project'],
  776. orderby: '-project',
  777. },
  778. ],
  779. };
  780. const dashboard = mockDashboard({widgets: [widget]});
  781. renderTestComponent({
  782. orgFeatures: [...defaultOrgFeatures, 'discover-frontend-use-events-endpoint'],
  783. dashboard,
  784. params: {
  785. widgetIndex: '0',
  786. },
  787. });
  788. await waitFor(async () => {
  789. expect(await screen.findAllByText('project')).toHaveLength(3);
  790. });
  791. await selectEvent.select(screen.getAllByText('project')[2], 'count()');
  792. await waitFor(() => {
  793. expect(eventsMock).toHaveBeenCalledWith(
  794. '/organizations/org-slug/events/',
  795. expect.objectContaining({
  796. query: expect.objectContaining({
  797. sort: ['-count()'],
  798. }),
  799. })
  800. );
  801. });
  802. });
  803. it('hides Custom Equation input and resets orderby when switching to table', async function () {
  804. renderTestComponent({
  805. orgFeatures: [...defaultOrgFeatures, 'discover-frontend-use-events-endpoint'],
  806. query: {
  807. source: DashboardWidgetSource.DASHBOARDS,
  808. displayType: DisplayType.LINE,
  809. },
  810. });
  811. await selectEvent.select(await screen.findByText('Select group'), 'project');
  812. expect(screen.getAllByText('count()')).toHaveLength(2);
  813. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  814. userEvent.paste(
  815. screen.getByPlaceholderText('Enter Equation'),
  816. 'count_unique(user) * 2'
  817. );
  818. userEvent.keyboard('{enter}');
  819. // Switch the display type to Table
  820. userEvent.click(screen.getByText('Line Chart'));
  821. userEvent.click(screen.getByText('Table'));
  822. expect(screen.getAllByText('count()')).toHaveLength(2);
  823. expect(screen.queryByPlaceholderText('Enter Equation')).not.toBeInTheDocument();
  824. await waitFor(() => {
  825. expect(eventsMock).toHaveBeenCalledWith(
  826. '/organizations/org-slug/events/',
  827. expect.objectContaining({
  828. query: expect.objectContaining({
  829. sort: ['-count()'],
  830. }),
  831. })
  832. );
  833. });
  834. });
  835. });
  836. });