widgetBuilderSortBy.spec.tsx 28 KB

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