widgetBuilderSortBy.spec.tsx 31 KB

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