Browse Source

SONAR-21767 Project overview show old count when analysis data is missing

tags/10.5.0.89998
Ismail Cherri 2 months ago
parent
commit
92f48cb487

+ 17
- 27
server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx View File

@@ -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) ? (
<FlagMessage variant="warning" className="sw-my-4">
{intl.formatMessage({
id: 'overview.missing_project_data.APP',
})}
</FlagMessage>
) : null;
const analysisMissingInfo = isMissingMeasures && (
<AnalysisMissingInfoMessage qualifier={component.qualifier} className="sw-mt-6" />
);

return (
<>
@@ -184,15 +177,12 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
{isNewCodeTab && (
<>
{hasNewCodeMeasures ? (
<>
{appReanalysisWarning}
<NewCodeMeasuresPanel
qgStatuses={qgStatuses}
branch={branch}
component={component}
measures={measures}
/>
</>
<NewCodeMeasuresPanel
qgStatuses={qgStatuses}
branch={branch}
component={component}
measures={measures}
/>
) : (
<MeasuresPanelNoNewCode
branch={branch}
@@ -205,7 +195,7 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp

{!isNewCodeTab && (
<>
{appReanalysisWarning}
{analysisMissingInfo}
<OverallCodeMeasuresPanel
branch={branch}
qgStatuses={qgStatuses}

+ 42
- 52
server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureCard.tsx View File

@@ -17,16 +17,10 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { LinkHighlight, LinkStandalone } from '@sonarsource/echoes-react';
import styled from '@emotion/styled';
import classNames from 'classnames';
import {
BasicSeparator,
LightGreyCard,
NakedLink,
TextBold,
TextSubdued,
themeColor,
} from 'design-system';
import { BasicSeparator, LightGreyCard, TextBold, TextSubdued } from 'design-system';
import * as React from 'react';
import { useIntl } from 'react-intl';
import Tooltip from '../../../components/controls/Tooltip';
@@ -45,6 +39,11 @@ import { OverviewDisabledLinkTooltip } from '../components/OverviewDisabledLinkT
import { softwareQualityToMeasure } from '../utils';
import SoftwareImpactMeasureBreakdownCard from './SoftwareImpactMeasureBreakdownCard';
import SoftwareImpactMeasureRating from './SoftwareImpactMeasureRating';
import { isDefined } from '../../../helpers/types';
import {
getIssueTypeBySoftwareQuality,
SOFTWARE_QUALITIES_METRIC_KEYS_MAP,
} from '../../../helpers/issues';

export interface SoftwareImpactBreakdownCardProps {
component: Component;
@@ -63,15 +62,20 @@ export function SoftwareImpactMeasureCard(props: Readonly<SoftwareImpactBreakdow
const metricKey = softwareQualityToMeasure(softwareQuality);
const measureRaw = measures.find((m) => 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<SoftwareImpactBreakdow
<div className="sw-flex sw-mt-2">
<div
className={classNames('sw-flex sw-gap-1 sw-items-center', {
'sw-opacity-60': renderDisabled,
'sw-opacity-60': component.needIssueSync,
})}
>
{measure ? (
{count ? (
<Tooltip overlay={countTooltipOverlay}>
<NakedLink
<LinkStandalone
data-testid={`overview__software-impact-${softwareQuality}`}
aria-label={intl.formatMessage(
{
id: `overview.measures.software_impact.see_list_of_x_open_issues`,
},
{
count: measure.total,
count,
softwareQuality: intl.formatMessage({
id: `software_quality.${softwareQuality}`,
}),
},
)}
className="sw-text-lg"
className="sw-text-lg sw-font-semibold"
highlight={LinkHighlight.CurrentColor}
to={totalLinkHref}
disabled={component.needIssueSync}
>
{formatMeasure(measure.total, MetricType.ShortInteger)}
</NakedLink>
{formatMeasure(count, MetricType.ShortInteger)}
</LinkStandalone>
</Tooltip>
) : (
<StyledDash className="sw-font-bold" name="-" />
@@ -139,36 +143,26 @@ export function SoftwareImpactMeasureCard(props: Readonly<SoftwareImpactBreakdow
/>
</div>
</div>
<div className="sw-flex sw-gap-2">
{[
SoftwareImpactSeverity.High,
SoftwareImpactSeverity.Medium,
SoftwareImpactSeverity.Low,
].map((severity) => (
<SoftwareImpactMeasureBreakdownCard
branch={branch}
key={severity}
component={component}
softwareQuality={softwareQuality}
value={measure?.[severity]?.toString()}
severity={severity}
active={highlightedSeverity === severity}
/>
))}
</div>
{measure && (
<div className="sw-flex sw-gap-2">
{[
SoftwareImpactSeverity.High,
SoftwareImpactSeverity.Medium,
SoftwareImpactSeverity.Low,
].map((severity) => (
<SoftwareImpactMeasureBreakdownCard
branch={branch}
key={severity}
component={component}
softwareQuality={softwareQuality}
value={measure?.[severity]?.toString()}
severity={severity}
active={highlightedSeverity === severity}
/>
))}
</div>
)}
</div>
{!measure && (
<>
<BasicSeparator className="sw--mx-4 sw-mb-0 sw-mt-3" />
<StyledInfoSection className="sw--ml-4 sw--mr-4 sw--mb-4 sw-text-xs sw-p-4 sw-flex sw-gap-1 sw-flex-wrap">
<span>
{intl.formatMessage({
id: `overview.run_analysis_to_compute.${component.qualifier}`,
})}
</span>
</StyledInfoSection>
</>
)}
</LightGreyCard>
);
}
@@ -177,8 +171,4 @@ const StyledDash = styled(TextBold)`
font-size: 36px;
`;

const StyledInfoSection = styled.div`
background-color: ${themeColor('overviewSoftwareImpactSeverityNeutral')};
`;

export default SoftwareImpactMeasureCard;

+ 4
- 12
server/sonar-web/src/main/js/apps/overview/branches/TabsPanel.tsx View File

@@ -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<MeasuresPanelProps>) {

return (
<div data-testid="overview__measures-panel">
<div className="sw-flex sw-justify-between sw-items-center sw-mb-4">
<PageTitle as="h2" text={translate('overview.measures')} />
<div className="sw-flex sw-justify-end sw-items-center sw-mb-4">
<LastAnalysisLabel analysisDate={branch?.analysisDate} />
</div>
<BasicSeparator className="sw--mx-6 sw-mb-3" />

{loading ? (
<div>
<Spinner loading={loading} />
<Spinner isLoading={loading} />
</div>
) : (
<>

+ 80
- 41
server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx View File

@@ -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],

+ 20
- 0
server/sonar-web/src/main/js/apps/overview/branches/test-utils.ts View File

@@ -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,

+ 54
- 0
server/sonar-web/src/main/js/components/shared/AnalysisMissingInfoMessage.tsx View File

@@ -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<AnalysisMissingInfoMessageProps>) {
const intl = useIntl();

return (
<FlagMessage variant="info" className={className}>
<FormattedMessage
id={`overview.missing_project_data.${qualifier}`}
tagName="div"
values={{
learn_more: (
<DocumentationLink
className="sw-ml-2 sw-whitespace-nowrap"
to="/user-guide/clean-code/code-analysis/"
>
{intl.formatMessage({ id: 'learn_more' })}
</DocumentationLink>
),
}}
/>
</FlagMessage>
);
}

+ 32
- 0
server/sonar-web/src/main/js/helpers/issues.ts View File

@@ -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,

+ 2
- 2
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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

Loading…
Cancel
Save