widgetBuilderSortBy.spec.tsx 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936
  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('sortBy defaults to the first field value when changing display type to table', async function () {
  230. const widget: Widget = {
  231. id: '1',
  232. title: 'Errors over time',
  233. interval: '5m',
  234. displayType: DisplayType.LINE,
  235. queries: [
  236. {
  237. name: 'errors',
  238. conditions: 'event.type:error',
  239. fields: ['count()', 'count_unique(id)'],
  240. aggregates: ['count()', 'count_unique(id)'],
  241. columns: [],
  242. orderby: '',
  243. },
  244. {
  245. name: 'csp',
  246. conditions: 'event.type:csp',
  247. fields: ['count()', 'count_unique(id)'],
  248. aggregates: ['count()', 'count_unique(id)'],
  249. columns: [],
  250. orderby: '',
  251. },
  252. ],
  253. };
  254. const dashboard = mockDashboard({widgets: [widget]});
  255. renderTestComponent({
  256. dashboard,
  257. params: {
  258. widgetIndex: '0',
  259. },
  260. });
  261. // Click on the displayType selector
  262. userEvent.click(await screen.findByText('Line Chart'));
  263. // Choose the table visualization
  264. userEvent.click(screen.getByText('Table'));
  265. expect(await screen.findByText('Sort by a column')).toBeInTheDocument();
  266. // Selector "sortDirection"
  267. expect(screen.getByText('High to low')).toBeInTheDocument();
  268. // Selector "sortBy"
  269. expect(screen.getAllByText('count()')).toHaveLength(3);
  270. });
  271. it('can update selectors values', async function () {
  272. const handleSave = jest.fn();
  273. const widget: Widget = {
  274. id: '1',
  275. title: 'Errors over time',
  276. interval: '5m',
  277. displayType: DisplayType.TABLE,
  278. queries: [
  279. {
  280. name: '',
  281. conditions: '',
  282. fields: ['count()', 'count_unique(id)'],
  283. aggregates: ['count()', 'count_unique(id)'],
  284. columns: [],
  285. orderby: '-count()',
  286. },
  287. ],
  288. };
  289. const dashboard = mockDashboard({widgets: [widget]});
  290. renderTestComponent({
  291. dashboard,
  292. onSave: handleSave,
  293. params: {
  294. widgetIndex: '0',
  295. },
  296. });
  297. expect(await screen.findByText('Sort by a column')).toBeInTheDocument();
  298. // Selector "sortDirection"
  299. expect(screen.getByText('High to low')).toBeInTheDocument();
  300. // Selector "sortBy"
  301. expect(screen.getAllByText('count()')).toHaveLength(3);
  302. await selectEvent.select(screen.getAllByText('count()')[2], 'count_unique(id)');
  303. // Wait for the Builder update the widget values
  304. await waitFor(() => {
  305. expect(screen.getAllByText('count()')).toHaveLength(2);
  306. });
  307. // Now count_unique(id) is selected in the "sortBy" selector
  308. expect(screen.getAllByText('count_unique(id)')).toHaveLength(2);
  309. await selectEvent.select(screen.getByText('High to low'), 'Low to high');
  310. // Saves the widget
  311. userEvent.click(screen.getByText('Update Widget'));
  312. await waitFor(() => {
  313. expect(handleSave).toHaveBeenCalledWith([
  314. expect.objectContaining({
  315. queries: [expect.objectContaining({orderby: 'count_unique(id)'})],
  316. }),
  317. ]);
  318. });
  319. });
  320. it('sortBy defaults to the first field value when coming from discover', async function () {
  321. const defaultWidgetQuery = {
  322. name: '',
  323. fields: ['title', 'count()', 'count_unique(user)', 'epm()', 'count()'],
  324. columns: ['title'],
  325. aggregates: ['count()', 'count_unique(user)', 'epm()', 'count()'],
  326. conditions: 'tag:value',
  327. orderby: '',
  328. };
  329. const {router} = renderTestComponent({
  330. query: {
  331. source: DashboardWidgetSource.DISCOVERV2,
  332. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  333. displayType: DisplayType.TABLE,
  334. defaultTableColumns: ['title', 'count()', 'count_unique(user)', 'epm()'],
  335. },
  336. });
  337. expect(await screen.findByText('Sort by a column')).toBeInTheDocument();
  338. // Selector "sortDirection"
  339. expect(screen.getByText('Low to high')).toBeInTheDocument();
  340. // Selector "sortBy"
  341. expect(screen.getAllByText('title')).toHaveLength(2);
  342. // Saves the widget
  343. userEvent.click(screen.getByText('Add Widget'));
  344. await waitFor(() => {
  345. expect(router.push).toHaveBeenCalledWith(
  346. expect.objectContaining({
  347. query: expect.objectContaining({queryOrderby: 'count()'}),
  348. })
  349. );
  350. });
  351. });
  352. it('sortBy is only visible on tabular visualizations or when there is a groupBy value selected on time-series visualizations', async function () {
  353. renderTestComponent();
  354. // Sort by shall be visible on table visualization
  355. expect(await screen.findByText('Sort by a column')).toBeInTheDocument();
  356. // Update visualization to be a time-series
  357. userEvent.click(screen.getByText('Table'));
  358. userEvent.click(screen.getByText('Line Chart'));
  359. // Time-series visualizations display GroupBy step
  360. expect(await screen.findByText('Group your results')).toBeInTheDocument();
  361. // Do not show sortBy when empty columns (groupBys) are added
  362. userEvent.click(screen.getByText('Add Group'));
  363. expect(screen.getAllByText('Select group')).toHaveLength(2);
  364. // SortBy step shall not be visible
  365. expect(screen.queryByText('Sort by a y-axis')).not.toBeInTheDocument();
  366. // Select GroupBy value
  367. await selectEvent.select(screen.getAllByText('Select group')[0], 'project');
  368. // Now that at least one groupBy value is selected, the SortBy step shall be visible
  369. expect(screen.getByText('Sort by a y-axis')).toBeInTheDocument();
  370. // Remove selected GroupBy value
  371. userEvent.click(screen.getAllByLabelText('Remove group')[0]);
  372. // SortBy step shall no longer be visible
  373. expect(screen.queryByText('Sort by a y-axis')).not.toBeInTheDocument();
  374. });
  375. it('allows for sorting by a custom equation', async function () {
  376. renderTestComponent({
  377. query: {
  378. source: DashboardWidgetSource.DASHBOARDS,
  379. displayType: DisplayType.LINE,
  380. },
  381. });
  382. await selectEvent.select(await screen.findByText('Select group'), 'project');
  383. expect(screen.getAllByText('count()')).toHaveLength(2);
  384. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  385. userEvent.paste(
  386. screen.getByPlaceholderText('Enter Equation'),
  387. 'count_unique(user) * 2'
  388. );
  389. userEvent.keyboard('{enter}');
  390. await waitFor(() => {
  391. expect(eventsStatsMock).toHaveBeenCalledWith(
  392. '/organizations/org-slug/events-stats/',
  393. expect.objectContaining({
  394. query: expect.objectContaining({
  395. field: expect.arrayContaining(['equation|count_unique(user) * 2']),
  396. orderby: '-equation[0]',
  397. }),
  398. })
  399. );
  400. });
  401. }, 10000);
  402. it('persists the state when toggling between sorting options', async function () {
  403. renderTestComponent({
  404. query: {
  405. source: DashboardWidgetSource.DASHBOARDS,
  406. displayType: DisplayType.LINE,
  407. },
  408. });
  409. await selectEvent.select(await screen.findByText('Select group'), 'project');
  410. expect(screen.getAllByText('count()')).toHaveLength(2);
  411. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  412. userEvent.paste(
  413. screen.getByPlaceholderText('Enter Equation'),
  414. 'count_unique(user) * 2'
  415. );
  416. userEvent.keyboard('{enter}');
  417. // Switch away from the Custom Equation
  418. expect(screen.getByText('project')).toBeInTheDocument();
  419. await selectEvent.select(screen.getByText('Custom Equation'), 'project');
  420. expect(screen.getAllByText('project')).toHaveLength(2);
  421. // Switch back, the equation should still be visible
  422. await selectEvent.select(screen.getAllByText('project')[1], 'Custom Equation');
  423. expect(screen.getByPlaceholderText('Enter Equation')).toHaveValue(
  424. 'count_unique(user) * 2'
  425. );
  426. });
  427. it('persists the state when updating y-axes', async function () {
  428. renderTestComponent({
  429. query: {
  430. source: DashboardWidgetSource.DASHBOARDS,
  431. displayType: DisplayType.LINE,
  432. },
  433. });
  434. await selectEvent.select(await screen.findByText('Select group'), 'project');
  435. expect(screen.getAllByText('count()')).toHaveLength(2);
  436. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  437. userEvent.paste(
  438. screen.getByPlaceholderText('Enter Equation'),
  439. 'count_unique(user) * 2'
  440. );
  441. userEvent.keyboard('{enter}');
  442. // Add a y-axis
  443. userEvent.click(screen.getByText('Add Overlay'));
  444. // The equation should still be visible
  445. expect(screen.getByPlaceholderText('Enter Equation')).toHaveValue(
  446. 'count_unique(user) * 2'
  447. );
  448. });
  449. it('displays the custom equation if the widget has it saved', async function () {
  450. const widget: Widget = {
  451. id: '1',
  452. title: 'Test Widget',
  453. interval: '5m',
  454. displayType: DisplayType.LINE,
  455. queries: [
  456. {
  457. name: '',
  458. conditions: '',
  459. fields: ['count()', 'project'],
  460. aggregates: ['count()'],
  461. columns: ['project'],
  462. orderby: '-equation|count_unique(user) * 2',
  463. },
  464. ],
  465. };
  466. const dashboard = mockDashboard({widgets: [widget]});
  467. renderTestComponent({
  468. query: {
  469. source: DashboardWidgetSource.DASHBOARDS,
  470. displayType: DisplayType.LINE,
  471. },
  472. params: {
  473. widgetIndex: '0',
  474. },
  475. dashboard,
  476. });
  477. expect(await screen.findByPlaceholderText('Enter Equation')).toHaveValue(
  478. 'count_unique(user) * 2'
  479. );
  480. });
  481. it('displays Operators in the input dropdown', async function () {
  482. renderTestComponent({
  483. query: {
  484. source: DashboardWidgetSource.DASHBOARDS,
  485. displayType: DisplayType.LINE,
  486. },
  487. });
  488. await selectEvent.select(await screen.findByText('Select group'), 'project');
  489. expect(screen.getAllByText('count()')).toHaveLength(2);
  490. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  491. selectEvent.openMenu(screen.getByPlaceholderText('Enter Equation'));
  492. expect(screen.getByText('Operators')).toBeInTheDocument();
  493. expect(screen.queryByText('Fields')).not.toBeInTheDocument();
  494. });
  495. it('hides Custom Equation input and resets orderby when switching to table', async function () {
  496. renderTestComponent({
  497. query: {
  498. source: DashboardWidgetSource.DASHBOARDS,
  499. displayType: DisplayType.LINE,
  500. },
  501. });
  502. await selectEvent.select(await screen.findByText('Select group'), 'project');
  503. expect(screen.getAllByText('count()')).toHaveLength(2);
  504. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  505. userEvent.paste(
  506. screen.getByPlaceholderText('Enter Equation'),
  507. 'count_unique(user) * 2'
  508. );
  509. userEvent.keyboard('{enter}');
  510. // Switch the display type to Table
  511. userEvent.click(screen.getByText('Line Chart'));
  512. userEvent.click(screen.getByText('Table'));
  513. expect(screen.getAllByText('count()')).toHaveLength(2);
  514. expect(screen.queryByPlaceholderText('Enter Equation')).not.toBeInTheDocument();
  515. await waitFor(() => {
  516. expect(eventsv2Mock).toHaveBeenCalledWith(
  517. '/organizations/org-slug/eventsv2/',
  518. expect.objectContaining({
  519. query: expect.objectContaining({
  520. sort: ['-count()'],
  521. }),
  522. })
  523. );
  524. });
  525. });
  526. it('does not show the Custom Equation input if the only y-axis left is an empty equation', async function () {
  527. renderTestComponent({
  528. query: {
  529. source: DashboardWidgetSource.DASHBOARDS,
  530. displayType: DisplayType.LINE,
  531. },
  532. });
  533. await selectEvent.select(await screen.findByText('Select group'), 'project');
  534. userEvent.click(screen.getByText('Add an Equation'));
  535. userEvent.click(screen.getAllByLabelText('Remove this Y-Axis')[0]);
  536. expect(screen.queryByPlaceholderText('Enter Equation')).not.toBeInTheDocument();
  537. });
  538. it('persists a sort by a grouping when changing y-axes', async function () {
  539. renderTestComponent({
  540. query: {
  541. source: DashboardWidgetSource.DASHBOARDS,
  542. displayType: DisplayType.LINE,
  543. },
  544. });
  545. await selectEvent.select(await screen.findByText('Select group'), 'project');
  546. expect(screen.getAllByText('count()')).toHaveLength(2);
  547. // Change the sort option to a grouping field, and then change a y-axis
  548. await selectEvent.select(screen.getAllByText('count()')[1], 'project');
  549. await selectEvent.select(screen.getAllByText('count()')[0], /count_unique/);
  550. // project should appear in the group by field, as well as the sort field
  551. expect(screen.getAllByText('project')).toHaveLength(2);
  552. });
  553. it('persists sort by a y-axis when grouping changes', async function () {
  554. renderTestComponent({
  555. query: {
  556. source: DashboardWidgetSource.DASHBOARDS,
  557. displayType: DisplayType.LINE,
  558. },
  559. });
  560. userEvent.click(await screen.findByText('Add Overlay'));
  561. await selectEvent.select(screen.getByText('Select group'), 'project');
  562. // Change the sort by to count_unique
  563. await selectEvent.select(screen.getAllByText('count()')[1], /count_unique/);
  564. // Change the grouping
  565. await selectEvent.select(screen.getByText('project'), 'environment');
  566. // count_unique(user) should still be the sorting field
  567. expect(screen.getByText(/count_unique/)).toBeInTheDocument();
  568. expect(screen.getByText('user')).toBeInTheDocument();
  569. });
  570. it('does not remove the Custom Equation field if a grouping is updated', async function () {
  571. renderTestComponent({
  572. query: {
  573. source: DashboardWidgetSource.DASHBOARDS,
  574. displayType: DisplayType.LINE,
  575. },
  576. });
  577. await selectEvent.select(await screen.findByText('Select group'), 'project');
  578. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  579. userEvent.paste(
  580. screen.getByPlaceholderText('Enter Equation'),
  581. 'count_unique(user) * 2'
  582. );
  583. userEvent.keyboard('{enter}');
  584. userEvent.click(screen.getByText('Add Group'));
  585. expect(screen.getByPlaceholderText('Enter Equation')).toHaveValue(
  586. 'count_unique(user) * 2'
  587. );
  588. });
  589. it.each`
  590. directionPrefix | expectedOrderSelection | displayType
  591. ${'-'} | ${'High to low'} | ${DisplayType.TABLE}
  592. ${''} | ${'Low to high'} | ${DisplayType.TABLE}
  593. ${'-'} | ${'High to low'} | ${DisplayType.LINE}
  594. ${''} | ${'Low to high'} | ${DisplayType.LINE}
  595. `(
  596. `opens a widget with the '$expectedOrderSelection' sort order when the widget was saved with that direction`,
  597. async function ({directionPrefix, expectedOrderSelection}) {
  598. const widget: Widget = {
  599. id: '1',
  600. title: 'Test Widget',
  601. interval: '5m',
  602. displayType: DisplayType.LINE,
  603. queries: [
  604. {
  605. name: '',
  606. conditions: '',
  607. fields: ['count_unique(user)'],
  608. aggregates: ['count_unique(user)'],
  609. columns: ['project'],
  610. orderby: `${directionPrefix}count_unique(user)`,
  611. },
  612. ],
  613. };
  614. const dashboard = mockDashboard({widgets: [widget]});
  615. renderTestComponent({
  616. dashboard,
  617. params: {
  618. widgetIndex: '0',
  619. },
  620. });
  621. await screen.findByText(expectedOrderSelection);
  622. }
  623. );
  624. it('saved widget with aggregate alias as orderby should persist alias when y-axes change', async function () {
  625. const widget: Widget = {
  626. id: '1',
  627. title: 'Test Widget',
  628. interval: '5m',
  629. displayType: DisplayType.TABLE,
  630. queries: [
  631. {
  632. name: '',
  633. conditions: '',
  634. fields: ['project', 'count_unique(user)'],
  635. aggregates: ['count_unique(user)'],
  636. columns: ['project'],
  637. orderby: 'count_unique(user)',
  638. },
  639. ],
  640. };
  641. const dashboard = mockDashboard({widgets: [widget]});
  642. renderTestComponent({
  643. dashboard,
  644. params: {
  645. widgetIndex: '0',
  646. },
  647. });
  648. await screen.findByText('Sort by a column');
  649. // Assert for length 2 since one in the table header and one in sort by
  650. expect(screen.getAllByText('count_unique(user)')).toHaveLength(2);
  651. userEvent.click(screen.getByText('Add a Column'));
  652. // The sort by should still have count_unique(user)
  653. await waitFor(() =>
  654. expect(screen.getAllByText('count_unique(user)')).toHaveLength(2)
  655. );
  656. });
  657. it('will reset the sort field when going from line to table when sorting by a value not in fields', async function () {
  658. renderTestComponent({
  659. query: {
  660. displayType: DisplayType.LINE,
  661. },
  662. });
  663. await selectEvent.select(await screen.findByText('Select group'), 'project');
  664. expect(screen.getAllByText('count()')).toHaveLength(2);
  665. await selectEvent.select(screen.getAllByText('count()')[1], /count_unique/);
  666. userEvent.click(screen.getByText('Line Chart'));
  667. userEvent.click(screen.getByText('Table'));
  668. // 1 for table header, 1 for column selection, and 1 for sorting
  669. await waitFor(() => {
  670. expect(screen.getAllByText('count()')).toHaveLength(3);
  671. });
  672. });
  673. it('equations in y-axis appear in sort by field for grouped timeseries', async function () {
  674. renderTestComponent({
  675. query: {
  676. displayType: DisplayType.LINE,
  677. },
  678. });
  679. userEvent.click(await screen.findByText('Add an Equation'));
  680. userEvent.paste(screen.getByPlaceholderText('Equation'), 'count() * 100');
  681. userEvent.keyboard('{enter}');
  682. await selectEvent.select(screen.getByText('Select group'), 'project');
  683. expect(screen.getAllByText('count()')).toHaveLength(2);
  684. await selectEvent.select(screen.getAllByText('count()')[1], 'count() * 100');
  685. });
  686. it('does not reset the orderby when ordered by an equation in table', async function () {
  687. const widget: Widget = {
  688. id: '1',
  689. title: 'Errors over time',
  690. interval: '5m',
  691. displayType: DisplayType.TABLE,
  692. queries: [
  693. {
  694. name: '',
  695. conditions: '',
  696. fields: [
  697. 'count()',
  698. 'count_unique(id)',
  699. 'equation|count() + count_unique(id)',
  700. ],
  701. aggregates: [
  702. 'count()',
  703. 'count_unique(id)',
  704. 'equation|count() + count_unique(id)',
  705. ],
  706. columns: [],
  707. orderby: '-equation[0]',
  708. },
  709. ],
  710. };
  711. const dashboard = mockDashboard({widgets: [widget]});
  712. renderTestComponent({
  713. dashboard,
  714. params: {
  715. widgetIndex: '0',
  716. },
  717. });
  718. await screen.findByText('Sort by a column');
  719. // 1 in the column selector, 1 in the sort by field
  720. expect(screen.getAllByText('count() + count_unique(id)')).toHaveLength(2);
  721. });
  722. });
  723. describe('with events > Sort by selectors', function () {
  724. it('ordering by column uses field form when selecting orderby', async function () {
  725. const widget: Widget = {
  726. id: '1',
  727. title: 'Test Widget',
  728. interval: '5m',
  729. displayType: DisplayType.TABLE,
  730. queries: [
  731. {
  732. name: 'errors',
  733. conditions: 'event.type:error',
  734. fields: ['count()'],
  735. aggregates: ['count()'],
  736. columns: ['project'],
  737. orderby: '-project',
  738. },
  739. ],
  740. };
  741. const dashboard = mockDashboard({widgets: [widget]});
  742. renderTestComponent({
  743. orgFeatures: [...defaultOrgFeatures, 'discover-frontend-use-events-endpoint'],
  744. dashboard,
  745. params: {
  746. widgetIndex: '0',
  747. },
  748. });
  749. const projectElements = screen.getAllByText('project');
  750. await selectEvent.select(projectElements[projectElements.length - 1], 'count()');
  751. await waitFor(() => {
  752. expect(eventsMock).toHaveBeenCalledWith(
  753. '/organizations/org-slug/events/',
  754. expect.objectContaining({
  755. query: expect.objectContaining({
  756. sort: ['-count()'],
  757. }),
  758. })
  759. );
  760. });
  761. });
  762. it('hides Custom Equation input and resets orderby when switching to table', async function () {
  763. renderTestComponent({
  764. orgFeatures: [...defaultOrgFeatures, 'discover-frontend-use-events-endpoint'],
  765. query: {
  766. source: DashboardWidgetSource.DASHBOARDS,
  767. displayType: DisplayType.LINE,
  768. },
  769. });
  770. await selectEvent.select(await screen.findByText('Select group'), 'project');
  771. expect(screen.getAllByText('count()')).toHaveLength(2);
  772. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  773. userEvent.paste(
  774. screen.getByPlaceholderText('Enter Equation'),
  775. 'count_unique(user) * 2'
  776. );
  777. userEvent.keyboard('{enter}');
  778. // Switch the display type to Table
  779. userEvent.click(screen.getByText('Line Chart'));
  780. userEvent.click(screen.getByText('Table'));
  781. expect(screen.getAllByText('count()')).toHaveLength(2);
  782. expect(screen.queryByPlaceholderText('Enter Equation')).not.toBeInTheDocument();
  783. await waitFor(() => {
  784. expect(eventsMock).toHaveBeenCalledWith(
  785. '/organizations/org-slug/events/',
  786. expect.objectContaining({
  787. query: expect.objectContaining({
  788. sort: ['-count()'],
  789. }),
  790. })
  791. );
  792. });
  793. });
  794. });
  795. });