/* * SonarQube * Copyright (C) 2009-2023 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 { isBefore, sub } from 'date-fns'; import { ButtonLink, Card, CoverageIndicator, DuplicationsIndicator, FlagMessage, LightLabel, PageTitle, Spinner, ToggleButton, } from 'design-system'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import DocLink from '../../../components/common/DocLink'; import ComponentReportActions from '../../../components/controls/ComponentReportActions'; import { Location, withRouter } from '../../../components/hoc/withRouter'; import { duplicationRatingConverter } from '../../../components/measure/utils'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { findMeasure, isDiffMetric } from '../../../helpers/measures'; import { CodeScope } from '../../../helpers/urls'; import { ApplicationPeriod } from '../../../types/application'; import { Branch } from '../../../types/branch-like'; import { ComponentQualifier } from '../../../types/component'; import { IssueType } from '../../../types/issues'; import { MetricKey } from '../../../types/metrics'; import { Analysis, ProjectAnalysisEventCategory } from '../../../types/project-activity'; import { QualityGateStatus } from '../../../types/quality-gates'; import { Component, MeasureEnhanced, Period } from '../../../types/types'; import { MeasurementType, parseQuery } from '../utils'; import { MAX_ANALYSES_NB } from './ActivityPanel'; import { LeakPeriodInfo } from './LeakPeriodInfo'; import MeasuresPanelIssueMeasure from './MeasuresPanelIssueMeasure'; import MeasuresPanelNoNewCode from './MeasuresPanelNoNewCode'; import MeasuresPanelPercentMeasure from './MeasuresPanelPercentMeasure'; export interface MeasuresPanelProps { analyses?: Analysis[]; appLeak?: ApplicationPeriod; branch?: Branch; component: Component; loading?: boolean; measures?: MeasureEnhanced[]; period?: Period; location: Location; qgStatuses?: QualityGateStatus[]; } export enum MeasuresPanelTabs { New = 'new', Overall = 'overall', } const SQ_UPGRADE_NOTIFICATION_TIMEOUT = { weeks: 3 }; export function MeasuresPanel(props: MeasuresPanelProps) { const { analyses, appLeak, branch, component, loading, measures = [], period, qgStatuses = [], location, } = props; const hasDiffMeasures = measures.some((m) => isDiffMetric(m.metric.key)); const isApp = component.qualifier === ComponentQualifier.Application; const leakPeriod = isApp ? appLeak : period; const query = parseQuery(location.query); const { failingConditionsOnNewCode, failingConditionsOnOverallCode } = countFailingConditions(qgStatuses); const failingConditions = failingConditionsOnNewCode + failingConditionsOnOverallCode; const [tab, selectTab] = React.useState(() => { return query.codeScope === CodeScope.Overall ? MeasuresPanelTabs.Overall : MeasuresPanelTabs.New; }); const isNewCodeTab = tab === MeasuresPanelTabs.New; const recentSqUpgradeEvent = React.useMemo(() => { if (!analyses || analyses.length === 0) { return undefined; } const notificationExpirationTime = sub(new Date(), SQ_UPGRADE_NOTIFICATION_TIMEOUT); for (const analysis of analyses.slice(0, MAX_ANALYSES_NB)) { if (isBefore(new Date(analysis.date), notificationExpirationTime)) { return undefined; } let sqUpgradeEvent = undefined; let hasQpUpdateEvent = false; for (const event of analysis.events) { sqUpgradeEvent = event.category === ProjectAnalysisEventCategory.SqUpgrade ? event : sqUpgradeEvent; hasQpUpdateEvent = hasQpUpdateEvent || event.category === ProjectAnalysisEventCategory.QualityProfile; if (sqUpgradeEvent !== undefined && hasQpUpdateEvent) { return sqUpgradeEvent; } } } return undefined; }, [analyses]); const scrollToLatestSqUpgradeEvent = () => { document.querySelector(`#${recentSqUpgradeEvent?.key}`)?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center', }); }; React.useEffect(() => { // Open Overall tab by default if there are no new measures. if (loading === false && !hasDiffMeasures && isNewCodeTab) { selectTab(MeasuresPanelTabs.Overall); } // In this case, we explicitly do NOT want to mark tab as a dependency, as // it would prevent the user from selecting it, even if it's empty. /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [loading, hasDiffMeasures]); const tabs = [ { value: MeasuresPanelTabs.New, label: translate('overview.new_code'), counter: failingConditionsOnNewCode, }, { value: MeasuresPanelTabs.Overall, label: translate('overview.overall_code'), counter: failingConditionsOnOverallCode, }, ]; return (