widgetBuilderSortBy.spec.tsx 28 KB

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