widgetBuilderSortBy.spec.tsx 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985
  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/releases/stats/',
  212. body: [],
  213. });
  214. MockApiClient.addMockResponse({
  215. url: `/organizations/org-slug/spans/fields/`,
  216. body: [],
  217. });
  218. TagStore.reset();
  219. });
  220. afterEach(function () {
  221. MockApiClient.clearMockResponses();
  222. jest.clearAllMocks();
  223. jest.useRealTimers();
  224. });
  225. describe('with events > Sort by selectors', function () {
  226. it('renders', async function () {
  227. renderTestComponent();
  228. expect(await screen.findByText('Sort by a column')).toBeInTheDocument();
  229. expect(
  230. screen.getByText("Choose one of the columns you've created to sort by.")
  231. ).toBeInTheDocument();
  232. // Selector "sortDirection"
  233. expect(screen.getByText('High to low')).toBeInTheDocument();
  234. // Selector "sortBy"
  235. await waitFor(() => {
  236. expect(screen.getAllByText('count()')).toHaveLength(3);
  237. });
  238. });
  239. it('sortBy defaults to the first field value when changing display type to table', async function () {
  240. const widget: Widget = {
  241. id: '1',
  242. title: 'Errors over time',
  243. interval: '5m',
  244. displayType: DisplayType.LINE,
  245. queries: [
  246. {
  247. name: 'errors',
  248. conditions: 'event.type:error',
  249. fields: ['count()', 'count_unique(id)'],
  250. aggregates: ['count()', 'count_unique(id)'],
  251. columns: [],
  252. orderby: '',
  253. },
  254. {
  255. name: 'csp',
  256. conditions: 'event.type:csp',
  257. fields: ['count()', 'count_unique(id)'],
  258. aggregates: ['count()', 'count_unique(id)'],
  259. columns: [],
  260. orderby: '',
  261. },
  262. ],
  263. };
  264. const dashboard = mockDashboard({widgets: [widget]});
  265. renderTestComponent({
  266. dashboard,
  267. params: {
  268. widgetIndex: '0',
  269. },
  270. });
  271. // Click on the displayType selector
  272. await userEvent.click(await screen.findByText('Line Chart'));
  273. // Choose the table visualization
  274. await userEvent.click(screen.getByText('Table'));
  275. expect(await screen.findByText('Sort by a column')).toBeInTheDocument();
  276. // Selector "sortDirection"
  277. expect(screen.getByText('High to low')).toBeInTheDocument();
  278. // Selector "sortBy"
  279. expect(screen.getAllByText('count()')).toHaveLength(3);
  280. });
  281. it('can update selectors values', async function () {
  282. const handleSave = jest.fn();
  283. const widget: Widget = {
  284. id: '1',
  285. title: 'Errors over time',
  286. interval: '5m',
  287. displayType: DisplayType.TABLE,
  288. queries: [
  289. {
  290. name: '',
  291. conditions: '',
  292. fields: ['count()', 'count_unique(id)'],
  293. aggregates: ['count()', 'count_unique(id)'],
  294. columns: [],
  295. orderby: '-count()',
  296. },
  297. ],
  298. };
  299. const dashboard = mockDashboard({widgets: [widget]});
  300. renderTestComponent({
  301. dashboard,
  302. onSave: handleSave,
  303. params: {
  304. widgetIndex: '0',
  305. },
  306. });
  307. expect(await screen.findByText('Sort by a column')).toBeInTheDocument();
  308. // Selector "sortDirection"
  309. expect(screen.getByText('High to low')).toBeInTheDocument();
  310. // Selector "sortBy"
  311. expect(screen.getAllByText('count()')).toHaveLength(3);
  312. await selectEvent.select(screen.getAllByText('count()')[2], 'count_unique(id)');
  313. // Wait for the Builder update the widget values
  314. await waitFor(() => {
  315. expect(screen.getAllByText('count()')).toHaveLength(2);
  316. });
  317. // Now count_unique(id) is selected in the "sortBy" selector
  318. expect(screen.getAllByText('count_unique(id)')).toHaveLength(2);
  319. await selectEvent.select(screen.getByText('High to low'), 'Low to high');
  320. // Saves the widget
  321. await userEvent.click(screen.getByText('Update Widget'));
  322. await waitFor(() => {
  323. expect(handleSave).toHaveBeenCalledWith([
  324. expect.objectContaining({
  325. queries: [expect.objectContaining({orderby: 'count_unique(id)'})],
  326. }),
  327. ]);
  328. });
  329. });
  330. it('sortBy defaults to the first field value when coming from discover', async function () {
  331. const defaultWidgetQuery = {
  332. name: '',
  333. fields: ['title', 'count()', 'count_unique(user)', 'epm()', 'count()'],
  334. columns: ['title'],
  335. aggregates: ['count()', 'count_unique(user)', 'epm()', 'count()'],
  336. conditions: 'tag:value',
  337. orderby: '',
  338. };
  339. const {router} = renderTestComponent({
  340. query: {
  341. source: DashboardWidgetSource.DISCOVERV2,
  342. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  343. displayType: DisplayType.TABLE,
  344. defaultTableColumns: ['title', 'count()', 'count_unique(user)', 'epm()'],
  345. },
  346. });
  347. expect(await screen.findByText('Sort by a column')).toBeInTheDocument();
  348. // Selector "sortDirection"
  349. expect(await screen.findByText('Low to high')).toBeInTheDocument();
  350. // Selector "sortBy"
  351. await waitFor(() => {
  352. expect(screen.getAllByText('title')).toHaveLength(2);
  353. });
  354. // Saves the widget
  355. await userEvent.click(screen.getByText('Add Widget'));
  356. await waitFor(() => {
  357. expect(router.push).toHaveBeenCalledWith(
  358. expect.objectContaining({
  359. query: expect.objectContaining({queryOrderby: 'count()'}),
  360. })
  361. );
  362. });
  363. });
  364. it('sortBy is only visible on tabular visualizations or when there is a groupBy value selected on time-series visualizations', async function () {
  365. renderTestComponent();
  366. // Sort by shall be visible on table visualization
  367. expect(await screen.findByText('Sort by a column')).toBeInTheDocument();
  368. // Update visualization to be a time-series
  369. await userEvent.click(screen.getByText('Table'));
  370. await userEvent.click(screen.getByText('Line Chart'));
  371. // Time-series visualizations display GroupBy step
  372. expect(await screen.findByText('Group your results')).toBeInTheDocument();
  373. // Do not show sortBy when empty columns (groupBys) are added
  374. await userEvent.click(screen.getByText('Add Group'));
  375. expect(screen.getAllByText('Select group')).toHaveLength(2);
  376. // SortBy step shall not be visible
  377. expect(screen.queryByText('Sort by a y-axis')).not.toBeInTheDocument();
  378. // Select GroupBy value
  379. await selectEvent.select(screen.getAllByText('Select group')[0], 'project');
  380. // Now that at least one groupBy value is selected, the SortBy step shall be visible
  381. expect(screen.getByText('Sort by a y-axis')).toBeInTheDocument();
  382. // Remove selected GroupBy value
  383. await userEvent.click(screen.getAllByLabelText('Remove group')[0]);
  384. // SortBy step shall no longer be visible
  385. expect(screen.queryByText('Sort by a y-axis')).not.toBeInTheDocument();
  386. });
  387. it('allows for sorting by a custom equation', async function () {
  388. renderTestComponent({
  389. query: {
  390. source: DashboardWidgetSource.DASHBOARDS,
  391. displayType: DisplayType.LINE,
  392. },
  393. });
  394. await selectEvent.select(await screen.findByText('Select group'), 'project');
  395. expect(screen.getAllByText('count()')).toHaveLength(2);
  396. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  397. await userEvent.click(screen.getByPlaceholderText('Enter Equation'));
  398. await userEvent.paste('count_unique(user) * 2');
  399. await userEvent.keyboard('{Enter}');
  400. await waitFor(() => {
  401. expect(eventsStatsMock).toHaveBeenCalledWith(
  402. '/organizations/org-slug/events-stats/',
  403. expect.objectContaining({
  404. query: expect.objectContaining({
  405. field: expect.arrayContaining(['equation|count_unique(user) * 2']),
  406. orderby: '-equation[0]',
  407. }),
  408. })
  409. );
  410. });
  411. }, 10000);
  412. it('persists the state when toggling between sorting options', async function () {
  413. renderTestComponent({
  414. query: {
  415. source: DashboardWidgetSource.DASHBOARDS,
  416. displayType: DisplayType.LINE,
  417. },
  418. });
  419. await selectEvent.select(await screen.findByText('Select group'), 'project');
  420. expect(screen.getAllByText('count()')).toHaveLength(2);
  421. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  422. await userEvent.click(screen.getByPlaceholderText('Enter Equation'));
  423. await userEvent.paste('count_unique(user) * 2');
  424. await userEvent.keyboard('{Enter}');
  425. // Switch away from the Custom Equation
  426. expect(screen.getByText('project')).toBeInTheDocument();
  427. await selectEvent.select(screen.getByText('Custom Equation'), 'project');
  428. expect(screen.getAllByText('project')).toHaveLength(2);
  429. // Switch back, the equation should still be visible
  430. await selectEvent.select(screen.getAllByText('project')[1], 'Custom Equation');
  431. expect(screen.getByPlaceholderText('Enter Equation')).toHaveValue(
  432. 'count_unique(user) * 2'
  433. );
  434. });
  435. it('persists the state when updating y-axes', async function () {
  436. renderTestComponent({
  437. query: {
  438. source: DashboardWidgetSource.DASHBOARDS,
  439. displayType: DisplayType.LINE,
  440. },
  441. });
  442. await selectEvent.select(await screen.findByText('Select group'), 'project');
  443. expect(screen.getAllByText('count()')).toHaveLength(2);
  444. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  445. await userEvent.click(screen.getByPlaceholderText('Enter Equation'));
  446. await userEvent.paste('count_unique(user) * 2');
  447. await userEvent.keyboard('{Enter}');
  448. // Add a y-axis
  449. await userEvent.click(screen.getByText('Add Series'));
  450. // The equation should still be visible
  451. expect(screen.getByPlaceholderText('Enter Equation')).toHaveValue(
  452. 'count_unique(user) * 2'
  453. );
  454. });
  455. it('displays the custom equation if the widget has it saved', async function () {
  456. const widget: Widget = {
  457. id: '1',
  458. title: 'Test Widget',
  459. interval: '5m',
  460. displayType: DisplayType.LINE,
  461. queries: [
  462. {
  463. name: '',
  464. conditions: '',
  465. fields: ['count()', 'project'],
  466. aggregates: ['count()'],
  467. columns: ['project'],
  468. orderby: '-equation|count_unique(user) * 2',
  469. },
  470. ],
  471. };
  472. const dashboard = mockDashboard({widgets: [widget]});
  473. renderTestComponent({
  474. query: {
  475. source: DashboardWidgetSource.DASHBOARDS,
  476. displayType: DisplayType.LINE,
  477. },
  478. params: {
  479. widgetIndex: '0',
  480. },
  481. dashboard,
  482. });
  483. expect(await screen.findByPlaceholderText('Enter Equation')).toHaveValue(
  484. 'count_unique(user) * 2'
  485. );
  486. });
  487. it('displays Operators in the input dropdown', async function () {
  488. renderTestComponent({
  489. query: {
  490. source: DashboardWidgetSource.DASHBOARDS,
  491. displayType: DisplayType.LINE,
  492. },
  493. });
  494. await selectEvent.select(await screen.findByText('Select group'), 'project');
  495. expect(screen.getAllByText('count()')).toHaveLength(2);
  496. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  497. await selectEvent.openMenu(screen.getByPlaceholderText('Enter Equation'));
  498. await userEvent.click(screen.getByPlaceholderText('Enter Equation'));
  499. expect(screen.getByText('Operators')).toBeInTheDocument();
  500. expect(screen.queryByText('Fields')).not.toBeInTheDocument();
  501. });
  502. it('hides Custom Equation input and resets orderby when switching to table', async function () {
  503. renderTestComponent({
  504. query: {
  505. source: DashboardWidgetSource.DASHBOARDS,
  506. displayType: DisplayType.LINE,
  507. },
  508. });
  509. await selectEvent.select(await screen.findByText('Select group'), 'project');
  510. expect(screen.getAllByText('count()')).toHaveLength(2);
  511. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  512. await userEvent.click(screen.getByPlaceholderText('Enter Equation'));
  513. await userEvent.paste('count_unique(user) * 2');
  514. await userEvent.keyboard('{Enter}');
  515. // Switch the display type to Table
  516. await userEvent.click(screen.getByText('Line Chart'));
  517. await userEvent.click(screen.getByText('Table'));
  518. expect(screen.getAllByText('count()')).toHaveLength(3);
  519. expect(screen.queryByPlaceholderText('Enter Equation')).not.toBeInTheDocument();
  520. await waitFor(() => {
  521. expect(eventsMock).toHaveBeenCalledWith(
  522. '/organizations/org-slug/events/',
  523. expect.objectContaining({
  524. query: expect.objectContaining({
  525. sort: ['-count()'],
  526. }),
  527. })
  528. );
  529. });
  530. });
  531. it('does not show the Custom Equation input if the only y-axis left is an empty equation', async function () {
  532. renderTestComponent({
  533. query: {
  534. source: DashboardWidgetSource.DASHBOARDS,
  535. displayType: DisplayType.LINE,
  536. },
  537. });
  538. await selectEvent.select(await screen.findByText('Select group'), 'project');
  539. await userEvent.click(screen.getByText('Add an Equation'));
  540. await userEvent.click(screen.getAllByLabelText('Remove this Y-Axis')[0]);
  541. expect(screen.queryByPlaceholderText('Enter Equation')).not.toBeInTheDocument();
  542. });
  543. it('persists a sort by a grouping when changing y-axes', async function () {
  544. renderTestComponent({
  545. query: {
  546. source: DashboardWidgetSource.DASHBOARDS,
  547. displayType: DisplayType.LINE,
  548. },
  549. });
  550. await selectEvent.select(await screen.findByText('Select group'), 'project');
  551. expect(screen.getAllByText('count()')).toHaveLength(2);
  552. // Change the sort option to a grouping field, and then change a y-axis
  553. await selectEvent.select(screen.getAllByText('count()')[1], 'project');
  554. await selectEvent.select(screen.getAllByText('count()')[0], /count_unique/);
  555. // project should appear in the group by field, as well as the sort field
  556. expect(screen.getAllByText('project')).toHaveLength(2);
  557. });
  558. it('persists sort by a y-axis when grouping changes', async function () {
  559. renderTestComponent({
  560. query: {
  561. source: DashboardWidgetSource.DASHBOARDS,
  562. displayType: DisplayType.LINE,
  563. },
  564. });
  565. await userEvent.click(await screen.findByText('Add Series'));
  566. await selectEvent.select(screen.getByText('Select group'), 'project');
  567. // Change the sort by to count_unique
  568. await selectEvent.select(screen.getAllByText('count()')[1], /count_unique/);
  569. // Change the grouping
  570. await selectEvent.select(screen.getByText('project'), 'environment');
  571. // count_unique(user) should still be the sorting field
  572. expect(screen.getByText(/count_unique/)).toBeInTheDocument();
  573. expect(screen.getByText('user')).toBeInTheDocument();
  574. });
  575. it('does not remove the Custom Equation field if a grouping is updated', async function () {
  576. renderTestComponent({
  577. query: {
  578. source: DashboardWidgetSource.DASHBOARDS,
  579. displayType: DisplayType.LINE,
  580. },
  581. });
  582. await selectEvent.select(await screen.findByText('Select group'), 'project');
  583. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  584. await userEvent.click(screen.getByPlaceholderText('Enter Equation'));
  585. await userEvent.paste('count_unique(user) * 2');
  586. await userEvent.keyboard('{Enter}');
  587. await userEvent.click(screen.getByText('Add Group'));
  588. expect(screen.getByPlaceholderText('Enter Equation')).toHaveValue(
  589. 'count_unique(user) * 2'
  590. );
  591. });
  592. it.each`
  593. directionPrefix | expectedOrderSelection | displayType
  594. ${'-'} | ${'High to low'} | ${DisplayType.TABLE}
  595. ${''} | ${'Low to high'} | ${DisplayType.TABLE}
  596. ${'-'} | ${'High to low'} | ${DisplayType.LINE}
  597. ${''} | ${'Low to high'} | ${DisplayType.LINE}
  598. `(
  599. `opens a widget with the '$expectedOrderSelection' sort order when the widget was saved with that direction`,
  600. async function ({directionPrefix, expectedOrderSelection}) {
  601. const widget: Widget = {
  602. id: '1',
  603. title: 'Test Widget',
  604. interval: '5m',
  605. displayType: DisplayType.LINE,
  606. queries: [
  607. {
  608. name: '',
  609. conditions: '',
  610. fields: ['count_unique(user)'],
  611. aggregates: ['count_unique(user)'],
  612. columns: ['project'],
  613. orderby: `${directionPrefix}count_unique(user)`,
  614. },
  615. ],
  616. };
  617. const dashboard = mockDashboard({widgets: [widget]});
  618. renderTestComponent({
  619. dashboard,
  620. params: {
  621. widgetIndex: '0',
  622. },
  623. });
  624. await screen.findByText(expectedOrderSelection);
  625. }
  626. );
  627. it('saved widget with aggregate alias as orderby should persist alias when y-axes change', async function () {
  628. const widget: Widget = {
  629. id: '1',
  630. title: 'Test Widget',
  631. interval: '5m',
  632. displayType: DisplayType.TABLE,
  633. queries: [
  634. {
  635. name: '',
  636. conditions: '',
  637. fields: ['project', 'count_unique(user)'],
  638. aggregates: ['count_unique(user)'],
  639. columns: ['project'],
  640. orderby: 'count_unique(user)',
  641. },
  642. ],
  643. };
  644. const dashboard = mockDashboard({widgets: [widget]});
  645. renderTestComponent({
  646. dashboard,
  647. params: {
  648. widgetIndex: '0',
  649. },
  650. });
  651. await screen.findByText('Sort by a column');
  652. // Assert for length 2 since one in the table header and one in sort by
  653. expect(screen.getAllByText('count_unique(user)')).toHaveLength(2);
  654. await userEvent.click(screen.getByText('Add a Column'));
  655. // The sort by should still have count_unique(user)
  656. await waitFor(() =>
  657. expect(screen.getAllByText('count_unique(user)')).toHaveLength(2)
  658. );
  659. });
  660. it('will reset the sort field when going from line to table when sorting by a value not in fields', async function () {
  661. renderTestComponent({
  662. query: {
  663. displayType: DisplayType.LINE,
  664. },
  665. });
  666. await selectEvent.select(await screen.findByText('Select group'), 'project');
  667. expect(screen.getAllByText('count()')).toHaveLength(2);
  668. await selectEvent.select(screen.getAllByText('count()')[1], /count_unique/);
  669. await userEvent.click(screen.getByText('Line Chart'));
  670. await userEvent.click(screen.getByText('Table'));
  671. // 1 for table header, 1 for column selection, and 1 for sorting
  672. await waitFor(() => {
  673. expect(screen.getAllByText('count()')).toHaveLength(3);
  674. });
  675. });
  676. it('equations in y-axis appear in sort by field for grouped timeseries', async function () {
  677. renderTestComponent({
  678. query: {
  679. displayType: DisplayType.LINE,
  680. },
  681. });
  682. await userEvent.click(await screen.findByText('Add an Equation'));
  683. await userEvent.click(screen.getByPlaceholderText('Equation'));
  684. await userEvent.paste('count() * 100');
  685. await userEvent.keyboard('{Enter}');
  686. await selectEvent.select(screen.getByText('Select group'), 'project');
  687. expect(screen.getAllByText('count()')).toHaveLength(2);
  688. await selectEvent.select(screen.getAllByText('count()')[1], 'count() * 100');
  689. });
  690. it('does not reset the orderby when ordered by an equation in table', async function () {
  691. const widget: Widget = {
  692. id: '1',
  693. title: 'Errors over time',
  694. interval: '5m',
  695. displayType: DisplayType.TABLE,
  696. queries: [
  697. {
  698. name: '',
  699. conditions: '',
  700. fields: [
  701. 'count()',
  702. 'count_unique(id)',
  703. 'equation|count() + count_unique(id)',
  704. ],
  705. aggregates: [
  706. 'count()',
  707. 'count_unique(id)',
  708. 'equation|count() + count_unique(id)',
  709. ],
  710. columns: [],
  711. orderby: '-equation[0]',
  712. },
  713. ],
  714. };
  715. const dashboard = mockDashboard({widgets: [widget]});
  716. renderTestComponent({
  717. dashboard,
  718. params: {
  719. widgetIndex: '0',
  720. },
  721. });
  722. await screen.findByText('Sort by a column');
  723. // 1 in the column selector, 1 in the sort by field
  724. expect(screen.getAllByText('count() + count_unique(id)')).toHaveLength(2);
  725. });
  726. });
  727. it('ordering by column uses field form when selecting orderby', async function () {
  728. const widget: Widget = {
  729. id: '1',
  730. title: 'Test Widget',
  731. interval: '5m',
  732. displayType: DisplayType.TABLE,
  733. queries: [
  734. {
  735. name: 'errors',
  736. conditions: 'event.type:error',
  737. fields: ['count()'],
  738. aggregates: ['count()'],
  739. columns: ['project'],
  740. orderby: '-project',
  741. },
  742. ],
  743. };
  744. const dashboard = mockDashboard({widgets: [widget]});
  745. renderTestComponent({
  746. orgFeatures: [...defaultOrgFeatures],
  747. dashboard,
  748. params: {
  749. widgetIndex: '0',
  750. },
  751. });
  752. const projectElements = screen.getAllByText('project');
  753. await selectEvent.select(projectElements[projectElements.length - 1], 'count()');
  754. await waitFor(() => {
  755. expect(eventsMock).toHaveBeenCalledWith(
  756. '/organizations/org-slug/events/',
  757. expect.objectContaining({
  758. query: expect.objectContaining({
  759. sort: ['-count()'],
  760. }),
  761. })
  762. );
  763. });
  764. });
  765. it('hides Custom Equation input and resets orderby when switching to table', async function () {
  766. renderTestComponent({
  767. orgFeatures: [...defaultOrgFeatures],
  768. query: {
  769. source: DashboardWidgetSource.DASHBOARDS,
  770. displayType: DisplayType.LINE,
  771. },
  772. });
  773. await selectEvent.select(await screen.findByText('Select group'), 'project');
  774. expect(screen.getAllByText('count()')).toHaveLength(2);
  775. await selectEvent.select(screen.getAllByText('count()')[1], 'Custom Equation');
  776. await userEvent.click(screen.getByPlaceholderText('Enter Equation'));
  777. await userEvent.paste('count_unique(user) * 2');
  778. await userEvent.keyboard('{Enter}');
  779. // Switch the display type to Table
  780. await userEvent.click(screen.getByText('Line Chart'));
  781. await userEvent.click(screen.getByText('Table'));
  782. expect(screen.getAllByText('count()')).toHaveLength(3);
  783. expect(screen.queryByPlaceholderText('Enter Equation')).not.toBeInTheDocument();
  784. await waitFor(() => {
  785. expect(eventsMock).toHaveBeenCalledWith(
  786. '/organizations/org-slug/events/',
  787. expect.objectContaining({
  788. query: expect.objectContaining({
  789. sort: ['-count()'],
  790. }),
  791. })
  792. );
  793. });
  794. });
  795. describe('spans dataset timeseries', function () {
  796. it('returns only the selected aggregates and group by as options', async function () {
  797. const widget: Widget = {
  798. id: '1',
  799. title: 'Test Widget',
  800. interval: '5m',
  801. displayType: DisplayType.LINE,
  802. widgetType: WidgetType.SPANS,
  803. queries: [
  804. {
  805. name: '',
  806. conditions: '',
  807. fields: ['count(span.duration)', 'avg(span.duration)', 'transaction'],
  808. aggregates: ['count(span.duration)', 'avg(span.duration)'],
  809. columns: ['transaction'],
  810. orderby: '-count(span.duration)',
  811. },
  812. ],
  813. };
  814. const dashboard = mockDashboard({widgets: [widget]});
  815. renderTestComponent({
  816. dashboard,
  817. params: {
  818. widgetIndex: '0',
  819. },
  820. orgFeatures: [...defaultOrgFeatures, 'dashboards-eap'],
  821. });
  822. await screen.findByText('Sort by a y-axis');
  823. await selectEvent.openMenu(await screen.findByText('count(span.duration)'));
  824. // 3 options in the dropdown
  825. expect(screen.queryAllByTestId('menu-list-item-label')).toHaveLength(3);
  826. // Appears once in the dropdown and once in the sort by field
  827. expect(await screen.findAllByText('count(span.duration)')).toHaveLength(2);
  828. // Appears once in the dropdown
  829. expect(await screen.findAllByText('avg(span.duration)')).toHaveLength(1);
  830. // Appears once in the dropdown and once in the group by field
  831. expect(await screen.findAllByText('transaction')).toHaveLength(2);
  832. });
  833. });
  834. });