From: Ismail Cherri Date: Thu, 7 Mar 2024 20:08:35 +0000 (-0600) Subject: SONAR-21767 Project overview show old count when analysis data is missing X-Git-Tag: 10.5.0.89998~142 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=92f48cb48753902d5356deb57c3b13f2a1c59370;p=sonarqube.git SONAR-21767 Project overview show old count when analysis data is missing --- diff --git a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx index c56c808d48b..5faad7c2615 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx @@ -19,21 +19,20 @@ */ import { BasicSeparator, - FlagMessage, LargeCenteredLayout, LightGreyCard, PageContentFontWrapper, } from 'design-system'; import * as React from 'react'; -import { useIntl } from 'react-intl'; import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget'; import { useLocation, useRouter } from '../../../components/hoc/withRouter'; +import AnalysisMissingInfoMessage from '../../../components/shared/AnalysisMissingInfoMessage'; import { parseDate } from '../../../helpers/dates'; import { isDiffMetric } from '../../../helpers/measures'; import { CodeScope } from '../../../helpers/urls'; import { ApplicationPeriod } from '../../../types/application'; import { Branch } from '../../../types/branch-like'; -import { ComponentQualifier, isApplication } from '../../../types/component'; +import { ComponentQualifier } from '../../../types/component'; import { MetricKey } from '../../../types/metrics'; import { Analysis, GraphType, MeasureHistory } from '../../../types/project-activity'; import { QualityGateStatus } from '../../../types/quality-gates'; @@ -94,18 +93,17 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp const { query } = useLocation(); const router = useRouter(); - const intl = useIntl(); const tab = query.codeScope === CodeScope.Overall ? CodeScope.Overall : CodeScope.New; const leakPeriod = component.qualifier === ComponentQualifier.Application ? appLeak : period; const isNewCodeTab = tab === CodeScope.New; const hasNewCodeMeasures = measures.some((m) => isDiffMetric(m.metric.key)); // Check if any potentially missing uncomputed measure is not present - const isMissingMeasures = ( - isNewCodeTab - ? [MetricKey.new_accepted_issues] - : [MetricKey.security_issues, MetricKey.maintainability_issues, MetricKey.reliability_issues] - ).some((key) => !measures.find((measure) => measure.metric.key === key)); + const isMissingMeasures = [ + MetricKey.security_issues, + MetricKey.maintainability_issues, + MetricKey.reliability_issues, + ].some((key) => !measures.find((measure) => measure.metric.key === key)); const selectTab = (tab: CodeScope) => { router.replace({ query: { ...query, codeScope: tab } }); @@ -121,14 +119,9 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [loadingStatus, hasNewCodeMeasures]); - const appReanalysisWarning = - isMissingMeasures && isApplication(component.qualifier) ? ( - - {intl.formatMessage({ - id: 'overview.missing_project_data.APP', - })} - - ) : null; + const analysisMissingInfo = isMissingMeasures && ( + + ); return ( <> @@ -184,15 +177,12 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp {isNewCodeTab && ( <> {hasNewCodeMeasures ? ( - <> - {appReanalysisWarning} - - + ) : ( - {appReanalysisWarning} + {analysisMissingInfo} m.metric.key === metricKey); const measure = JSON.parse(measureRaw?.value ?? 'null') as SoftwareImpactMeasureData; - - const renderDisabled = !measure || component.needIssueSync; + const alternativeMeasure = measures.find( + (m) => m.metric.key === SOFTWARE_QUALITIES_METRIC_KEYS_MAP[softwareQuality].deprecatedMetric, + ); // Find rating measure const ratingMeasure = measures.find((m) => m.metric.key === ratingMetricKey); + const count = measure?.total ?? alternativeMeasure?.value; + const totalLinkHref = getComponentIssuesUrl(component.key, { ...DEFAULT_ISSUES_QUERY, - impactSoftwareQualities: softwareQuality, + ...(isDefined(measure) + ? { impactSoftwareQualities: softwareQuality } + : { types: getIssueTypeBySoftwareQuality(softwareQuality) }), branch: branch?.name, }); @@ -99,30 +103,30 @@ export function SoftwareImpactMeasureCard(props: Readonly
- {measure ? ( + {count ? ( - - {formatMeasure(measure.total, MetricType.ShortInteger)} - + {formatMeasure(count, MetricType.ShortInteger)} + ) : ( @@ -139,36 +143,26 @@ export function SoftwareImpactMeasureCard(props: Readonly
-
- {[ - SoftwareImpactSeverity.High, - SoftwareImpactSeverity.Medium, - SoftwareImpactSeverity.Low, - ].map((severity) => ( - - ))} -
+ {measure && ( +
+ {[ + SoftwareImpactSeverity.High, + SoftwareImpactSeverity.Medium, + SoftwareImpactSeverity.Low, + ].map((severity) => ( + + ))} +
+ )} - {!measure && ( - <> - - - - {intl.formatMessage({ - id: `overview.run_analysis_to_compute.${component.qualifier}`, - })} - - - - )} ); } @@ -177,8 +171,4 @@ const StyledDash = styled(TextBold)` font-size: 36px; `; -const StyledInfoSection = styled.div` - background-color: ${themeColor('overviewSoftwareImpactSeverityNeutral')}; -`; - export default SoftwareImpactMeasureCard; diff --git a/server/sonar-web/src/main/js/apps/overview/branches/TabsPanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/TabsPanel.tsx index 489860ba459..de0ca9c9830 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/TabsPanel.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/TabsPanel.tsx @@ -17,16 +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 { Spinner } from '@sonarsource/echoes-react'; import { isBefore, sub } from 'date-fns'; -import { - BasicSeparator, - ButtonLink, - FlagMessage, - LightLabel, - PageTitle, - Spinner, - Tabs, -} from 'design-system'; +import { BasicSeparator, ButtonLink, FlagMessage, LightLabel, Tabs } from 'design-system'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import DocumentationLink from '../../../components/common/DocumentationLink'; @@ -131,15 +124,14 @@ export function TabsPanel(props: React.PropsWithChildren) { return (
-
- +
{loading ? (
- +
) : ( <> diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx index ba8015aa4c1..d4ffff4011f 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx @@ -37,7 +37,7 @@ import { mockAnalysis, mockAnalysisEvent } from '../../../../helpers/mocks/proje import { mockQualityGateProjectStatus } from '../../../../helpers/mocks/quality-gates'; import { mockLoggedInUser, mockMeasure, mockPaging } from '../../../../helpers/testMocks'; import { renderComponent } from '../../../../helpers/testReactTestingUtils'; -import { byRole, byText } from '../../../../helpers/testSelector'; +import { byLabelText, byRole, byText } from '../../../../helpers/testSelector'; import { SoftwareImpactSeverity, SoftwareQuality } from '../../../../types/clean-code-taxonomy'; import { ComponentQualifier } from '../../../../types/component'; import { MetricKey } from '../../../../types/metrics'; @@ -323,9 +323,11 @@ describe('project overview', () => { ); }); - it('should render missing software impact measure cards', async () => { - // Make as if reliability_issues was not computed + it('should render old measures if software impact are missing', async () => { + // Make as if new analysis after upgrade is missing measuresHandler.deleteComponentMeasure('foo', MetricKey.maintainability_issues); + measuresHandler.deleteComponentMeasure('foo', MetricKey.security_issues); + measuresHandler.deleteComponentMeasure('foo', MetricKey.reliability_issues); const { user, ui } = getPageObjects(); renderBranchOverview(); @@ -334,43 +336,91 @@ describe('project overview', () => { expect(await ui.softwareImpactMeasureCard(SoftwareQuality.Security).find()).toBeInTheDocument(); - ui.expectSoftwareImpactMeasureCard( + ui.expectSoftwareImpactMeasureCard(SoftwareQuality.Security); + ui.expectSoftwareImpactMeasureCardToHaveOldMeasures( SoftwareQuality.Security, 'B', - { - total: 1, - [SoftwareImpactSeverity.High]: 0, - [SoftwareImpactSeverity.Medium]: 1, - [SoftwareImpactSeverity.Low]: 0, - }, - [false, true, false], + 2, + 'VULNERABILITY', ); - ui.expectSoftwareImpactMeasureCard( - SoftwareQuality.Reliability, - 'A', - { - total: 3, - [SoftwareImpactSeverity.High]: 0, - [SoftwareImpactSeverity.Medium]: 2, - [SoftwareImpactSeverity.Low]: 1, - }, - [false, true, false], + + ui.expectSoftwareImpactMeasureCard(SoftwareQuality.Reliability); + ui.expectSoftwareImpactMeasureCardToHaveOldMeasures(SoftwareQuality.Reliability, 'A', 0, 'BUG'); + + ui.expectSoftwareImpactMeasureCard(SoftwareQuality.Maintainability); + ui.expectSoftwareImpactMeasureCardToHaveOldMeasures( + SoftwareQuality.Maintainability, + 'E', + 8, + 'CODE_SMELL', ); + }); + + it('should render missing software impact measure cards if both software qualities and old measures are missing', async () => { + // Make as if no measures at all + measuresHandler.deleteComponentMeasure('foo', MetricKey.maintainability_issues); + measuresHandler.deleteComponentMeasure('foo', MetricKey.code_smells); + + measuresHandler.deleteComponentMeasure('foo', MetricKey.security_issues); + measuresHandler.deleteComponentMeasure('foo', MetricKey.vulnerabilities); + + measuresHandler.deleteComponentMeasure('foo', MetricKey.reliability_issues); + measuresHandler.deleteComponentMeasure('foo', MetricKey.bugs); + + const { user, ui } = getPageObjects(); + renderBranchOverview(); + + await user.click(await ui.overallCodeButton.find()); + + expect(await ui.softwareImpactMeasureCard(SoftwareQuality.Security).find()).toBeInTheDocument(); + + expect(byText('-', { exact: true }).getAll()).toHaveLength(3); + + ui.expectSoftwareImpactMeasureCard(SoftwareQuality.Security); + expect( + byLabelText( + `overview.project.software_impact.has_rating.software_quality.${SoftwareQuality.Security}.B`, + ).get(), + ).toBeInTheDocument(); + + ui.expectSoftwareImpactMeasureCard(SoftwareQuality.Reliability); + expect( + byLabelText( + `overview.project.software_impact.has_rating.software_quality.${SoftwareQuality.Reliability}.A`, + ).get(), + ).toBeInTheDocument(); - // Maintainability is not computed ui.expectSoftwareImpactMeasureCard(SoftwareQuality.Maintainability); expect( - byText('overview.run_analysis_to_compute.TRK').get( - ui.softwareImpactMeasureCard(SoftwareQuality.Maintainability).get(), - ), + byLabelText( + `overview.project.software_impact.has_rating.software_quality.${SoftwareQuality.Maintainability}.E`, + ).get(), ).toBeInTheDocument(); - ui.expectSoftwareImpactMeasureCard(SoftwareQuality.Maintainability, undefined, undefined, [ - false, - false, - false, - ]); }); + it.each([ + ['security_issues', MetricKey.security_issues], + ['reliability_issues', MetricKey.reliability_issues], + ['maintainability_issues', MetricKey.maintainability_issues], + ])( + 'should display info about missing analysis if a project is not computed for %s', + async (missingMetricKey) => { + measuresHandler.deleteComponentMeasure('foo', missingMetricKey as MetricKey); + const { user, ui } = getPageObjects(); + renderBranchOverview(); + + await user.click(await ui.overallCodeButton.find()); + + expect( + await ui.softwareImpactMeasureCard(SoftwareQuality.Security).find(), + ).toBeInTheDocument(); + + await user.click(await ui.overallCodeButton.find()); + + expect(await screen.findByText('overview.missing_project_data.TRK')).toBeInTheDocument(); + }, + ); + it('should disable software impact measure card links during reindexing', async () => { const { user, ui } = getPageObjects(); renderBranchOverview({ @@ -528,17 +578,6 @@ describe('application overview', () => { expect(await screen.findByText('portfolio.app.empty')).toBeInTheDocument(); }); - it.each([['new_accepted_issues', MetricKey.new_accepted_issues]])( - 'should ask to reanalyze all projects if a project is not computed for %s', - async (missingMetricKey) => { - measuresHandler.deleteComponentMeasure('foo', missingMetricKey as MetricKey); - - renderBranchOverview({ component }); - - expect(await screen.findByText('overview.missing_project_data.APP')).toBeInTheDocument(); - }, - ); - it.each([ ['security_issues', MetricKey.security_issues], ['reliability_issues', MetricKey.reliability_issues], diff --git a/server/sonar-web/src/main/js/apps/overview/branches/test-utils.ts b/server/sonar-web/src/main/js/apps/overview/branches/test-utils.ts index d748b941b48..21925f6e066 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/test-utils.ts +++ b/server/sonar-web/src/main/js/apps/overview/branches/test-utils.ts @@ -100,6 +100,26 @@ export const getPageObjects = () => { ); } }, + expectSoftwareImpactMeasureCardToHaveOldMeasures: ( + softwareQuality: SoftwareQuality, + rating: string, + total: number, + oldMetric: string, + branch = 'master', + ) => { + const branchQuery = branch ? `&branch=${branch}` : ''; + expect( + byText(rating, { exact: true }).get(ui.softwareImpactMeasureCard(softwareQuality).get()), + ).toBeInTheDocument(); + expect( + byRole('link', { + name: `overview.measures.software_impact.see_list_of_x_open_issues.${total}.software_quality.${softwareQuality}`, + }).get(), + ).toHaveAttribute( + 'href', + `/project/issues?issueStatuses=OPEN%2CCONFIRMED&types=${oldMetric}${branchQuery}&id=foo`, + ); + }, expectSoftwareImpactMeasureBreakdownCard: ( softwareQuality: SoftwareQuality, severity: SoftwareImpactSeverity, diff --git a/server/sonar-web/src/main/js/components/shared/AnalysisMissingInfoMessage.tsx b/server/sonar-web/src/main/js/components/shared/AnalysisMissingInfoMessage.tsx new file mode 100644 index 00000000000..685f987470e --- /dev/null +++ b/server/sonar-web/src/main/js/components/shared/AnalysisMissingInfoMessage.tsx @@ -0,0 +1,54 @@ +/* + * 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 { FlagMessage } from 'design-system'; +import * as React from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; +import DocumentationLink from '../common/DocumentationLink'; + +interface AnalysisMissingInfoMessageProps { + qualifier: string; + className?: string; +} + +export default function AnalysisMissingInfoMessage({ + qualifier, + className, +}: Readonly) { + const intl = useIntl(); + + return ( + + + {intl.formatMessage({ id: 'learn_more' })} + + ), + }} + /> + + ); +} diff --git a/server/sonar-web/src/main/js/helpers/issues.ts b/server/sonar-web/src/main/js/helpers/issues.ts index bbccf072160..c62a9b29222 100644 --- a/server/sonar-web/src/main/js/helpers/issues.ts +++ b/server/sonar-web/src/main/js/helpers/issues.ts @@ -24,6 +24,7 @@ import { MetricKey } from '../types/metrics'; import { Dict, Flow, FlowLocation, FlowType, Issue, TextRange } from '../types/types'; import { UserBase } from '../types/users'; import { ISSUE_TYPES } from './constants'; +import { SoftwareQuality } from '../types/clean-code-taxonomy'; interface Rule {} @@ -163,6 +164,37 @@ export function parseIssueFromResponse( } as Issue; } +export function getIssueTypeBySoftwareQuality(quality: SoftwareQuality): IssueType { + const map = { + [SoftwareQuality.Maintainability]: IssueType.CodeSmell, + [SoftwareQuality.Security]: IssueType.Vulnerability, + [SoftwareQuality.Reliability]: IssueType.Bug, + }; + + return map[quality]; +} + +export const SOFTWARE_QUALITIES_METRIC_KEYS_MAP = { + [SoftwareQuality.Security]: { + metric: MetricKey.security_issues, + deprecatedMetric: MetricKey.vulnerabilities, + rating: MetricKey.security_rating, + newRating: MetricKey.new_security_rating, + }, + [SoftwareQuality.Reliability]: { + metric: MetricKey.reliability_issues, + deprecatedMetric: MetricKey.bugs, + rating: MetricKey.reliability_rating, + newRating: MetricKey.new_reliability_rating, + }, + [SoftwareQuality.Maintainability]: { + metric: MetricKey.maintainability_issues, + deprecatedMetric: MetricKey.code_smells, + rating: MetricKey.sqale_rating, + newRating: MetricKey.new_maintainability_rating, + }, +}; + export const ISSUETYPE_METRIC_KEYS_MAP = { [IssueType.CodeSmell]: { metric: MetricKey.code_smells, 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 c829ea2be0f..2092486d43a 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3929,7 +3929,6 @@ overview.accepted_issues=Accepted issues overview.accepted_issues.description=Issues that are valid, but were not fixed and represent accepted technical debt. overview.accepted_issues.total=Total accepted issues overview.high_impact_accepted_issues=High impact accepted issues -overview.measures=Measures overview.measures.empty_explanation=Measures on New Code will appear after the second analysis of this branch. overview.measures.empty_link={learn_more_link} about the Clean as You Code approach. overview.measures.same_reference.explanation=This branch is configured to use itself as reference branch. It will never have New Code. @@ -3969,7 +3968,8 @@ overview.project.next_steps.links.set_up_ci=set up analysis in your favorite CI overview.project.software_impact.has_rating=Software Quality {softwareQuality} has rating {rating} overview.run_analysis_to_compute.TRK=Run new analysis to compute the missing data. overview.run_analysis_to_compute.APP=Analyse all projects to compute the missing data. -overview.missing_project_data.APP=Some projects are missing data. All projects in the application need to be analysed to compute the missing data. +overview.missing_project_data.APP=The way Security, Reliability, and Maintainability are calculated has changed. These values may change after all projects in this application have been analyzed again. {learn_more} +overview.missing_project_data.TRK=The way Security, Reliability, and Maintainability are calculated has changed. These values may change after the next analysis. {learn_more} overview.coverage_on=Coverage on overview.coverage_on_X_lines=Coverage on {count} Lines to cover