widgetBuilderSortBy.spec.tsx 28 KB

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