From: Ismail Cherri Date: Mon, 18 Mar 2024 09:07:15 +0000 (+0100) Subject: SONAR-21799 Measures page reflects software qualities X-Git-Tag: 10.5.0.89998~96 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=01170ab5bd96576e433033085aab8ed6c8c0607e;p=sonarqube.git SONAR-21799 Measures page reflects software qualities --- diff --git a/server/sonar-web/src/main/js/api/mocks/data/measures.ts b/server/sonar-web/src/main/js/api/mocks/data/measures.ts index 6c571bb0703..91f1480c84a 100644 --- a/server/sonar-web/src/main/js/api/mocks/data/measures.ts +++ b/server/sonar-web/src/main/js/api/mocks/data/measures.ts @@ -28,6 +28,7 @@ import { ComponentTree } from './components'; import { IssueData } from './issues'; import { listAllComponent, listAllComponentTrees } from './utils'; +const MAX_RATING = 5; export type MeasureRecords = Record>; export function mockFullMeasureData(tree: ComponentTree, issueList: IssueData[]) { @@ -68,6 +69,21 @@ function mockComponentMeasure(tree: ComponentTree, issueList: IssueData[], metri }), }); + case MetricKey.new_security_issues: + return mockMeasure({ + metric: metricKey, + period: { + index: 1, + value: JSON.stringify({ + total: 3, + [SoftwareImpactSeverity.High]: 2, + [SoftwareImpactSeverity.Medium]: 0, + [SoftwareImpactSeverity.Low]: 1, + }), + }, + value: undefined, + }); + case MetricKey.reliability_issues: return mockMeasure({ metric: metricKey, @@ -79,6 +95,21 @@ function mockComponentMeasure(tree: ComponentTree, issueList: IssueData[], metri }), }); + case MetricKey.new_reliability_issues: + return mockMeasure({ + metric: metricKey, + period: { + index: 1, + value: JSON.stringify({ + total: 2, + [SoftwareImpactSeverity.High]: 0, + [SoftwareImpactSeverity.Medium]: 1, + [SoftwareImpactSeverity.Low]: 1, + }), + }, + value: undefined, + }); + case MetricKey.maintainability_issues: return mockMeasure({ metric: metricKey, @@ -89,6 +120,21 @@ function mockComponentMeasure(tree: ComponentTree, issueList: IssueData[], metri [SoftwareImpactSeverity.Low]: 1, }), }); + + case MetricKey.new_maintainability_issues: + return mockMeasure({ + metric: metricKey, + period: { + index: 1, + value: JSON.stringify({ + total: 5, + [SoftwareImpactSeverity.High]: 2, + [SoftwareImpactSeverity.Medium]: 2, + [SoftwareImpactSeverity.Low]: 1, + }), + }, + value: undefined, + }); } const issues = issueList @@ -234,13 +280,16 @@ function mockComponentMeasure(tree: ComponentTree, issueList: IssueData[], metri export function getMetricTypeFromKey(metricKey: string) { if (/(coverage|duplication)$/.test(metricKey)) { return MetricType.Percent; - } else if (/_rating$/.test(metricKey)) { + } else if (metricKey.includes('_rating')) { return MetricType.Rating; } else if ( [ MetricKey.reliability_issues, + MetricKey.new_reliability_issues, MetricKey.security_issues, + MetricKey.new_security_issues, MetricKey.maintainability_issues, + MetricKey.new_maintainability_issues, ].includes(metricKey as MetricKey) ) { return MetricType.Data; @@ -276,7 +325,7 @@ function isIssueRelatedRating(metricKey: MetricKey) { * ratio to the LOC. But using the number will suffice as an approximation in our tests. */ function computeRating(issues: RawIssue[], type: IssueType) { - const value = Math.max(Math.min(issues.filter((i) => i.type === type).length, 5), 1); + const value = Math.max(Math.min(issues.filter((i) => i.type === type).length, MAX_RATING), 1); return { value: `${value}.0`, bestValue: value === 1, diff --git a/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx b/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx index 80db0b61b39..2f115c6d4dd 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx @@ -88,11 +88,11 @@ describe('rendering', () => { // Check one of the domains. await user.click(ui.maintainabilityDomainBtn.get()); [ - 'New Code Smells 8', + 'component_measures.metric.new_maintainability_issues.name 5', 'Added Technical Debt work_duration.x_minutes.1', 'Technical Debt Ratio on New Code 1.0%', 'Maintainability Rating on New Code metric.has_rating_X.E', - 'Code Smells 8', + 'component_measures.metric.maintainability_issues.name 2', 'Technical Debt work_duration.x_minutes.1', 'Technical Debt Ratio 1.0%', 'Maintainability Rating metric.has_rating_X.E', @@ -102,6 +102,32 @@ describe('rendering', () => { }); }); + it('should correctly revert to old measures when analysis is missing', async () => { + measuresHandler.deleteComponentMeasure('foo', MetricKey.maintainability_issues); + measuresHandler.deleteComponentMeasure('foo', MetricKey.new_maintainability_issues); + + const { ui, user } = getPageObject(); + renderMeasuresApp(); + await ui.appLoaded(); + + // Check one of the domains. + await user.click(ui.maintainabilityDomainBtn.get()); + [ + 'component_measures.metric.new_code_smells.name 8', + 'Added Technical Debt work_duration.x_minutes.1', + 'Technical Debt Ratio on New Code 1.0%', + 'Maintainability Rating on New Code metric.has_rating_X.E', + 'component_measures.metric.code_smells.name 8', + 'Technical Debt work_duration.x_minutes.1', + 'Technical Debt Ratio 1.0%', + 'Maintainability Rating metric.has_rating_X.E', + 'Effort to Reach Maintainability Rating A work_duration.x_minutes.1', + ].forEach((measure) => { + expect(ui.measureBtn(measure).get()).toBeInTheDocument(); + }); + expect(screen.getByText('overview.missing_project_data.TRK')).toBeInTheDocument(); + }); + it('should correctly render a list view', async () => { const { ui } = getPageObject(); renderMeasuresApp('component_measures?id=foo&metric=code_smells&view=list'); @@ -172,7 +198,17 @@ describe('rendering', () => { renderMeasuresApp('component_measures?id=foo&metric=open_issues'); await ui.appLoaded(); - expect(screen.getAllByText('Issues').length).toBeGreaterThan(1); + expect(screen.getAllByText('Issues').length).toEqual(1); + [ + 'component_measures.metric.new_violations.name 1', + 'component_measures.metric.violations.name 1', + 'component_measures.metric.confirmed_issues.name 1', + 'component_measures.metric.accepted_issues.name 1', + 'component_measures.metric.new_accepted_issues.name 1', + 'component_measures.metric.false_positive_issues.name 1', + ].forEach((measure) => { + expect(ui.measureBtn(measure).get()).toBeInTheDocument(); + }); }); it('should render correctly if there are no measures', async () => { @@ -267,14 +303,16 @@ describe('rendering', () => { it('should correctly render a link to the activity page', async () => { const { ui, user } = getPageObject(); - renderMeasuresApp('component_measures?id=foo&metric=new_code_smells'); + renderMeasuresApp('component_measures?id=foo&metric=new_maintainability_issues'); await ui.appLoaded(); expect(ui.goToActivityLink.query()).not.toBeInTheDocument(); - await user.click(ui.measureBtn('Code Smells 8').get()); + await user.click( + ui.measureBtn('component_measures.metric.maintainability_issues.name 2').get(), + ); expect(ui.goToActivityLink.get()).toHaveAttribute( 'href', - '/project/activity?id=foo&graph=custom&custom_metrics=code_smells', + '/project/activity?id=foo&graph=custom&custom_metrics=maintainability_issues', ); }); @@ -305,9 +343,11 @@ describe('navigation', () => { // Drilldown to the file level. await user.click(ui.maintainabilityDomainBtn.get()); - await user.click(ui.measureBtn('Code Smells 8').get()); + await user.click( + ui.measureBtn('component_measures.metric.maintainability_issues.name 2').get(), + ); expect( - within(ui.measuresRow('folderA').get()).getByRole('cell', { name: '3' }), + within(ui.measuresRow('folderA').get()).getByRole('cell', { name: '2' }), ).toBeInTheDocument(); expect( within(ui.measuresRow('test1.js').get()).getByRole('cell', { name: '2' }), @@ -315,7 +355,7 @@ describe('navigation', () => { await user.click(ui.fileLink('folderA').get()); expect( - within(ui.measuresRow('out.tsx').get()).getByRole('cell', { name: '1' }), + within(ui.measuresRow('out.tsx').get()).getByRole('cell', { name: '2' }), ).toBeInTheDocument(); expect( within(ui.measuresRow('in.tsx').get()).getByRole('cell', { name: '2' }), @@ -336,11 +376,13 @@ describe('navigation', () => { await ui.appLoaded(); await user.click(ui.maintainabilityDomainBtn.get()); - await user.click(ui.measureBtn('Code Smells 8').get()); + await user.click( + ui.measureBtn('component_measures.metric.maintainability_issues.name 2').get(), + ); await waitFor(() => ui.changeViewToList()); expect( - within(await ui.measuresRow('out.tsx').find()).getByRole('cell', { name: '1' }), + within(await ui.measuresRow('out.tsx').find()).getByRole('cell', { name: '2' }), ).toBeInTheDocument(); expect( within(ui.measuresRow('test1.js').get()).getByRole('cell', { name: '2' }), @@ -378,13 +420,15 @@ describe('navigation', () => { // Drilldown to the file level. await user.click(ui.maintainabilityDomainBtn.get()); - await user.click(ui.measureBtn('Code Smells 8').get()); + await user.click( + ui.measureBtn('component_measures.metric.maintainability_issues.name 2').get(), + ); await ui.arrowDown(); // Select the 1st element ("folderA") await ui.arrowRight(); // Open "folderA" expect( - within(ui.measuresRow('out.tsx').get()).getByRole('cell', { name: '1' }), + within(ui.measuresRow('out.tsx').get()).getByRole('cell', { name: '2' }), ).toBeInTheDocument(); expect( within(ui.measuresRow('in.tsx').get()).getByRole('cell', { name: '2' }), @@ -394,7 +438,7 @@ describe('navigation', () => { await ui.arrowLeft(); // Close "folderA" expect( - within(ui.measuresRow('folderA').get()).getByRole('cell', { name: '3' }), + within(ui.measuresRow('folderA').get()).getByRole('cell', { name: '2' }), ).toBeInTheDocument(); await ui.arrowRight(); // Open "folderA" @@ -416,18 +460,44 @@ describe('redirects', () => { }); it('should redirect old metric route', async () => { + measuresHandler.deleteComponentMeasure('foo', MetricKey.maintainability_issues); + measuresHandler.deleteComponentMeasure('foo', MetricKey.new_maintainability_issues); + const { ui } = getPageObject(); renderMeasuresApp('component_measures/metric/bugs?id=foo'); await ui.appLoaded(); - expect(ui.measureBtn('Bugs 0').get()).toHaveAttribute('aria-current', 'true'); + expect(ui.measureBtn('component_measures.metric.bugs.name 0').get()).toHaveAttribute( + 'aria-current', + 'true', + ); + }); + + it('should redirect old metric route for software qualities', async () => { + const { ui } = getPageObject(); + renderMeasuresApp('component_measures/metric/security_issues?id=foo'); + await ui.appLoaded(); + expect(ui.measureBtn('component_measures.metric.security_issues.name 1').get()).toHaveAttribute( + 'aria-current', + 'true', + ); }); it('should redirect old domain route', async () => { + measuresHandler.deleteComponentMeasure('foo', MetricKey.maintainability_issues); + measuresHandler.deleteComponentMeasure('foo', MetricKey.new_maintainability_issues); + const { ui } = getPageObject(); renderMeasuresApp('component_measures/domain/bugs?id=foo'); await ui.appLoaded(); expect(ui.reliabilityDomainBtn.get()).toHaveAttribute('aria-expanded', 'true'); }); + + it('should redirect old domain route for software qualities', async () => { + const { ui } = getPageObject(); + renderMeasuresApp('component_measures/domain/reliability_issues?id=foo'); + await ui.appLoaded(); + expect(ui.reliabilityDomainBtn.get()).toHaveAttribute('aria-expanded', 'true'); + }); }); it('should allow to load more components', async () => { diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx index e0ea9d3461b..72b5e0bbdd7 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx @@ -21,6 +21,7 @@ import { withTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { Spinner } from '@sonarsource/echoes-react'; import { + FlagMessage, LargeCenteredLayout, Note, PageContentFontWrapper, @@ -33,12 +34,15 @@ import { Helmet } from 'react-helmet-async'; import { getMeasuresWithPeriod } from '../../../api/measures'; import { getAllMetrics } from '../../../api/metrics'; import { ComponentContext } from '../../../app/components/componentContext/ComponentContext'; +import HelpTooltip from '../../../components/controls/HelpTooltip'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; import { enhanceMeasure } from '../../../components/measure/utils'; import '../../../components/search-navigator.css'; +import AnalysisMissingInfoMessage from '../../../components/shared/AnalysisMissingInfoMessage'; import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; +import { areLeakAndOverallCCTMeasuresComputed } from '../../../helpers/measures'; import { WithBranchLikesProps, useBranchesQuery } from '../../../queries/branch'; import { ComponentQualifier, isPortfolioLike } from '../../../types/component'; import { MeasurePageView } from '../../../types/measures'; @@ -134,7 +138,10 @@ class ComponentMeasuresApp extends React.PureComponent { fetchMeasures(metrics: State['metrics']) { const { branchLike } = this.props; const query = parseQuery(this.props.location.query); - const componentKey = query.selected || this.props.component.key; + const componentKey = + query.selected !== undefined && query.selected !== '' + ? query.selected + : this.props.component.key; const filteredKeys = getMeasuresPageMetricKeys(metrics, branchLike); @@ -285,15 +292,25 @@ class ComponentMeasuresApp extends React.PureComponent { {measures.length > 0 ? (
+ {!canBrowseAllChildProjects && isPortfolioLike(qualifier) && ( + + {translate('component_measures.not_all_measures_are_shown')} + + + )} + {!areLeakAndOverallCCTMeasuresComputed(measures) && ( + + )} {this.renderContent(displayOverview, query, metric)}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx index ae408fbdccd..6c31247d817 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx @@ -29,7 +29,7 @@ import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-li import { getComponentMeasureUniqueKey } from '../../../helpers/component'; import { KeyboardKeys } from '../../../helpers/keycodes'; import { translate } from '../../../helpers/l10n'; -import { isDiffMetric } from '../../../helpers/measures'; +import { getCCTMeasureValue, isDiffMetric } from '../../../helpers/measures'; import { RequestData } from '../../../helpers/request'; import { isDefined } from '../../../helpers/types'; import { getProjectUrl } from '../../../helpers/urls'; @@ -94,8 +94,14 @@ export default class MeasureContent extends React.PureComponent { } componentDidUpdate(prevProps: Props) { - const prevComponentKey = prevProps.selected || prevProps.rootComponent.key; - const componentKey = this.props.selected || this.props.rootComponent.key; + const prevComponentKey = + prevProps.selected !== undefined && prevProps.selected !== '' + ? prevProps.selected + : prevProps.rootComponent.key; + const componentKey = + this.props.selected !== undefined && this.props.selected !== '' + ? this.props.selected + : this.props.rootComponent.key; if ( prevComponentKey !== componentKey || !isSameBranchLike(prevProps.branchLike, this.props.branchLike) || @@ -116,7 +122,7 @@ export default class MeasureContent extends React.PureComponent { const { metricKeys, opts, strategy } = this.getComponentRequestParams(view, requestedMetric, { ...(asc !== undefined && { asc }), }); - const componentKey = selected || rootComponent.key; + const componentKey = selected !== undefined && selected !== '' ? selected : rootComponent.key; const baseComponentMetrics = [requestedMetric.key]; if (requestedMetric.key === MetricKey.ncloc) { baseComponentMetrics.push(MetricKey.ncloc_language_distribution); @@ -347,8 +353,10 @@ export default class MeasureContent extends React.PureComponent { return null; } - const measureValue = + const rawMeasureValue = measure && (isDiffMetric(measure.metric) ? measure.period?.value : measure.value); + const measureValue = getCCTMeasureValue(metric.key, rawMeasureValue); + const isFileComponent = isFile(baseComponent.qualifier); const selectedIdx = this.getSelectedIndex(); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx index 0d784992527..a6790e2af7a 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx @@ -17,8 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { LinkStandalone } from '@sonarsource/echoes-react'; import classNames from 'classnames'; -import { Link, MetricsLabel, MetricsRatingBadge } from 'design-system'; +import { MetricsLabel, MetricsRatingBadge } from 'design-system'; import * as React from 'react'; import LanguageDistribution from '../../../components/charts/LanguageDistribution'; import Tooltip from '../../../components/controls/Tooltip'; @@ -30,7 +31,7 @@ import { BranchLike } from '../../../types/branch-like'; import { ComponentQualifier } from '../../../types/component'; import { MetricKey, MetricType } from '../../../types/metrics'; import { ComponentMeasure, Metric, Period, Measure as TypeMeasure } from '../../../types/types'; -import { hasFullMeasures } from '../utils'; +import { getMetricSubnavigationName, hasFullMeasures } from '../utils'; import LeakPeriodLegend from './LeakPeriodLegend'; interface Props { @@ -42,7 +43,7 @@ interface Props { secondaryMeasure?: TypeMeasure; } -export default function MeasureHeader(props: Props) { +export default function MeasureHeader(props: Readonly) { const { branchLike, component, leakPeriod, measureValue, metric, secondaryMeasure } = props; const isDiff = isDiffMetric(metric.key); const hasHistory = @@ -53,11 +54,13 @@ export default function MeasureHeader(props: Props) { ComponentQualifier.Project, ].includes(component.qualifier as ComponentQualifier) && hasFullMeasures(branchLike); const displayLeak = hasFullMeasures(branchLike); + const title = getMetricSubnavigationName(metric, getLocalizedMetricName, isDiff); + return (
- {getLocalizedMetricName(metric)} + {title}
- {translate('component_measures.see_metric_history')} - + )} diff --git a/server/sonar-web/src/main/js/apps/component-measures/config/domains.ts b/server/sonar-web/src/main/js/apps/component-measures/config/domains.ts index f653b0cde0e..b9c6fcd8f3e 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/config/domains.ts +++ b/server/sonar-web/src/main/js/apps/component-measures/config/domains.ts @@ -23,16 +23,21 @@ interface Domains { [domain: string]: { categories?: string[]; order: string[] }; } +const NEW_CODE_CATEGORY = 'new_code_category'; +const OVERALL_CATEGORY = 'overall_category'; + export const domains: Domains = { Reliability: { - categories: ['new_code_category', 'overall_category'], + categories: [NEW_CODE_CATEGORY, OVERALL_CATEGORY], order: [ - 'new_code_category', + NEW_CODE_CATEGORY, + MetricKey.new_reliability_issues, MetricKey.new_bugs, MetricKey.new_reliability_rating, MetricKey.new_reliability_remediation_effort, - 'overall_category', + OVERALL_CATEGORY, + MetricKey.reliability_issues, MetricKey.bugs, MetricKey.reliability_rating, MetricKey.reliability_remediation_effort, @@ -40,14 +45,16 @@ export const domains: Domains = { }, Security: { - categories: ['new_code_category', 'overall_category'], + categories: [NEW_CODE_CATEGORY, OVERALL_CATEGORY], order: [ - 'new_code_category', + NEW_CODE_CATEGORY, + MetricKey.new_security_issues, MetricKey.new_vulnerabilities, MetricKey.new_security_rating, MetricKey.new_security_remediation_effort, - 'overall_category', + OVERALL_CATEGORY, + MetricKey.security_issues, MetricKey.vulnerabilities, MetricKey.security_rating, MetricKey.security_remediation_effort, @@ -55,14 +62,14 @@ export const domains: Domains = { }, SecurityReview: { - categories: ['new_code_category', 'overall_category'], + categories: [NEW_CODE_CATEGORY, OVERALL_CATEGORY], order: [ - 'new_code_category', + NEW_CODE_CATEGORY, MetricKey.new_security_hotspots, MetricKey.new_security_review_rating, MetricKey.new_security_hotspots_reviewed, - 'overall_category', + OVERALL_CATEGORY, MetricKey.security_hotspots, MetricKey.security_review_rating, MetricKey.security_hotspots_reviewed, @@ -70,15 +77,17 @@ export const domains: Domains = { }, Maintainability: { - categories: ['new_code_category', 'overall_category'], + categories: [NEW_CODE_CATEGORY, OVERALL_CATEGORY], order: [ - 'new_code_category', + NEW_CODE_CATEGORY, + MetricKey.new_maintainability_issues, MetricKey.new_code_smells, MetricKey.new_technical_debt, MetricKey.new_sqale_debt_ratio, MetricKey.new_maintainability_rating, - 'overall_category', + OVERALL_CATEGORY, + MetricKey.maintainability_issues, MetricKey.code_smells, MetricKey.sqale_index, MetricKey.sqale_debt_ratio, @@ -88,9 +97,9 @@ export const domains: Domains = { }, Coverage: { - categories: ['new_code_category', 'overall_category', 'tests_category'], + categories: [NEW_CODE_CATEGORY, OVERALL_CATEGORY, 'tests_category'], order: [ - 'new_code_category', + NEW_CODE_CATEGORY, MetricKey.new_coverage, MetricKey.new_lines_to_cover, MetricKey.new_uncovered_lines, @@ -99,7 +108,7 @@ export const domains: Domains = { MetricKey.new_uncovered_conditions, MetricKey.new_branch_coverage, - 'overall_category', + OVERALL_CATEGORY, MetricKey.coverage, MetricKey.lines_to_cover, MetricKey.uncovered_lines, @@ -119,14 +128,14 @@ export const domains: Domains = { }, Duplications: { - categories: ['new_code_category', 'overall_category'], + categories: [NEW_CODE_CATEGORY, OVERALL_CATEGORY], order: [ - 'new_code_category', + NEW_CODE_CATEGORY, MetricKey.new_duplicated_lines_density, MetricKey.new_duplicated_lines, MetricKey.new_duplicated_blocks, - 'overall_category', + OVERALL_CATEGORY, MetricKey.duplicated_lines_density, MetricKey.duplicated_lines, MetricKey.duplicated_blocks, @@ -157,23 +166,16 @@ export const domains: Domains = { }, Issues: { + categories: [NEW_CODE_CATEGORY, OVERALL_CATEGORY], order: [ + NEW_CODE_CATEGORY, MetricKey.new_violations, - MetricKey.new_blocker_violations, - MetricKey.new_critical_violations, - MetricKey.new_major_violations, - MetricKey.new_minor_violations, - MetricKey.new_info_violations, + MetricKey.new_accepted_issues, + OVERALL_CATEGORY, MetricKey.violations, - MetricKey.blocker_violations, - MetricKey.critical_violations, - MetricKey.major_violations, - MetricKey.minor_violations, - MetricKey.info_violations, - MetricKey.open_issues, - MetricKey.reopened_issues, MetricKey.confirmed_issues, + MetricKey.accepted_issues, MetricKey.false_positive_issues, ], }, diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.tsx index dc2a073033a..56a1ac29744 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.tsx @@ -21,7 +21,7 @@ import { MetricsLabel, MetricsRatingBadge, NumericalCell } from 'design-system'; import * as React from 'react'; import Measure from '../../../components/measure/Measure'; import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; +import { formatMeasure, getCCTMeasureValue, isDiffMetric } from '../../../helpers/measures'; import { MetricType } from '../../../types/metrics'; import { ComponentMeasureEnhanced, MeasureEnhanced, Metric } from '../../../types/types'; @@ -35,7 +35,8 @@ export default function MeasureCell({ component, measure, metric }: Props) { const getValue = (item: { leak?: string; value?: string }) => isDiffMetric(metric.key) ? item.leak : item.value; - const value = getValue(measure || component); + const rawValue = getValue(measure || component); + const value = getCCTMeasureValue(metric.key, rawValue); return ( diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigation.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigation.tsx index 0d90caaf923..685e3c0d9c6 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigation.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigation.tsx @@ -34,7 +34,12 @@ import { translate, } from '../../../helpers/l10n'; import { MeasureEnhanced } from '../../../types/types'; -import { addMeasureCategories, hasBubbleChart, sortMeasures } from '../utils'; +import { + addMeasureCategories, + getMetricSubnavigationName, + hasBubbleChart, + sortMeasures, +} from '../utils'; import DomainSubnavigationItem from './DomainSubnavigationItem'; interface Props { @@ -45,7 +50,7 @@ interface Props { showFullMeasures: boolean; } -export default function DomainSubnavigation(props: Props) { +export default function DomainSubnavigation(props: Readonly) { const { domain, onChange, open, selected, showFullMeasures } = props; const helperMessageKey = `component_measures.domain_subnavigation.${domain.name}.help`; const helper = hasMessage(helperMessageKey) ? translate(helperMessageKey) : undefined; @@ -100,7 +105,7 @@ export default function DomainSubnavigation(props: Props) { diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigationItem.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigationItem.tsx index a9070e7c981..7bd33db86cd 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigationItem.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigationItem.tsx @@ -29,7 +29,12 @@ interface Props { selected: string; } -export default function DomainSubnavigationItem({ measure, name, onChange, selected }: Props) { +export default function DomainSubnavigationItem({ + measure, + name, + onChange, + selected, +}: Readonly) { const { key } = measure.metric; return ( diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx index 67ec7546d9b..d32cb1deea4 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx @@ -21,7 +21,6 @@ import { withTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { BareButton, - FlagMessage, LAYOUT_FOOTER_HEIGHT, LAYOUT_GLOBAL_NAV_HEIGHT, LAYOUT_PROJECT_NAV_HEIGHT, @@ -32,33 +31,24 @@ import { } from 'design-system'; import * as React from 'react'; import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget'; -import HelpTooltip from '../../../components/controls/HelpTooltip'; import { translate } from '../../../helpers/l10n'; import useFollowScroll from '../../../hooks/useFollowScroll'; -import { isPortfolioLike } from '../../../types/component'; import { MeasureEnhanced } from '../../../types/types'; -import { PROJECT_OVERVEW, Query, groupByDomains, isProjectOverview } from '../utils'; +import { PROJECT_OVERVEW, Query, isProjectOverview, populateDomainsFromMeasures } from '../utils'; import DomainSubnavigation from './DomainSubnavigation'; +import { Domain } from '../../../types/measures'; interface Props { - canBrowseAllChildProjects: boolean; measures: MeasureEnhanced[]; - qualifier: string; selectedMetric: string; showFullMeasures: boolean; updateQuery: (query: Partial) => void; } -export default function Sidebar(props: Props) { - const { - showFullMeasures, - canBrowseAllChildProjects, - qualifier, - updateQuery, - selectedMetric, - measures, - } = props; +export default function Sidebar(props: Readonly) { + const { showFullMeasures, updateQuery, selectedMetric, measures } = props; const { top: topScroll, scrolledOnce } = useFollowScroll(); + const domains = populateDomainsFromMeasures(measures); const handleChangeMetric = React.useCallback( (metric: string) => { @@ -89,15 +79,6 @@ export default function Sidebar(props: Props) { )`, }} > - {!canBrowseAllChildProjects && isPortfolioLike(qualifier) && ( - - {translate('component_measures.not_all_measures_are_shown')} - - - )}
- {groupByDomains(measures).map((domain: Domain) => ( + {domains.map((domain: Domain) => ( ) { const isDiff = isDiffMetric(measure.metric.key); const value = isDiff ? measure.leak : measure.value; const formatted = formatMeasure(value, MetricType.Rating); diff --git a/server/sonar-web/src/main/js/apps/component-measures/utils.ts b/server/sonar-web/src/main/js/apps/component-measures/utils.ts index 3d271fe3a61..fb947000929 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/utils.ts +++ b/server/sonar-web/src/main/js/apps/component-measures/utils.ts @@ -20,9 +20,22 @@ import { groupBy, memoize, sortBy, toPairs } from 'lodash'; import { enhanceMeasure } from '../../components/measure/utils'; import { isBranch, isPullRequest } from '../../helpers/branch-like'; -import { HIDDEN_METRICS } from '../../helpers/constants'; -import { getLocalizedMetricName } from '../../helpers/l10n'; -import { MEASURES_REDIRECTION, getDisplayMetrics, isDiffMetric } from '../../helpers/measures'; +import { + CCT_SOFTWARE_QUALITY_METRICS, + HIDDEN_METRICS, + LEAK_CCT_SOFTWARE_QUALITY_METRICS, + LEAK_OLD_TAXONOMY_METRICS, + OLD_TAXONOMY_METRICS, +} from '../../helpers/constants'; +import { getLocalizedMetricName, translate } from '../../helpers/l10n'; +import { + MEASURES_REDIRECTION, + areLeakCCTMeasuresComputed, + areCCTMeasuresComputed, + getDisplayMetrics, + isDiffMetric, + getCCTMeasureValue, +} from '../../helpers/measures'; import { cleanQuery, parseAsOptionalBoolean, @@ -31,7 +44,7 @@ import { } from '../../helpers/query'; import { BranchLike } from '../../types/branch-like'; import { ComponentQualifier } from '../../types/component'; -import { MeasurePageView } from '../../types/measures'; +import { Domain, MeasurePageView } from '../../types/measures'; import { MetricKey, MetricType } from '../../types/metrics'; import { ComponentMeasure, @@ -51,16 +64,103 @@ export const DEFAULT_VIEW = MeasurePageView.tree; export const DEFAULT_METRIC = PROJECT_OVERVEW; export const KNOWN_DOMAINS = [ 'Releasability', - 'Reliability', 'Security', - 'SecurityReview', + 'Reliability', 'Maintainability', + 'SecurityReview', 'Coverage', 'Duplications', 'Size', 'Complexity', ]; +const CCT_METRIC_DOMAIN_MAP: Dict = { + [MetricKey.security_issues]: 'Security', + [MetricKey.new_security_issues]: 'Security', + [MetricKey.reliability_issues]: 'Reliability', + [MetricKey.new_reliability_issues]: 'Reliability', + [MetricKey.maintainability_issues]: 'Maintainability', + [MetricKey.new_maintainability_issues]: 'Maintainability', +}; + +const DEPRECATED_METRICS = [ + MetricKey.blocker_violations, + MetricKey.new_blocker_violations, + MetricKey.critical_violations, + MetricKey.new_critical_violations, + MetricKey.major_violations, + MetricKey.new_major_violations, + MetricKey.info_violations, + MetricKey.new_info_violations, + MetricKey.minor_violations, + MetricKey.new_minor_violations, + MetricKey.high_impact_accepted_issues, +]; + +const ISSUES_METRICS = [ + MetricKey.accepted_issues, + MetricKey.new_accepted_issues, + MetricKey.confirmed_issues, + MetricKey.false_positive_issues, + MetricKey.violations, + MetricKey.new_violations, +]; + +export const populateDomainsFromMeasures = memoize((measures: MeasureEnhanced[]): Domain[] => { + let populatedMeasures = measures + .filter((measure) => !DEPRECATED_METRICS.includes(measure.metric.key as MetricKey)) + .map((measure) => { + const isDiff = isDiffMetric(measure.metric.key); + const calculatedValue = getCCTMeasureValue( + measure.metric.key, + isDiff ? measure.leak : measure.value, + ); + + return { + ...measure, + metric: { + ...measure.metric, + domain: CCT_METRIC_DOMAIN_MAP[measure.metric.key] ?? measure.metric.domain, + }, + ...(!isDiff && { value: calculatedValue }), + ...(isDiff && { leak: calculatedValue }), + }; + }); + if (areLeakCCTMeasuresComputed(measures)) { + populatedMeasures = populatedMeasures.filter( + (measure) => !LEAK_OLD_TAXONOMY_METRICS.includes(measure.metric.key as MetricKey), + ); + } + if (areCCTMeasuresComputed(measures)) { + populatedMeasures = populatedMeasures.filter( + (measure) => !OLD_TAXONOMY_METRICS.includes(measure.metric.key as MetricKey), + ); + } + + return groupByDomains(populatedMeasures); +}); + +export function getMetricSubnavigationName( + metric: Metric, + translateFn: (metric: Metric) => string, + isDiff = false, +) { + if ( + [ + ...LEAK_CCT_SOFTWARE_QUALITY_METRICS, + ...CCT_SOFTWARE_QUALITY_METRICS, + ...ISSUES_METRICS, + ...OLD_TAXONOMY_METRICS, + ...LEAK_OLD_TAXONOMY_METRICS, + ].includes(metric.key as MetricKey) + ) { + return translate( + `component_measures.metric.${metric.key}.${isDiff ? 'detailed_name' : 'name'}`, + ); + } + return translateFn(metric); +} + export function filterMeasures(measures: MeasureEnhanced[]): MeasureEnhanced[] { return measures.filter((measure) => !HIDDEN_METRICS.includes(measure.metric.key as MetricKey)); } diff --git a/server/sonar-web/src/main/js/helpers/constants.ts b/server/sonar-web/src/main/js/helpers/constants.ts index 809bc472756..193034eaaab 100644 --- a/server/sonar-web/src/main/js/helpers/constants.ts +++ b/server/sonar-web/src/main/js/helpers/constants.ts @@ -92,12 +92,24 @@ export const CCT_SOFTWARE_QUALITY_METRICS = [ MetricKey.maintainability_issues, ]; +export const LEAK_CCT_SOFTWARE_QUALITY_METRICS = [ + MetricKey.new_security_issues, + MetricKey.new_reliability_issues, + MetricKey.new_maintainability_issues, +]; + export const OLD_TAXONOMY_METRICS = [ MetricKey.vulnerabilities, MetricKey.bugs, MetricKey.code_smells, ]; +export const LEAK_OLD_TAXONOMY_METRICS = [ + MetricKey.new_vulnerabilities, + MetricKey.new_bugs, + MetricKey.new_code_smells, +]; + export const OLD_TO_NEW_TAXONOMY_METRICS_MAP: { [key in MetricKey]?: MetricKey } = { [MetricKey.vulnerabilities]: MetricKey.security_issues, [MetricKey.bugs]: MetricKey.reliability_issues, diff --git a/server/sonar-web/src/main/js/helpers/measures.ts b/server/sonar-web/src/main/js/helpers/measures.ts index f8cef7ad0ae..33412639614 100644 --- a/server/sonar-web/src/main/js/helpers/measures.ts +++ b/server/sonar-web/src/main/js/helpers/measures.ts @@ -23,7 +23,11 @@ import { QualityGateStatusConditionEnhanced, } from '../types/quality-gates'; import { Dict, Measure, MeasureEnhanced, Metric } from '../types/types'; -import { CCT_SOFTWARE_QUALITY_METRICS, ONE_SECOND } from './constants'; +import { + CCT_SOFTWARE_QUALITY_METRICS, + LEAK_CCT_SOFTWARE_QUALITY_METRICS, + ONE_SECOND, +} from './constants'; import { translate, translateWithParameters } from './l10n'; import { getCurrentLocale } from './l10nBundle'; import { isDefined } from './types'; @@ -72,13 +76,28 @@ export function isDiffMetric(metricKey: MetricKey | string): boolean { } export function getDisplayMetrics(metrics: Metric[]) { - return metrics.filter((metric) => !metric.hidden && !['DATA', 'DISTRIB'].includes(metric.type)); + 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[]) { + 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) => @@ -87,10 +106,26 @@ export function areCCTMeasuresComputed(measures?: Measure[] | MeasureEnhanced[]) ); } +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) => string; diff --git a/server/sonar-web/src/main/js/helpers/mocks/metrics.ts b/server/sonar-web/src/main/js/helpers/mocks/metrics.ts index 75a97dcabbd..a909cef1232 100644 --- a/server/sonar-web/src/main/js/helpers/mocks/metrics.ts +++ b/server/sonar-web/src/main/js/helpers/mocks/metrics.ts @@ -912,11 +912,20 @@ export const DEFAULT_METRICS: Dict = { }, reliability_issues: { key: 'reliability_issues', - type: 'INT', + type: 'DATA', name: 'Reliability', description: 'Reliability issues', - direction: -1, - qualitative: true, + direction: 0, + qualitative: false, + hidden: false, + }, + new_reliability_issues: { + key: 'new_reliability_issues', + type: 'DATA', + name: 'New Reliability', + description: 'New Reliability issues', + direction: 0, + qualitative: false, hidden: false, }, reliability_rating: { @@ -1023,11 +1032,21 @@ export const DEFAULT_METRICS: Dict = { }, security_issues: { key: 'security_issues', - type: 'INT', + type: 'DATA', name: 'Security', description: 'Security issues', - direction: -1, - qualitative: true, + direction: 0, + qualitative: false, + hidden: false, + }, + new_security_issues: { + key: 'new_security_issues', + type: 'DATA', + name: 'Security', + description: 'New Security issues', + domain: 'Issues', + direction: 0, + qualitative: false, hidden: false, }, security_rating: { @@ -1192,11 +1211,22 @@ export const DEFAULT_METRICS: Dict = { }, maintainability_issues: { key: 'maintainability_issues', - type: 'INT', + type: 'DATA', name: 'Maintainability', description: 'Maintainability issues', - direction: -1, - qualitative: true, + domain: 'Issues', + direction: 0, + qualitative: false, + hidden: false, + }, + new_maintainability_issues: { + key: 'new_maintainability_issues', + type: 'DATA', + name: 'Maintainability', + description: 'New Maintainability issues', + domain: 'Issues', + direction: 0, + qualitative: false, hidden: false, }, sqale_index: { @@ -1389,4 +1419,14 @@ export const DEFAULT_METRICS: Dict = { qualitative: false, hidden: false, }, + new_accepted_issues: { + key: 'new_accepted_issues', + type: 'INT', + name: 'New Accepted Issues', + description: 'New Accepted issues', + domain: 'Issues', + direction: -1, + qualitative: false, + hidden: false, + }, }; diff --git a/server/sonar-web/src/main/js/types/measures.ts b/server/sonar-web/src/main/js/types/measures.ts index 3fb80310940..57610cf7935 100644 --- a/server/sonar-web/src/main/js/types/measures.ts +++ b/server/sonar-web/src/main/js/types/measures.ts @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { ComponentMeasure, Metric, Period, PeriodMeasure } from './types'; +import { ComponentMeasure, MeasureEnhanced, Metric, Period, PeriodMeasure } from './types'; export interface MeasuresForProjects { component: string; @@ -41,3 +41,8 @@ export enum MeasurePageView { tree = 'tree', treemap = 'treemap', } + +export interface Domain { + measures: MeasureEnhanced[]; + name: string; +} diff --git a/server/sonar-web/src/main/js/types/metrics.ts b/server/sonar-web/src/main/js/types/metrics.ts index c59e1aadaf4..ca94b3dde1e 100644 --- a/server/sonar-web/src/main/js/types/metrics.ts +++ b/server/sonar-web/src/main/js/types/metrics.ts @@ -93,15 +93,18 @@ export enum MetricKey { new_line_coverage = 'new_line_coverage', new_lines = 'new_lines', new_lines_to_cover = 'new_lines_to_cover', + new_maintainability_issues = 'new_maintainability_issues', new_maintainability_rating = 'new_maintainability_rating', new_maintainability_rating_distribution = 'new_maintainability_rating_distribution', new_major_violations = 'new_major_violations', new_minor_violations = 'new_minor_violations', + new_reliability_issues = 'new_reliability_issues', new_reliability_rating = 'new_reliability_rating', new_reliability_remediation_effort = 'new_reliability_remediation_effort', new_reliability_rating_distribution = 'new_reliability_rating_distribution', new_security_hotspots = 'new_security_hotspots', new_security_hotspots_reviewed = 'new_security_hotspots_reviewed', + new_security_issues = 'new_security_issues', new_security_rating = 'new_security_rating', new_security_rating_distribution = 'new_security_rating_distribution', new_security_remediation_effort = 'new_security_remediation_effort', diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index b92c78759ad..7f39eea439a 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -4180,6 +4180,33 @@ component_measures.bubble_chart.zoom_level=Current zoom level. Scroll on the cha component_measures.not_all_measures_are_shown=Not all projects and applications are included component_measures.not_all_measures_are_shown.help=You do not have access to all projects and/or applications. Measures are still computed based on all projects and applications. +component_measures.metric.new_security_issues.name=Issues +component_measures.metric.new_security_issues.detailed_name=New Issues +component_measures.metric.new_vulnerabilities.name=Issues +component_measures.metric.new_vulnerabilities.detailed_name=New Issues +component_measures.metric.new_reliability_issues.name=Issues +component_measures.metric.new_reliability_issues.detailed_name=New Issues +component_measures.metric.new_maintainability_issues.name=Issues +component_measures.metric.new_maintainability_issues.detailed_name=New Issues +component_measures.metric.new_code_smells.name=Issues +component_measures.metric.new_code_smells.detailed_name=New Issues +component_measures.metric.new_violations.name=Open Issues +component_measures.metric.new_violations.detailed_name=New Open Issues +component_measures.metric.new_accepted_issues.name=Accepted Issues +component_measures.metric.new_accepted_issues.detailed_name=New Accepted Issues +component_measures.metric.new_bugs.name=Issues +component_measures.metric.new_bugs.detailed_name=New Issues +component_measures.metric.security_issues.name=Issues +component_measures.metric.vulnerabilities.name=Issues +component_measures.metric.reliability_issues.name=Issues +component_measures.metric.bugs.name=Issues +component_measures.metric.maintainability_issues.name=Issues +component_measures.metric.code_smells.name=Issues +component_measures.metric.violations.name=Open Issues +component_measures.metric.accepted_issues.name=Accepted Issues +component_measures.metric.confirmed_issues.name=Confirmed Issues +component_measures.metric.false_positive_issues.name=False Positive Issues + #------------------------------------------------------------------------------ # # DOCS