|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449 |
- /*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
- import { MetricKey, MetricType } from '../types/metrics';
- import {
- QualityGateStatusCondition,
- QualityGateStatusConditionEnhanced,
- } from '../types/quality-gates';
- import { Dict, Measure, MeasureEnhanced, Metric } from '../types/types';
- import {
- CCT_SOFTWARE_QUALITY_METRICS,
- LEAK_CCT_SOFTWARE_QUALITY_METRICS,
- LEAK_OLD_TAXONOMY_METRICS,
- ONE_SECOND,
- } from './constants';
- import { translate, translateWithParameters } from './l10n';
- import { getCurrentLocale } from './l10nBundle';
- import { isDefined } from './types';
-
- export const MEASURES_REDIRECTION: Partial<Record<MetricKey, MetricKey>> = {
- [MetricKey.wont_fix_issues]: MetricKey.accepted_issues,
- [MetricKey.open_issues]: MetricKey.violations,
- [MetricKey.reopened_issues]: MetricKey.violations,
- };
-
- export function enhanceMeasuresWithMetrics(
- measures: Measure[],
- metrics: Metric[],
- ): MeasureEnhanced[] {
- return measures
- .map((measure) => {
- const metric = metrics.find((metric) => metric.key === measure.metric);
- return metric && { ...measure, metric };
- })
- .filter(isDefined);
- }
-
- export function enhanceConditionWithMeasure(
- condition: QualityGateStatusCondition,
- measures: MeasureEnhanced[],
- ): QualityGateStatusConditionEnhanced | undefined {
- const measure = measures.find((m) => m.metric.key === condition.metric);
-
- // Make sure we have a period index. This is necessary when dealing with
- // applications.
- let { period } = condition;
- if (measure?.period && !period) {
- period = measure.period.index;
- }
-
- return measure && { ...condition, period, measure };
- }
-
- export function isPeriodBestValue(measure: Measure | MeasureEnhanced): boolean {
- return measure.period?.bestValue ?? false;
- }
-
- /** Check if metric is differential */
- export function isDiffMetric(metricKey: MetricKey | string): boolean {
- return metricKey.startsWith('new_');
- }
-
- export function getDisplayMetrics(metrics: Metric[]) {
- return metrics.filter(
- (metric) =>
- !metric.hidden &&
- ([...CCT_SOFTWARE_QUALITY_METRICS, ...LEAK_CCT_SOFTWARE_QUALITY_METRICS].includes(
- metric.key as MetricKey,
- ) ||
- ![MetricType.Data, MetricType.Distribution].includes(metric.type as MetricType)),
- );
- }
-
- export function findMeasure(measures: MeasureEnhanced[], metric: MetricKey | string) {
- return measures.find((measure) => measure.metric.key === metric);
- }
-
- export function areLeakCCTMeasuresComputed(measures?: Measure[] | MeasureEnhanced[]) {
- if (
- LEAK_OLD_TAXONOMY_METRICS.every(
- (metric) =>
- !measures?.find((measure) =>
- isMeasureEnhanced(measure) ? measure.metric.key === metric : measure.metric === metric,
- ),
- )
- ) {
- return true;
- }
- return LEAK_CCT_SOFTWARE_QUALITY_METRICS.every((metric) =>
- measures?.find((measure) =>
- isMeasureEnhanced(measure) ? measure.metric.key === metric : measure.metric === metric,
- ),
- );
- }
-
- export function areCCTMeasuresComputed(measures?: Measure[] | MeasureEnhanced[]) {
- return CCT_SOFTWARE_QUALITY_METRICS.every((metric) =>
- measures?.find((measure) =>
- isMeasureEnhanced(measure) ? measure.metric.key === metric : measure.metric === metric,
- ),
- );
- }
-
- export function areLeakAndOverallCCTMeasuresComputed(measures?: Measure[] | MeasureEnhanced[]) {
- return areLeakCCTMeasuresComputed(measures) && areCCTMeasuresComputed(measures);
- }
-
- function isMeasureEnhanced(measure: Measure | MeasureEnhanced): measure is MeasureEnhanced {
- return (measure.metric as Metric)?.key !== undefined;
- }
-
- export const getCCTMeasureValue = (key: string, value?: string) => {
- if (
- CCT_SOFTWARE_QUALITY_METRICS.concat(LEAK_CCT_SOFTWARE_QUALITY_METRICS).includes(
- key as MetricKey,
- ) &&
- value !== undefined
- ) {
- return JSON.parse(value).total;
- }
- return value;
- };
-
- const HOURS_IN_DAY = 8;
-
- type Formatter = (value: string | number, options?: Dict<unknown>) => string;
-
- /**
- * Format a measure value for a given type
- * ! For Ratings, use formatRating instead
- */
- export function formatMeasure(
- value: string | number | undefined,
- type: string,
- options?: Dict<unknown>,
- ): string {
- const formatter = getFormatter(type);
- // eslint-disable-next-line react-hooks/rules-of-hooks
- return useFormatter(value, formatter, options);
- }
-
- type RatingValue = 'A' | 'B' | 'C' | 'D' | 'E';
- const RATING_VALUES: RatingValue[] = ['A', 'B', 'C', 'D', 'E'];
- export function formatRating(value: string | number | undefined): RatingValue | undefined {
- if (!value) {
- return undefined;
- }
-
- if (typeof value === 'string') {
- value = parseInt(value, 10);
- }
-
- // rating is 1-5, adjust for 0-based indexing
- return RATING_VALUES[value - 1];
- }
-
- /** Return a localized metric name */
- export function localizeMetric(metricKey: string): string {
- return translate('metric', metricKey, 'name');
- }
-
- /** Return corresponding "short" for better display in UI */
- export function getShortType(type: string): string {
- if (type === MetricType.Integer) {
- return MetricType.ShortInteger;
- } else if (type === 'WORK_DUR') {
- return MetricType.ShortWorkDuration;
- }
- return type;
- }
-
- function useFormatter(
- value: string | number | undefined,
- formatter: Formatter,
- options?: Dict<unknown>,
- ): string {
- return value !== undefined && value !== '' ? formatter(value, options) : '';
- }
-
- function getFormatter(type: string): Formatter {
- const FORMATTERS: Dict<Formatter> = {
- INT: intFormatter,
- SHORT_INT: shortIntFormatter,
- FLOAT: floatFormatter,
- PERCENT: percentFormatter,
- WORK_DUR: durationFormatter,
- SHORT_WORK_DUR: shortDurationFormatter,
- RATING: ratingFormatter,
- LEVEL: levelFormatter,
- MILLISEC: millisecondsFormatter,
- };
- return FORMATTERS[type] || noFormatter;
- }
-
- function numberFormatter(
- value: string | number,
- minimumFractionDigits = 0,
- maximumFractionDigits = minimumFractionDigits,
- ) {
- const { format } = new Intl.NumberFormat(getCurrentLocale(), {
- minimumFractionDigits,
- maximumFractionDigits,
- });
- if (typeof value === 'string') {
- return format(parseFloat(value));
- }
- return format(value);
- }
-
- function noFormatter(value: string | number): string | number {
- return value;
- }
-
- function intFormatter(value: string | number): string {
- return numberFormatter(value);
- }
-
- const shortIntFormats = [
- { unit: 1e10, formatUnit: 1e9, fraction: 0, suffix: 'short_number_suffix.g' },
- { unit: 1e9, formatUnit: 1e9, fraction: 1, suffix: 'short_number_suffix.g' },
- { unit: 1e7, formatUnit: 1e6, fraction: 0, suffix: 'short_number_suffix.m' },
- { unit: 1e6, formatUnit: 1e6, fraction: 1, suffix: 'short_number_suffix.m' },
- { unit: 1e4, formatUnit: 1e3, fraction: 0, suffix: 'short_number_suffix.k' },
- { unit: 1e3, formatUnit: 1e3, fraction: 1, suffix: 'short_number_suffix.k' },
- ];
-
- function shortIntFormatter(
- value: string | number,
- option?: { roundingFunc?: (x: number) => number },
- ): string {
- const roundingFunc = option?.roundingFunc;
- if (typeof value === 'string') {
- value = parseFloat(value);
- }
- for (let i = 0; i < shortIntFormats.length; i++) {
- const { unit, formatUnit, fraction, suffix } = shortIntFormats[i];
- const nextFraction = unit / (shortIntFormats[i + 1] ? shortIntFormats[i + 1].unit / 10 : 1);
- const roundedValue = numberRound(value / unit, nextFraction, roundingFunc);
- if (roundedValue >= 1) {
- return (
- numberFormatter(
- numberRound(value / formatUnit, Math.pow(10, fraction), roundingFunc),
- 0,
- fraction,
- ) + translate(suffix)
- );
- }
- }
-
- return numberFormatter(value);
- }
-
- function numberRound(
- value: number,
- fraction: number = 1000,
- roundingFunc: (x: number) => number = Math.round,
- ) {
- return roundingFunc(value * fraction) / fraction;
- }
-
- function floatFormatter(value: string | number): string {
- return numberFormatter(value, 1, 5);
- }
-
- function percentFormatter(
- value: string | number,
- { decimals, omitExtraDecimalZeros }: { decimals?: number; omitExtraDecimalZeros?: boolean } = {},
- ): string {
- if (typeof value === 'string') {
- value = parseFloat(value);
- }
- if (value === 100) {
- return '100%';
- } else if (omitExtraDecimalZeros && decimals) {
- // If omitExtraDecimalZeros is true, all trailing decimal 0s will be removed,
- // except for the first decimal.
- // E.g. for decimals=3:
- // - omitExtraDecimalZeros: false, value: 45.450 => 45.450
- // - omitExtraDecimalZeros: true, value: 45.450 => 45.45
- // - omitExtraDecimalZeros: false, value: 85 => 85.000
- // - omitExtraDecimalZeros: true, value: 85 => 85.0
- return `${numberFormatter(value, 1, decimals)}%`;
- }
- return `${numberFormatter(value, decimals || 1)}%`;
- }
-
- function ratingFormatter(value: string | number): string {
- if (typeof value === 'string') {
- value = parseInt(value, 10);
- }
- return String.fromCharCode(97 + value - 1).toUpperCase();
- }
-
- function levelFormatter(value: string | number): string {
- if (typeof value === 'number') {
- value = value.toString();
- }
- const l10nKey = `metric.level.${value}`;
- const result = translate(l10nKey);
-
- // if couldn't translate, return the initial value
- return l10nKey !== result ? result : value;
- }
-
- function millisecondsFormatter(value: string | number): string {
- if (typeof value === 'string') {
- value = parseInt(value, 10);
- }
- const ONE_MINUTE = 60 * ONE_SECOND;
- if (value >= ONE_MINUTE) {
- const minutes = Math.round(value / ONE_MINUTE);
- return `${minutes}min`;
- } else if (value >= ONE_SECOND) {
- const seconds = Math.round(value / ONE_SECOND);
- return `${seconds}s`;
- }
- return `${value}ms`;
- }
-
- /*
- * Debt Formatters
- */
-
- function shouldDisplayDays(days: number): boolean {
- return days > 0;
- }
-
- function shouldDisplayDaysInShortFormat(days: number): boolean {
- return days > 0.9;
- }
-
- function shouldDisplayHours(days: number, hours: number): boolean {
- return hours > 0 && days < 10;
- }
-
- function shouldDisplayHoursInShortFormat(hours: number): boolean {
- return hours > 0.9;
- }
-
- function shouldDisplayMinutes(days: number, hours: number, minutes: number): boolean {
- return minutes > 0 && hours < 10 && days === 0;
- }
-
- function addSpaceIfNeeded(value: string): string {
- return value.length > 0 ? `${value} ` : value;
- }
-
- function formatDuration(isNegative: boolean, days: number, hours: number, minutes: number): string {
- let formatted = '';
- if (shouldDisplayDays(days)) {
- formatted += translateWithParameters('work_duration.x_days', isNegative ? -1 * days : days);
- }
- if (shouldDisplayHours(days, hours)) {
- formatted = addSpaceIfNeeded(formatted);
- formatted += translateWithParameters(
- 'work_duration.x_hours',
- isNegative && formatted.length === 0 ? -1 * hours : hours,
- );
- }
- if (shouldDisplayMinutes(days, hours, minutes)) {
- formatted = addSpaceIfNeeded(formatted);
- formatted += translateWithParameters(
- 'work_duration.x_minutes',
- isNegative && formatted.length === 0 ? -1 * minutes : minutes,
- );
- }
- return formatted;
- }
-
- function formatDurationShort(
- isNegative: boolean,
- days: number,
- hours: number,
- minutes: number,
- ): string {
- if (shouldDisplayDaysInShortFormat(days)) {
- const roundedDays = Math.round(days);
- const formattedDays = formatMeasure(
- isNegative ? -1 * roundedDays : roundedDays,
- MetricType.ShortInteger,
- );
- return translateWithParameters('work_duration.x_days', formattedDays);
- }
-
- if (shouldDisplayHoursInShortFormat(hours)) {
- const roundedHours = Math.round(hours);
- const formattedHours = formatMeasure(
- isNegative ? -1 * roundedHours : roundedHours,
- MetricType.ShortInteger,
- );
- return translateWithParameters('work_duration.x_hours', formattedHours);
- }
-
- const formattedMinutes = formatMeasure(
- isNegative ? -1 * minutes : minutes,
- MetricType.ShortInteger,
- );
- return translateWithParameters('work_duration.x_minutes', formattedMinutes);
- }
-
- function durationFormatter(value: string | number): string {
- if (typeof value === 'string') {
- value = parseInt(value, 10);
- }
- if (value === 0) {
- return '0';
- }
- const hoursInDay = HOURS_IN_DAY;
- const isNegative = value < 0;
- const absValue = Math.abs(value);
- const days = Math.floor(absValue / hoursInDay / 60);
- let remainingValue = absValue - days * hoursInDay * 60;
- const hours = Math.floor(remainingValue / 60);
- remainingValue -= hours * 60;
- return formatDuration(isNegative, days, hours, remainingValue);
- }
-
- function shortDurationFormatter(value: string | number): string {
- if (typeof value === 'string') {
- value = parseInt(value, 10);
- }
- if (value === 0) {
- return '0';
- }
- const hoursInDay = HOURS_IN_DAY;
- const isNegative = value < 0;
- const absValue = Math.abs(value);
- const days = absValue / hoursInDay / 60;
- let remainingValue = absValue - Math.floor(days) * hoursInDay * 60;
- const hours = remainingValue / 60;
- remainingValue -= Math.floor(hours) * 60;
- return formatDurationShort(isNegative, days, hours, remainingValue);
- }
|