widgetBuilderSortBy.spec.tsx 28 KB

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