utils.spec.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. import {
  2. DashboardDetails,
  3. DisplayType,
  4. Widget,
  5. WidgetType,
  6. } from 'sentry/views/dashboards/types';
  7. import {
  8. constructWidgetFromQuery,
  9. eventViewFromWidget,
  10. flattenErrors,
  11. getDashboardsMEPQueryParams,
  12. getFieldsFromEquations,
  13. getNumEquations,
  14. getWidgetDiscoverUrl,
  15. getWidgetIssueUrl,
  16. hasUnsavedFilterChanges,
  17. isCustomMeasurementWidget,
  18. isWidgetUsingTransactionName,
  19. } from 'sentry/views/dashboards/utils';
  20. describe('Dashboards util', () => {
  21. const selection = {
  22. datetime: {
  23. period: '7d',
  24. utc: null,
  25. start: null,
  26. end: null,
  27. },
  28. environments: [],
  29. projects: [],
  30. };
  31. describe('constructWidgetFromQuery', () => {
  32. let baseQuery;
  33. beforeEach(() => {
  34. baseQuery = {
  35. displayType: 'line',
  36. interval: '5m',
  37. queryConditions: ['title:test', 'event.type:test'],
  38. queryFields: ['count()', 'failure_count()'],
  39. queryAggregates: ['count()', 'failure_count()'],
  40. queryColumns: [],
  41. queryNames: ['1', '2'],
  42. queryOrderby: '',
  43. title: 'Widget Title',
  44. };
  45. });
  46. it('returns a widget when given a valid query', () => {
  47. const widget = constructWidgetFromQuery(baseQuery);
  48. expect(widget?.displayType).toEqual(DisplayType.LINE);
  49. expect(widget?.interval).toEqual('5m');
  50. expect(widget?.title).toEqual('Widget Title');
  51. expect(widget?.queries).toEqual([
  52. {
  53. name: '1',
  54. fields: ['count()', 'failure_count()'],
  55. aggregates: ['count()', 'failure_count()'],
  56. columns: [],
  57. conditions: 'title:test',
  58. orderby: '',
  59. },
  60. {
  61. name: '2',
  62. fields: ['count()', 'failure_count()'],
  63. aggregates: ['count()', 'failure_count()'],
  64. columns: [],
  65. conditions: 'event.type:test',
  66. orderby: '',
  67. },
  68. ]);
  69. expect(widget?.widgetType).toEqual('discover');
  70. });
  71. it('returns undefined if query is missing title', () => {
  72. baseQuery.title = '';
  73. const widget = constructWidgetFromQuery(baseQuery);
  74. expect(widget).toBeUndefined();
  75. });
  76. it('returns undefined if query is missing interval', () => {
  77. baseQuery.interval = '';
  78. const widget = constructWidgetFromQuery(baseQuery);
  79. expect(widget).toBeUndefined();
  80. });
  81. it('returns undefined if query is missing displayType', () => {
  82. baseQuery.displayType = '';
  83. const widget = constructWidgetFromQuery(baseQuery);
  84. expect(widget).toBeUndefined();
  85. });
  86. it('returns a widget when given string fields and conditions', () => {
  87. baseQuery.queryConditions = 'title:test';
  88. baseQuery.queryFields = 'count()';
  89. baseQuery.queryAggregates = 'count()';
  90. const widget = constructWidgetFromQuery(baseQuery);
  91. expect(widget?.displayType).toEqual(DisplayType.LINE);
  92. expect(widget?.interval).toEqual('5m');
  93. expect(widget?.title).toEqual('Widget Title');
  94. expect(widget?.queries).toEqual([
  95. {
  96. name: '1',
  97. fields: ['count()'],
  98. aggregates: ['count()'],
  99. columns: [],
  100. conditions: 'title:test',
  101. orderby: '',
  102. },
  103. ]);
  104. });
  105. });
  106. describe('eventViewFromWidget', () => {
  107. let widget;
  108. beforeEach(() => {
  109. widget = {
  110. title: 'Test Query',
  111. displayType: DisplayType.AREA,
  112. widgetType: WidgetType.DISCOVER,
  113. interval: '5m',
  114. queries: [
  115. {
  116. name: '',
  117. conditions: '',
  118. fields: ['count()'],
  119. aggregates: ['count()'],
  120. columns: [],
  121. orderby: '',
  122. },
  123. ],
  124. };
  125. });
  126. it('handles sorts in function format', () => {
  127. const query = {...widget.queries[0], orderby: '-count()'};
  128. const eventView = eventViewFromWidget(widget.title, query, selection);
  129. expect(eventView.fields[0].field).toEqual('count()');
  130. expect(eventView.sorts).toEqual([{field: 'count', kind: 'desc'}]);
  131. });
  132. });
  133. describe('getFieldsFromEquations', function () {
  134. it('returns a list of fields that includes individual terms of provided equations', () => {
  135. const fields = [
  136. 'equation|(count_if(transaction.duration,greater,300) / count()) * 100',
  137. 'equation|(count_if(transaction.duration,lessOrEquals,300) / count()) * 100',
  138. ];
  139. expect(getFieldsFromEquations(fields)).toEqual(
  140. expect.arrayContaining([
  141. 'count_if(transaction.duration,lessOrEquals,300)',
  142. 'count()',
  143. 'count_if(transaction.duration,greater,300)',
  144. ])
  145. );
  146. });
  147. });
  148. describe('getWidgetDiscoverUrl', function () {
  149. let widget;
  150. beforeEach(() => {
  151. widget = {
  152. title: 'Test Query',
  153. displayType: DisplayType.LINE,
  154. widgetType: WidgetType.DISCOVER,
  155. interval: '5m',
  156. queries: [
  157. {
  158. name: '',
  159. conditions: '',
  160. fields: ['count()'],
  161. aggregates: ['count()'],
  162. columns: [],
  163. orderby: '',
  164. },
  165. ],
  166. };
  167. });
  168. it('returns the discover url of the widget query', () => {
  169. const url = getWidgetDiscoverUrl(widget, selection, TestStubs.Organization());
  170. expect(url).toEqual(
  171. '/organizations/org-slug/discover/results/?field=count%28%29&name=Test%20Query&query=&statsPeriod=7d&yAxis=count%28%29'
  172. );
  173. });
  174. it('returns the discover url of a topn widget query', () => {
  175. widget = {
  176. ...widget,
  177. ...{
  178. displayType: DisplayType.TOP_N,
  179. queries: [
  180. {
  181. name: '',
  182. conditions: 'error.unhandled:true',
  183. fields: ['error.type', 'count()'],
  184. aggregates: ['count()'],
  185. columns: ['error.type'],
  186. orderby: '-count',
  187. },
  188. ],
  189. },
  190. };
  191. const url = getWidgetDiscoverUrl(widget, selection, TestStubs.Organization());
  192. expect(url).toEqual(
  193. '/organizations/org-slug/discover/results/?display=top5&field=error.type&field=count%28%29&name=Test%20Query&query=error.unhandled%3Atrue&sort=-count&statsPeriod=7d&yAxis=count%28%29'
  194. );
  195. });
  196. });
  197. describe('getWidgetIssueUrl', function () {
  198. let widget;
  199. beforeEach(() => {
  200. widget = {
  201. title: 'Test Query',
  202. displayType: DisplayType.TABLE,
  203. widgetType: WidgetType.ISSUE,
  204. interval: '5m',
  205. queries: [
  206. {
  207. name: '',
  208. conditions: 'is:unresolved',
  209. fields: ['events'],
  210. orderby: 'date',
  211. },
  212. ],
  213. };
  214. });
  215. it('returns the issue url of the widget query', () => {
  216. const url = getWidgetIssueUrl(widget, selection, TestStubs.Organization());
  217. expect(url).toEqual(
  218. '/organizations/org-slug/issues/?query=is%3Aunresolved&sort=date&statsPeriod=7d'
  219. );
  220. });
  221. });
  222. describe('flattenErrors', function () {
  223. it('flattens nested errors', () => {
  224. const errorResponse = {
  225. widgets: [
  226. {
  227. title: ['Ensure this field has no more than 3 characters.'],
  228. },
  229. ],
  230. };
  231. expect(flattenErrors(errorResponse, {})).toEqual({
  232. title: 'Ensure this field has no more than 3 characters.',
  233. });
  234. });
  235. it('does not spread error strings', () => {
  236. const errorResponse = 'Dashboard title already taken.';
  237. expect(flattenErrors(errorResponse, {})).toEqual({
  238. error: 'Dashboard title already taken.',
  239. });
  240. });
  241. });
  242. describe('getDashboardsMEPQueryParams', function () {
  243. it('returns correct params if enabled', function () {
  244. expect(getDashboardsMEPQueryParams(true)).toEqual({
  245. dataset: 'metricsEnhanced',
  246. });
  247. });
  248. it('returns empty object if disabled', function () {
  249. expect(getDashboardsMEPQueryParams(false)).toEqual({});
  250. });
  251. });
  252. describe('getNumEquations', function () {
  253. it('returns 0 if there are no equations', function () {
  254. expect(getNumEquations(['count()', 'epm()', 'count_unique(user)'])).toBe(0);
  255. });
  256. it('returns the count of equations if there are multiple', function () {
  257. expect(
  258. getNumEquations([
  259. 'count()',
  260. 'equation|count_unique(user) * 2',
  261. 'count_unique(user)',
  262. 'equation|count_unique(user) * 3',
  263. ])
  264. ).toBe(2);
  265. });
  266. it('returns 0 if the possible equations array is empty', function () {
  267. expect(getNumEquations([])).toBe(0);
  268. });
  269. });
  270. describe('isCustomMeasurementWidget', function () {
  271. it('returns false on a non custom measurement widget', function () {
  272. const widget: Widget = {
  273. title: 'Title',
  274. interval: '5m',
  275. displayType: DisplayType.LINE,
  276. widgetType: WidgetType.DISCOVER,
  277. queries: [
  278. {
  279. conditions: '',
  280. fields: [],
  281. aggregates: ['count()', 'p99(measurements.lcp)'],
  282. columns: [],
  283. name: 'widget',
  284. orderby: '',
  285. },
  286. ],
  287. };
  288. expect(isCustomMeasurementWidget(widget)).toBe(false);
  289. });
  290. it('returns true on a custom measurement widget', function () {
  291. const widget: Widget = {
  292. title: 'Title',
  293. interval: '5m',
  294. displayType: DisplayType.LINE,
  295. widgetType: WidgetType.DISCOVER,
  296. queries: [
  297. {
  298. conditions: '',
  299. fields: [],
  300. aggregates: ['p99(measurements.custom.measurement)'],
  301. columns: [],
  302. name: 'widget',
  303. orderby: '',
  304. },
  305. ],
  306. };
  307. expect(isCustomMeasurementWidget(widget)).toBe(true);
  308. });
  309. });
  310. describe('hasUnsavedFilterChanges', function () {
  311. it('ignores the order of projects', function () {
  312. const initialDashboard = {
  313. projects: [1, 2],
  314. } as DashboardDetails;
  315. const location = {
  316. ...TestStubs.location(),
  317. query: {
  318. project: ['2', '1'],
  319. },
  320. };
  321. expect(hasUnsavedFilterChanges(initialDashboard, location)).toBe(false);
  322. });
  323. it('ignores the order of environments', function () {
  324. const initialDashboard = {
  325. environment: ['alpha', 'beta'],
  326. } as DashboardDetails;
  327. const location = {
  328. ...TestStubs.location(),
  329. query: {
  330. environment: ['beta', 'alpha'],
  331. },
  332. };
  333. expect(hasUnsavedFilterChanges(initialDashboard, location)).toBe(false);
  334. });
  335. it('ignores the order of releases', function () {
  336. const initialDashboard = {
  337. filters: {
  338. release: ['v1', 'v2'],
  339. },
  340. } as DashboardDetails;
  341. expect(
  342. hasUnsavedFilterChanges(initialDashboard, {
  343. ...TestStubs.location(),
  344. query: {
  345. release: ['v2', 'v1'],
  346. },
  347. })
  348. ).toBe(false);
  349. });
  350. });
  351. });
  352. describe('isWidgetUsingTransactionName', () => {
  353. let baseQuery;
  354. beforeEach(() => {
  355. baseQuery = {
  356. displayType: 'line',
  357. interval: '5m',
  358. queryConditions: ['title:test', 'event.type:test'],
  359. queryFields: ['count()', 'failure_count()'],
  360. queryNames: ['1', '2'],
  361. queryOrderby: '',
  362. title: 'Widget Title',
  363. };
  364. });
  365. it('returns false when widget does not use transaction', () => {
  366. const widget = constructWidgetFromQuery(baseQuery)!;
  367. expect(isWidgetUsingTransactionName(widget)).toEqual(false);
  368. });
  369. it('returns true when widget uses transaction as a selected field', () => {
  370. baseQuery.queryFields.push('transaction');
  371. const widget = constructWidgetFromQuery(baseQuery)!;
  372. expect(isWidgetUsingTransactionName(widget)).toEqual(true);
  373. });
  374. it('returns true when widget uses transaction as part of the query filter', () => {
  375. baseQuery.queryConditions = ['transaction:test'];
  376. const widget = constructWidgetFromQuery(baseQuery)!;
  377. expect(isWidgetUsingTransactionName(widget)).toEqual(true);
  378. });
  379. });