widgetBuilderSortBy.spec.tsx 28 KB

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