You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

utils.ts 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import { invert } from 'lodash';
  21. import { Facet, getScannableProjects, searchProjects } from '../../api/components';
  22. import { getMeasuresForProjects } from '../../api/measures';
  23. import { translate, translateWithParameters } from '../../helpers/l10n';
  24. import { isDiffMetric } from '../../helpers/measures';
  25. import { RequestData } from '../../helpers/request';
  26. import { MetricKey } from '../../types/metrics';
  27. import { Dict } from '../../types/types';
  28. import { Query, convertToFilter } from './query';
  29. interface SortingOption {
  30. class?: string;
  31. value: string;
  32. }
  33. export const PROJECTS_DEFAULT_FILTER = 'sonarqube.projects.default';
  34. export const PROJECTS_FAVORITE = 'favorite';
  35. export const PROJECTS_ALL = 'all';
  36. export const SORTING_METRICS: SortingOption[] = [
  37. { value: 'name' },
  38. { value: 'analysis_date' },
  39. { value: 'creation_date' },
  40. { value: 'reliability' },
  41. { value: 'security' },
  42. { value: 'security_review' },
  43. { value: 'maintainability' },
  44. { value: 'coverage' },
  45. { value: 'duplications' },
  46. { value: 'size' },
  47. ];
  48. export const SORTING_LEAK_METRICS: SortingOption[] = [
  49. { value: 'name' },
  50. { value: 'analysis_date' },
  51. { value: 'creation_date' },
  52. { value: 'new_reliability', class: 'projects-leak-sorting-option' },
  53. { value: 'new_security', class: 'projects-leak-sorting-option' },
  54. { value: 'new_security_review', class: 'projects-leak-sorting-option' },
  55. { value: 'new_maintainability', class: 'projects-leak-sorting-option' },
  56. { value: 'new_coverage', class: 'projects-leak-sorting-option' },
  57. { value: 'new_duplications', class: 'projects-leak-sorting-option' },
  58. { value: 'new_lines', class: 'projects-leak-sorting-option' },
  59. ];
  60. export const SORTING_SWITCH: Dict<string> = {
  61. analysis_date: 'analysis_date',
  62. name: 'name',
  63. reliability: 'new_reliability',
  64. security: 'new_security',
  65. security_review: 'new_security_review',
  66. maintainability: 'new_maintainability',
  67. coverage: 'new_coverage',
  68. duplications: 'new_duplications',
  69. size: 'new_lines',
  70. new_reliability: 'reliability',
  71. new_security: 'security',
  72. new_security_review: 'security_review',
  73. new_maintainability: 'maintainability',
  74. new_coverage: 'coverage',
  75. new_duplications: 'duplications',
  76. new_lines: 'size',
  77. };
  78. export const VIEWS = [
  79. { value: 'overall', label: 'overall' },
  80. { value: 'leak', label: 'new_code' },
  81. ];
  82. const PAGE_SIZE = 50;
  83. export const METRICS = [
  84. MetricKey.alert_status,
  85. MetricKey.reliability_issues,
  86. MetricKey.bugs,
  87. MetricKey.reliability_rating,
  88. MetricKey.security_issues,
  89. MetricKey.vulnerabilities,
  90. MetricKey.security_rating,
  91. MetricKey.maintainability_issues,
  92. MetricKey.code_smells,
  93. MetricKey.sqale_rating,
  94. MetricKey.security_hotspots_reviewed,
  95. MetricKey.security_review_rating,
  96. MetricKey.duplicated_lines_density,
  97. MetricKey.coverage,
  98. MetricKey.ncloc,
  99. MetricKey.ncloc_language_distribution,
  100. MetricKey.projects,
  101. ];
  102. export const LEAK_METRICS = [
  103. MetricKey.alert_status,
  104. MetricKey.new_violations,
  105. MetricKey.new_security_hotspots_reviewed,
  106. MetricKey.new_security_review_rating,
  107. MetricKey.new_coverage,
  108. MetricKey.new_duplicated_lines_density,
  109. MetricKey.new_lines,
  110. MetricKey.projects,
  111. ];
  112. export const FACETS = [
  113. 'reliability_rating',
  114. 'security_rating',
  115. 'security_review_rating',
  116. 'sqale_rating',
  117. 'coverage',
  118. 'duplicated_lines_density',
  119. 'ncloc',
  120. 'alert_status',
  121. 'languages',
  122. 'tags',
  123. 'qualifier',
  124. ];
  125. export const LEAK_FACETS = [
  126. 'new_reliability_rating',
  127. 'new_security_rating',
  128. 'new_security_review_rating',
  129. 'new_maintainability_rating',
  130. 'new_coverage',
  131. 'new_duplicated_lines_density',
  132. 'new_lines',
  133. 'alert_status',
  134. 'languages',
  135. 'tags',
  136. 'qualifier',
  137. ];
  138. const REVERSED_FACETS = ['coverage', 'new_coverage'];
  139. let scannableProjectsCached: { key: string; name: string }[] | null = null;
  140. export function localizeSorting(sort?: string): string {
  141. return translate('projects.sort', sort ?? 'name');
  142. }
  143. export function parseSorting(sort: string): { sortValue: string; sortDesc: boolean } {
  144. const desc = sort.startsWith('-');
  145. return { sortValue: desc ? sort.substring(1) : sort, sortDesc: desc };
  146. }
  147. export async function fetchScannableProjects() {
  148. if (scannableProjectsCached) {
  149. return Promise.resolve({ scannableProjects: scannableProjectsCached });
  150. }
  151. const response = await getScannableProjects().then(({ projects }) => {
  152. scannableProjectsCached = projects;
  153. return projects;
  154. });
  155. return { scannableProjects: response };
  156. }
  157. export function fetchProjects({
  158. isFavorite,
  159. query,
  160. pageIndex = 1,
  161. }: {
  162. query: Query;
  163. isFavorite: boolean;
  164. pageIndex?: number;
  165. }) {
  166. const ps = PAGE_SIZE;
  167. const data = convertToQueryData(query, isFavorite, {
  168. p: pageIndex > 1 ? pageIndex : undefined,
  169. ps,
  170. facets: defineFacets(query).join(),
  171. f: 'analysisDate,leakPeriodDate',
  172. });
  173. return searchProjects(data)
  174. .then((response) =>
  175. Promise.all([
  176. fetchProjectMeasures(response.components, query),
  177. Promise.resolve(response),
  178. fetchScannableProjects(),
  179. ]),
  180. )
  181. .then(([measures, { components, facets, paging }, { scannableProjects }]) => {
  182. return {
  183. facets: getFacetsMap(facets),
  184. projects: components.map((component) => {
  185. const componentMeasures: Dict<string> = {};
  186. measures
  187. .filter((measure) => measure.component === component.key)
  188. .forEach((measure) => {
  189. const value = isDiffMetric(measure.metric) ? measure.period?.value : measure.value;
  190. if (value !== undefined) {
  191. componentMeasures[measure.metric] = value;
  192. }
  193. });
  194. return {
  195. ...component,
  196. measures: componentMeasures,
  197. isScannable: scannableProjects.find((p) => p.key === component.key) !== undefined,
  198. };
  199. }),
  200. total: paging.total,
  201. };
  202. });
  203. }
  204. export function defineMetrics(query: Query): string[] {
  205. if (query.view === 'leak') {
  206. return LEAK_METRICS;
  207. }
  208. return METRICS;
  209. }
  210. function defineFacets(query: Query): string[] {
  211. if (query.view === 'leak') {
  212. return LEAK_FACETS;
  213. }
  214. return FACETS;
  215. }
  216. export function convertToQueryData(query: Query, isFavorite: boolean, defaultData = {}) {
  217. const data: RequestData = { ...defaultData };
  218. const filter = convertToFilter(query, isFavorite);
  219. const sort = convertToSorting(query);
  220. if (filter) {
  221. data.filter = filter;
  222. }
  223. if (sort.s) {
  224. data.s = sort.s;
  225. }
  226. if (sort.asc !== undefined) {
  227. data.asc = sort.asc;
  228. }
  229. return data;
  230. }
  231. export function fetchProjectMeasures(projects: Array<{ key: string }>, query: Query) {
  232. if (!projects.length) {
  233. return Promise.resolve([]);
  234. }
  235. const projectKeys = projects.map((project) => project.key);
  236. const metrics = defineMetrics(query);
  237. return getMeasuresForProjects(projectKeys, metrics);
  238. }
  239. function mapFacetValues(values: Array<{ val: string; count: number }>) {
  240. const map: Dict<number> = {};
  241. values.forEach((value) => {
  242. map[value.val] = value.count;
  243. });
  244. return map;
  245. }
  246. const propertyToMetricMap: Dict<string | undefined> = {
  247. analysis_date: 'analysisDate',
  248. reliability: 'reliability_rating',
  249. new_reliability: 'new_reliability_rating',
  250. security: 'security_rating',
  251. new_security: 'new_security_rating',
  252. security_review: 'security_review_rating',
  253. new_security_review: 'new_security_review_rating',
  254. maintainability: 'sqale_rating',
  255. new_maintainability: 'new_maintainability_rating',
  256. coverage: 'coverage',
  257. new_coverage: 'new_coverage',
  258. duplications: 'duplicated_lines_density',
  259. new_duplications: 'new_duplicated_lines_density',
  260. size: 'ncloc',
  261. new_lines: 'new_lines',
  262. gate: 'alert_status',
  263. languages: 'languages',
  264. tags: 'tags',
  265. search: 'query',
  266. qualifier: 'qualifier',
  267. creation_date: 'creationDate',
  268. };
  269. const metricToPropertyMap = invert(propertyToMetricMap);
  270. function getFacetsMap(facets: Facet[]) {
  271. const map: Dict<Dict<number>> = {};
  272. facets.forEach((facet) => {
  273. const property = metricToPropertyMap[facet.property];
  274. const { values } = facet;
  275. if (REVERSED_FACETS.includes(property)) {
  276. values.reverse();
  277. }
  278. map[property] = mapFacetValues(values);
  279. });
  280. return map;
  281. }
  282. export function convertToSorting({ sort }: Query): { s?: string; asc?: boolean } {
  283. if (sort?.startsWith('-')) {
  284. return { s: propertyToMetricMap[sort.substring(1)], asc: false };
  285. }
  286. return { s: propertyToMetricMap[sort ?? ''] };
  287. }
  288. const ONE_MINUTE = 60000;
  289. const ONE_HOUR = 60 * ONE_MINUTE;
  290. const ONE_DAY = 24 * ONE_HOUR;
  291. const ONE_MONTH = 30 * ONE_DAY;
  292. const ONE_YEAR = 12 * ONE_MONTH;
  293. function format(periods: Array<{ value: number; label: string }>) {
  294. let result = '';
  295. let count = 0;
  296. let lastId = -1;
  297. for (let i = 0; i < periods.length && count < 2; i++) {
  298. if (periods[i].value > 0) {
  299. count++;
  300. if (lastId < 0 || lastId + 1 === i) {
  301. lastId = i;
  302. result += translateWithParameters(periods[i].label, periods[i].value) + ' ';
  303. }
  304. }
  305. }
  306. return result;
  307. }
  308. export function formatDuration(ms: number) {
  309. if (ms < ONE_MINUTE) {
  310. return translate('duration.seconds');
  311. }
  312. const years = Math.floor(ms / ONE_YEAR);
  313. ms -= years * ONE_YEAR;
  314. const months = Math.floor(ms / ONE_MONTH);
  315. ms -= months * ONE_MONTH;
  316. const days = Math.floor(ms / ONE_DAY);
  317. ms -= days * ONE_DAY;
  318. const hours = Math.floor(ms / ONE_HOUR);
  319. ms -= hours * ONE_HOUR;
  320. const minutes = Math.floor(ms / ONE_MINUTE);
  321. return format([
  322. { value: years, label: 'duration.years' },
  323. { value: months, label: 'duration.months' },
  324. { value: days, label: 'duration.days' },
  325. { value: hours, label: 'duration.hours' },
  326. { value: minutes, label: 'duration.minutes' },
  327. ]);
  328. }