widgetBuilderSortBy.spec.tsx 28 KB

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