widgetBuilderSortBy.spec.tsx 28 KB

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