widgetBuilderSortBy.spec.tsx 30 KB

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