widgetBuilderSortBy.spec.tsx 29 KB

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