*/
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';
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 } });
/* 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 (
<>
{isNewCodeTab && (
<>
{hasNewCodeMeasures ? (
- <>
- {appReanalysisWarning}
- <NewCodeMeasuresPanel
- qgStatuses={qgStatuses}
- branch={branch}
- component={component}
- measures={measures}
- />
- </>
+ <NewCodeMeasuresPanel
+ qgStatuses={qgStatuses}
+ branch={branch}
+ component={component}
+ measures={measures}
+ />
) : (
<MeasuresPanelNoNewCode
branch={branch}
{!isNewCodeTab && (
<>
- {appReanalysisWarning}
+ {analysisMissingInfo}
<OverallCodeMeasuresPanel
branch={branch}
qgStatuses={qgStatuses}
* 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';
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;
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,
});
<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="-" />
/>
</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>
);
}
font-size: 36px;
`;
-const StyledInfoSection = styled.div`
- background-color: ${themeColor('overviewSoftwareImpactSeverityNeutral')};
-`;
-
export default SoftwareImpactMeasureCard;
* 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';
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>
) : (
<>
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';
);
});
- 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();
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({
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],
);
}
},
+ 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,
--- /dev/null
+/*
+ * 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>
+ );
+}
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 {}
} 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,
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.
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