aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js
diff options
context:
space:
mode:
authorstanislavh <stanislav.honcharov@sonarsource.com>2024-03-08 10:51:56 +0100
committersonartech <sonartech@sonarsource.com>2024-03-08 20:02:35 +0000
commitd421d0e24287af84af0dd0399574ad2a7bf06d14 (patch)
treedd05ff5482c0caf2ca2658d4d12dc526384cce84 /server/sonar-web/src/main/js
parent92f48cb48753902d5356deb57c3b13f2a1c59370 (diff)
downloadsonarqube-d421d0e24287af84af0dd0399574ad2a7bf06d14.tar.gz
sonarqube-d421d0e24287af84af0dd0399574ad2a7bf06d14.zip
SONAR-21768 Projects/code and app/projects pages adopt the new taxonomy
Diffstat (limited to 'server/sonar-web/src/main/js')
-rw-r--r--server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts86
-rw-r--r--server/sonar-web/src/main/js/apps/code/__tests__/__snapshots__/utils-test.tsx.snap15
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx23
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx27
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/code/utils.ts11
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureCard.tsx16
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx323
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx71
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.tsx45
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts28
-rw-r--r--server/sonar-web/src/main/js/helpers/constants.ts18
-rw-r--r--server/sonar-web/src/main/js/helpers/issues.ts2
-rw-r--r--server/sonar-web/src/main/js/helpers/measures.ts14
-rw-r--r--server/sonar-web/src/main/js/helpers/mocks/metrics.ts27
16 files changed, 516 insertions, 214 deletions
diff --git a/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts b/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts
index 857a4515d2e..83d30d441fa 100644
--- a/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts
+++ b/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts
@@ -20,10 +20,11 @@
import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
-import { keyBy, times } from 'lodash';
+import { keyBy, omit, times } from 'lodash';
import BranchesServiceMock from '../../../api/mocks/BranchesServiceMock';
import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock';
import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock';
+import { CCT_SOFTWARE_QUALITY_METRICS } from '../../../helpers/constants';
import { isDiffMetric } from '../../../helpers/measures';
import { mockComponent } from '../../../helpers/mocks/component';
import { mockMeasure } from '../../../helpers/testMocks';
@@ -238,9 +239,9 @@ it('should correctly show measures for a project', async () => {
const folderRow = ui.measureRow(/folderA/);
[
[MetricKey.ncloc, '2'],
- [MetricKey.bugs, '2'],
- [MetricKey.vulnerabilities, '2'],
- [MetricKey.code_smells, '2'],
+ [MetricKey.security_issues, '4'],
+ [MetricKey.reliability_issues, '4'],
+ [MetricKey.maintainability_issues, '4'],
[MetricKey.security_hotspots, '2'],
[MetricKey.coverage, '2.0%'],
[MetricKey.duplicated_lines_density, '2.0%'],
@@ -252,9 +253,73 @@ it('should correctly show measures for a project', async () => {
const fileRow = ui.measureRow(/index\.tsx/);
[
[MetricKey.ncloc, '—'],
- [MetricKey.bugs, '—'],
- [MetricKey.vulnerabilities, '—'],
- [MetricKey.code_smells, '—'],
+ [MetricKey.security_issues, '—'],
+ [MetricKey.reliability_issues, '—'],
+ [MetricKey.maintainability_issues, '—'],
+ [MetricKey.security_hotspots, '—'],
+ [MetricKey.coverage, '—'],
+ [MetricKey.duplicated_lines_density, '—'],
+ ].forEach(([domain, value]) => {
+ expect(ui.measureValueCell(fileRow, domain, value)).toBeInTheDocument();
+ });
+});
+
+it('should correctly show measures for a project when relying on old taxonomy', async () => {
+ const component = mockComponent(componentsHandler.findComponentTree('foo')?.component);
+ componentsHandler.registerComponentTree({
+ component,
+ ancestors: [],
+ children: [
+ {
+ component: mockComponent({
+ key: 'folderA',
+ name: 'folderA',
+ qualifier: ComponentQualifier.Directory,
+ }),
+ ancestors: [component],
+ children: [],
+ },
+ {
+ component: mockComponent({
+ key: 'index.tsx',
+ name: 'index.tsx',
+ qualifier: ComponentQualifier.File,
+ }),
+ ancestors: [component],
+ children: [],
+ },
+ ],
+ });
+ componentsHandler.registerComponentMeasures({
+ foo: { [MetricKey.ncloc]: mockMeasure({ metric: MetricKey.ncloc }) },
+ folderA: omit(generateMeasures('2.0'), CCT_SOFTWARE_QUALITY_METRICS),
+ 'index.tsx': {},
+ });
+ const ui = getPageObject(userEvent.setup());
+ renderCode();
+ await ui.appLoaded(component.name);
+
+ // Folder A
+ const folderRow = ui.measureRow(/folderA/);
+ [
+ [MetricKey.ncloc, '2'],
+ [MetricKey.security_issues, '2'],
+ [MetricKey.reliability_issues, '2'],
+ [MetricKey.maintainability_issues, '2'],
+ [MetricKey.security_hotspots, '2'],
+ [MetricKey.coverage, '2.0%'],
+ [MetricKey.duplicated_lines_density, '2.0%'],
+ ].forEach(([domain, value]) => {
+ expect(ui.measureValueCell(folderRow, domain, value)).toBeInTheDocument();
+ });
+
+ // index.tsx
+ const fileRow = ui.measureRow(/index\.tsx/);
+ [
+ [MetricKey.ncloc, '—'],
+ [MetricKey.security_issues, '—'],
+ [MetricKey.reliability_issues, '—'],
+ [MetricKey.maintainability_issues, '—'],
[MetricKey.security_hotspots, '—'],
[MetricKey.coverage, '—'],
[MetricKey.duplicated_lines_density, '—'],
@@ -445,6 +510,13 @@ function generateMeasures(overallValue = '1.0', newValue = '2.0') {
return keyBy(
[
...[
+ MetricKey.security_issues,
+ MetricKey.reliability_issues,
+ MetricKey.maintainability_issues,
+ ].map((metric) =>
+ mockMeasure({ metric, value: JSON.stringify({ total: 4 }), period: undefined }),
+ ),
+ ...[
MetricKey.ncloc,
MetricKey.new_lines,
MetricKey.bugs,
diff --git a/server/sonar-web/src/main/js/apps/code/__tests__/__snapshots__/utils-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/__tests__/__snapshots__/utils-test.tsx.snap
index f6289e4d45d..202da74f5ad 100644
--- a/server/sonar-web/src/main/js/apps/code/__tests__/__snapshots__/utils-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/code/__tests__/__snapshots__/utils-test.tsx.snap
@@ -4,8 +4,11 @@ exports[`getCodeMetrics should return the right metrics for apps 1`] = `
[
"alert_status",
"ncloc",
- "bugs",
+ "security_issues",
+ "reliability_issues",
+ "maintainability_issues",
"vulnerabilities",
+ "bugs",
"code_smells",
"security_hotspots",
"coverage",
@@ -75,8 +78,11 @@ exports[`getCodeMetrics should return the right metrics for portfolios 4`] = `
exports[`getCodeMetrics should return the right metrics for projects 1`] = `
[
"ncloc",
- "bugs",
+ "security_issues",
+ "reliability_issues",
+ "maintainability_issues",
"vulnerabilities",
+ "bugs",
"code_smells",
"security_hotspots",
"coverage",
@@ -87,8 +93,11 @@ exports[`getCodeMetrics should return the right metrics for projects 1`] = `
exports[`getCodeMetrics should return the right metrics for projects 2`] = `
[
"new_lines",
- "bugs",
+ "security_issues",
+ "reliability_issues",
+ "maintainability_issues",
"vulnerabilities",
+ "bugs",
"code_smells",
"security_hotspots",
"new_coverage",
diff --git a/server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx b/server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx
index ea6dca7d68b..493b623f77c 100644
--- a/server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx
@@ -17,6 +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 { Spinner } from '@sonarsource/echoes-react';
import {
Card,
FlagMessage,
@@ -24,9 +25,8 @@ import {
KeyboardHint,
LargeCenteredLayout,
LightLabel,
- Spinner,
} from 'design-system';
-import { intersection } from 'lodash';
+import { difference, intersection } from 'lodash';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
@@ -34,8 +34,11 @@ import HelpTooltip from '../../../components/controls/HelpTooltip';
import ListFooter from '../../../components/controls/ListFooter';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import { Location } from '../../../components/hoc/withRouter';
+import AnalysisMissingInfoMessage from '../../../components/shared/AnalysisMissingInfoMessage';
+import { CCT_SOFTWARE_QUALITY_METRICS, OLD_TAXONOMY_METRICS } from '../../../helpers/constants';
import { KeyboardKeys } from '../../../helpers/keycodes';
import { translate } from '../../../helpers/l10n';
+import { areCCTMeasuresComputed } from '../../../helpers/measures';
import { BranchLike } from '../../../types/branch-like';
import { isApplication, isPortfolioLike } from '../../../types/component';
import { Breadcrumb, Component, ComponentMeasure, Dict, Metric } from '../../../types/types';
@@ -99,7 +102,15 @@ export default function CodeAppRenderer(props: Props) {
getCodeMetrics(component.qualifier, branchLike, { newCode: newCodeSelected }),
Object.keys(metrics),
);
- const filteredMetrics = metricKeys.map((metric) => metrics[metric]);
+
+ const allComponentsHaveSoftwareQualityMeasures = components.every((component) =>
+ areCCTMeasuresComputed(component.measures),
+ );
+
+ const filteredMetrics = difference(
+ metricKeys,
+ allComponentsHaveSoftwareQualityMeasures ? OLD_TAXONOMY_METRICS : CCT_SOFTWARE_QUALITY_METRICS,
+ ).map((key) => metrics[key]);
let defaultTitle = translate('code.page');
if (isApplication(baseComponent?.qualifier)) {
@@ -129,6 +140,10 @@ export default function CodeAppRenderer(props: Props) {
</FlagMessage>
)}
+ {!allComponentsHaveSoftwareQualityMeasures && (
+ <AnalysisMissingInfoMessage qualifier={component.qualifier} className="sw-mb-4" />
+ )}
+
<div className="sw-flex sw-justify-between">
<div>
{hasComponents && (
@@ -181,7 +196,7 @@ export default function CodeAppRenderer(props: Props) {
{(showComponentList || showSearch) && (
<Card className="sw-mt-2 sw-overflow-auto">
- <Spinner loading={loading}>
+ <Spinner isLoading={loading}>
{showComponentList && (
<Components
baseComponent={baseComponent}
diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx
index b1065183efb..d3b36317eb3 100644
--- a/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx
+++ b/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx
@@ -28,8 +28,16 @@ import {
import * as React from 'react';
import Measure from '../../../components/measure/Measure';
import { getLeakValue } from '../../../components/measure/utils';
+import {
+ CCT_SOFTWARE_QUALITY_METRICS,
+ OLD_TO_NEW_TAXONOMY_METRICS_MAP,
+} from '../../../helpers/constants';
import { translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
+import {
+ areCCTMeasuresComputed as areCCTMeasuresComputedFn,
+ formatMeasure,
+ isDiffMetric,
+} from '../../../helpers/measures';
import { isApplication, isProject } from '../../../types/component';
import { MetricKey, MetricType } from '../../../types/metrics';
import { Metric, Status, ComponentMeasure as TypeComponentMeasure } from '../../../types/types';
@@ -44,14 +52,27 @@ export default function ComponentMeasure(props: Props) {
const isProjectLike = isProject(component.qualifier) || isApplication(component.qualifier);
const isReleasability = metric.key === MetricKey.releasability_rating;
- const finalMetricKey = isProjectLike && isReleasability ? MetricKey.alert_status : metric.key;
+ let finalMetricKey = isProjectLike && isReleasability ? MetricKey.alert_status : metric.key;
const finalMetricType = isProjectLike && isReleasability ? MetricType.Level : metric.type;
+ const areCCTMeasasuresComputed = areCCTMeasuresComputedFn(component.measures);
+ finalMetricKey = areCCTMeasasuresComputed
+ ? OLD_TO_NEW_TAXONOMY_METRICS_MAP[finalMetricKey as MetricKey] ?? finalMetricKey
+ : finalMetricKey;
+
const measure = Array.isArray(component.measures)
? component.measures.find((measure) => measure.metric === finalMetricKey)
: undefined;
- const value = isDiffMetric(metric.key) ? getLeakValue(measure) : measure?.value;
+ let value;
+ if (
+ measure?.value !== undefined &&
+ CCT_SOFTWARE_QUALITY_METRICS.includes(measure.metric as MetricKey)
+ ) {
+ value = JSON.parse(measure.value).total;
+ } else {
+ value = isDiffMetric(metric.key) ? getLeakValue(measure) : measure?.value;
+ }
switch (finalMetricType) {
case MetricType.Level: {
diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx
index be554df01ec..312ea27eb4c 100644
--- a/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx
@@ -19,6 +19,10 @@
*/
import { ContentCell, NumericalCell, RatingCell } from 'design-system';
import * as React from 'react';
+import {
+ CCT_SOFTWARE_QUALITY_METRICS,
+ OLD_TO_NEW_TAXONOMY_METRICS_MAP,
+} from '../../../helpers/constants';
import { translate } from '../../../helpers/l10n';
import { isPortfolioLike } from '../../../types/component';
import { MetricKey } from '../../../types/metrics';
@@ -33,6 +37,7 @@ interface ComponentsHeaderProps {
}
const SHORT_NAME_METRICS = [
+ ...CCT_SOFTWARE_QUALITY_METRICS,
MetricKey.duplicated_lines_density,
MetricKey.new_lines,
MetricKey.new_coverage,
@@ -60,13 +65,15 @@ export default function ComponentsHeader(props: ComponentsHeaderProps) {
Cell = RatingCell;
} else {
- columns = metrics.map((metric) =>
- translate(
+ columns = metrics.map((m: MetricKey) => {
+ const metric = OLD_TO_NEW_TAXONOMY_METRICS_MAP[m] ?? m;
+
+ return translate(
'metric',
metric,
SHORT_NAME_METRICS.includes(metric as MetricKey) ? 'short_name' : 'name',
- ),
- );
+ );
+ });
Cell = NumericalCell;
}
diff --git a/server/sonar-web/src/main/js/apps/code/utils.ts b/server/sonar-web/src/main/js/apps/code/utils.ts
index 93cd049d8c2..ca7fbc55154 100644
--- a/server/sonar-web/src/main/js/apps/code/utils.ts
+++ b/server/sonar-web/src/main/js/apps/code/utils.ts
@@ -19,6 +19,7 @@
*/
import { getBreadcrumbs, getChildren, getComponent, getComponentData } from '../../api/components';
import { getBranchLikeQuery, isPullRequest } from '../../helpers/branch-like';
+import { CCT_SOFTWARE_QUALITY_METRICS, OLD_TAXONOMY_METRICS } from '../../helpers/constants';
import { BranchLike } from '../../types/branch-like';
import { ComponentQualifier, isPortfolioLike } from '../../types/component';
import { MetricKey } from '../../types/metrics';
@@ -34,9 +35,8 @@ import {
const METRICS = [
MetricKey.ncloc,
- MetricKey.bugs,
- MetricKey.vulnerabilities,
- MetricKey.code_smells,
+ ...CCT_SOFTWARE_QUALITY_METRICS,
+ ...OLD_TAXONOMY_METRICS,
MetricKey.security_hotspots,
MetricKey.coverage,
MetricKey.duplicated_lines_density,
@@ -64,9 +64,8 @@ const NEW_PORTFOLIO_METRICS = [
const LEAK_METRICS = [
MetricKey.new_lines,
- MetricKey.bugs,
- MetricKey.vulnerabilities,
- MetricKey.code_smells,
+ ...CCT_SOFTWARE_QUALITY_METRICS,
+ ...OLD_TAXONOMY_METRICS,
MetricKey.security_hotspots,
MetricKey.new_coverage,
MetricKey.new_duplicated_lines_density,
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 5faad7c2615..1ded1eb2e24 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
@@ -28,12 +28,11 @@ 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 { areCCTMeasuresComputed, isDiffMetric } from '../../../helpers/measures';
import { CodeScope } from '../../../helpers/urls';
import { ApplicationPeriod } from '../../../types/application';
import { Branch } from '../../../types/branch-like';
import { ComponentQualifier } from '../../../types/component';
-import { MetricKey } from '../../../types/metrics';
import { Analysis, GraphType, MeasureHistory } from '../../../types/project-activity';
import { QualityGateStatus } from '../../../types/quality-gates';
import { Component, MeasureEnhanced, Metric, Period, QualityGate } from '../../../types/types';
@@ -99,11 +98,7 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
const hasNewCodeMeasures = measures.some((m) => isDiffMetric(m.metric.key));
// Check if any potentially missing uncomputed measure is not present
- const isMissingMeasures = [
- MetricKey.security_issues,
- MetricKey.maintainability_issues,
- MetricKey.reliability_issues,
- ].some((key) => !measures.find((measure) => measure.metric.key === key));
+ const isMissingMeasures = !areCCTMeasuresComputed(measures);
const selectTab = (tab: CodeScope) => {
router.replace({ query: { ...query, codeScope: tab } });
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureCard.tsx b/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureCard.tsx
index 52fcb66b016..9115a05f775 100644
--- a/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureCard.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureCard.tsx
@@ -17,15 +17,20 @@
* 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 { LinkHighlight, LinkStandalone } from '@sonarsource/echoes-react';
import classNames from 'classnames';
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 { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils';
+import {
+ SOFTWARE_QUALITIES_METRIC_KEYS_MAP,
+ getIssueTypeBySoftwareQuality,
+} from '../../../helpers/issues';
import { formatMeasure } from '../../../helpers/measures';
+import { isDefined } from '../../../helpers/types';
import { getComponentIssuesUrl } from '../../../helpers/urls';
import { Branch } from '../../../types/branch-like';
import {
@@ -39,11 +44,6 @@ 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;
@@ -69,7 +69,7 @@ export function SoftwareImpactMeasureCard(props: Readonly<SoftwareImpactBreakdow
// Find rating measure
const ratingMeasure = measures.find((m) => m.metric.key === ratingMetricKey);
- const count = measure?.total ?? alternativeMeasure?.value;
+ const count = formatMeasure(measure?.total ?? alternativeMeasure?.value, MetricType.ShortInteger);
const totalLinkHref = getComponentIssuesUrl(component.key, {
...DEFAULT_ISSUES_QUERY,
@@ -125,7 +125,7 @@ export function SoftwareImpactMeasureCard(props: Readonly<SoftwareImpactBreakdow
highlight={LinkHighlight.CurrentColor}
to={totalLinkHref}
>
- {formatMeasure(count, MetricType.ShortInteger)}
+ {count}
</LinkStandalone>
</Tooltip>
) : (
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx
index 07fe0ed08d4..2967f417707 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx
@@ -37,11 +37,18 @@ import {
themeColor,
} from 'design-system';
import * as React from 'react';
+import { useIntl } from 'react-intl';
import { getBranchLikeQuery } from '../../helpers/branch-like';
-import { ISSUE_TYPES } from '../../helpers/constants';
-import { ISSUETYPE_METRIC_KEYS_MAP } from '../../helpers/issues';
-import { translate } from '../../helpers/l10n';
-import { formatMeasure } from '../../helpers/measures';
+import { SOFTWARE_QUALITIES } from '../../helpers/constants';
+import {
+ ISSUETYPE_METRIC_KEYS_MAP,
+ SOFTWARE_QUALITIES_METRIC_KEYS_MAP,
+ getIssueTypeBySoftwareQuality,
+} from '../../helpers/issues';
+import {
+ areCCTMeasuresComputed as areCCTMeasuresComputedFn,
+ formatMeasure,
+} from '../../helpers/measures';
import { collapsedDirFromPath, fileFromPath } from '../../helpers/path';
import { omitNil } from '../../helpers/request';
import { getBaseUrl } from '../../helpers/system';
@@ -69,14 +76,19 @@ interface Props {
sourceViewerFile: SourceViewerFile;
}
-export default class SourceViewerHeader extends React.PureComponent<Props> {
- openInWorkspace = () => {
- const { key } = this.props.sourceViewerFile;
- this.props.openComponent({ branchLike: this.props.branchLike, key });
- };
+export default function SourceViewerHeader(props: Readonly<Props>) {
+ const intl = useIntl();
+
+ const { showMeasures, branchLike, hidePinOption, openComponent, componentMeasures } = props;
+ const { key, measures, path, project, projectName, q } = props.sourceViewerFile;
+ const unitTestsOrLines = q === ComponentQualifier.TestFile ? MetricKey.tests : MetricKey.lines;
+
+ const query = new URLSearchParams(omitNil({ key, ...getBranchLikeQuery(branchLike) })).toString();
- renderIssueMeasures = () => {
- const { branchLike, componentMeasures, sourceViewerFile } = this.props;
+ const rawSourcesLink = `${getBaseUrl()}/api/sources/raw?${query}`;
+
+ const renderIssueMeasures = () => {
+ const areCCTMeasuresComputed = areCCTMeasuresComputedFn(componentMeasures);
return (
componentMeasures &&
@@ -85,163 +97,192 @@ export default class SourceViewerHeader extends React.PureComponent<Props> {
<StyledVerticalSeparator className="sw-h-8 sw-mx-6" />
<div className="sw-flex sw-gap-6">
- {ISSUE_TYPES.map((type: IssueType) => {
- const params = {
- ...getBranchLikeQuery(branchLike),
- files: sourceViewerFile.path,
- ...DEFAULT_ISSUES_QUERY,
- types: type,
- };
-
+ {SOFTWARE_QUALITIES.map((quality) => {
+ const { deprecatedMetric, metric } = SOFTWARE_QUALITIES_METRIC_KEYS_MAP[quality];
const measure = componentMeasures.find(
- (m) => m.metric === ISSUETYPE_METRIC_KEYS_MAP[type].metric,
+ (m) => m.metric === (areCCTMeasuresComputed ? metric : deprecatedMetric),
);
+ const measureValue = areCCTMeasuresComputed
+ ? JSON.parse(measure?.value ?? 'null').total
+ : measure?.value ?? 0;
- const linkUrl =
- type === IssueType.SecurityHotspot
- ? getComponentSecurityHotspotsUrl(sourceViewerFile.project, params)
- : getComponentIssuesUrl(sourceViewerFile.project, params);
+ const linkUrl = getComponentIssuesUrl(project, {
+ ...getBranchLikeQuery(branchLike),
+ files: path,
+ ...DEFAULT_ISSUES_QUERY,
+ ...(areCCTMeasuresComputed
+ ? { impactSoftwareQualities: quality }
+ : { types: getIssueTypeBySoftwareQuality(quality) }),
+ });
+
+ const qualityTitle = intl.formatMessage({ id: `metric.${metric}.short_name` });
return (
- <div className="sw-flex sw-flex-col sw-gap-1" key={type}>
+ <div className="sw-flex sw-flex-col sw-gap-1" key={quality}>
<Note className="it__source-viewer-header-measure-label sw-body-lg">
- {translate('issue.type', type)}
+ {qualityTitle}
</Note>
<span>
- <StyledDrilldownLink className="sw-body-md" to={linkUrl}>
- {formatMeasure(measure?.value ?? 0, MetricType.Integer)}
+ <StyledDrilldownLink
+ className="sw-body-md"
+ aria-label={intl.formatMessage(
+ { id: 'source_viewer.issue_link_x' },
+ {
+ count: formatMeasure(measureValue, MetricType.Integer),
+ quality: qualityTitle,
+ },
+ )}
+ to={linkUrl}
+ >
+ {formatMeasure(measureValue, MetricType.Integer)}
</StyledDrilldownLink>
</span>
</div>
);
})}
+
+ <div className="sw-flex sw-flex-col sw-gap-1" key={IssueType.SecurityHotspot}>
+ <Note className="it__source-viewer-header-measure-label sw-body-lg">
+ {intl.formatMessage({ id: `issue.type.${IssueType.SecurityHotspot}` })}
+ </Note>
+
+ <span>
+ <StyledDrilldownLink
+ className="sw-body-md"
+ to={getComponentSecurityHotspotsUrl(project, {
+ ...getBranchLikeQuery(branchLike),
+ files: path,
+ ...DEFAULT_ISSUES_QUERY,
+ types: IssueType.SecurityHotspot,
+ })}
+ >
+ {formatMeasure(
+ componentMeasures.find(
+ (m) =>
+ m.metric === ISSUETYPE_METRIC_KEYS_MAP[IssueType.SecurityHotspot].metric,
+ )?.value ?? 0,
+ MetricType.Integer,
+ )}
+ </StyledDrilldownLink>
+ </span>
+ </div>
</div>
</>
)
);
};
- render() {
- const { showMeasures } = this.props;
- const { key, measures, path, project, projectName, q } = this.props.sourceViewerFile;
- const unitTestsOrLines = q === ComponentQualifier.TestFile ? MetricKey.tests : MetricKey.lines;
-
- const query = new URLSearchParams(
- omitNil({ key, ...getBranchLikeQuery(this.props.branchLike) }),
- ).toString();
-
- const rawSourcesLink = `${getBaseUrl()}/api/sources/raw?${query}`;
-
- return (
- <StyledHeaderContainer
- className={
- 'it__source-viewer-header sw-body-sm sw-flex sw-items-center sw-px-4 sw-py-3 ' +
- 'sw-relative'
- }
- >
- <div className="sw-flex sw-flex-1 sw-flex-col sw-gap-1 sw-mr-5 sw-my-1">
- <div className="sw-flex sw-gap-1 sw-items-center">
- <LinkStandalone
- iconLeft={<ProjectIcon className="sw-mr-2" />}
- to={getBranchLikeUrl(project, this.props.branchLike)}
- >
- {projectName}
- </LinkStandalone>
- </div>
-
- <div className="sw-flex sw-gap-2 sw-items-center">
- <QualifierIcon qualifier={q} />
-
- {collapsedDirFromPath(path)}
-
- {fileFromPath(path)}
-
- <span>
- <ClipboardIconButton
- aria-label={translate('component_viewer.copy_path_to_clipboard')}
- copyValue={path}
- />
- </span>
- </div>
+ return (
+ <StyledHeaderContainer
+ className={
+ 'it__source-viewer-header sw-body-sm sw-flex sw-items-center sw-px-4 sw-py-3 ' +
+ 'sw-relative'
+ }
+ >
+ <div className="sw-flex sw-flex-1 sw-flex-col sw-gap-1 sw-mr-5 sw-my-1">
+ <div className="sw-flex sw-gap-1 sw-items-center">
+ <LinkStandalone
+ iconLeft={<ProjectIcon className="sw-mr-2" />}
+ to={getBranchLikeUrl(project, branchLike)}
+ >
+ {projectName}
+ </LinkStandalone>
</div>
- {showMeasures && (
- <div className="sw-flex sw-gap-6 sw-items-center">
- {isDefined(measures[unitTestsOrLines]) && (
- <div className="sw-flex sw-flex-col sw-gap-1">
- <Note className="it__source-viewer-header-measure-label sw-body-lg">
- {translate(`metric.${unitTestsOrLines}.name`)}
- </Note>
-
- <span className="sw-body-lg">
- {formatMeasure(measures[unitTestsOrLines], MetricType.ShortInteger)}
- </span>
- </div>
- )}
-
- {isDefined(measures.coverage) && (
- <div className="sw-flex sw-flex-col sw-gap-1">
- <Note className="it__source-viewer-header-measure-label sw-body-lg">
- {translate('metric.coverage.name')}
- </Note>
+ <div className="sw-flex sw-gap-2 sw-items-center">
+ <QualifierIcon qualifier={q} />
- <span className="sw-body-lg">
- {formatMeasure(measures.coverage, MetricType.Percent)}
- </span>
- </div>
- )}
+ {collapsedDirFromPath(path)}
- {isDefined(measures.duplicationDensity) && (
- <div className="sw-flex sw-flex-col sw-gap-1">
- <Note className="it__source-viewer-header-measure-label sw-body-lg">
- {translate('duplications')}
- </Note>
+ {fileFromPath(path)}
- <span className="sw-body-lg">
- {formatMeasure(measures.duplicationDensity, MetricType.Percent)}
- </span>
- </div>
+ <span>
+ <ClipboardIconButton
+ aria-label={intl.formatMessage({ id: 'component_viewer.copy_path_to_clipboard' })}
+ copyValue={path}
+ />
+ </span>
+ </div>
+ </div>
+
+ {showMeasures && (
+ <div className="sw-flex sw-gap-6 sw-items-center">
+ {isDefined(measures[unitTestsOrLines]) && (
+ <div className="sw-flex sw-flex-col sw-gap-1">
+ <Note className="it__source-viewer-header-measure-label sw-body-lg">
+ {intl.formatMessage({ id: `metric.${unitTestsOrLines}.name` })}
+ </Note>
+
+ <span className="sw-body-lg">
+ {formatMeasure(measures[unitTestsOrLines], MetricType.ShortInteger)}
+ </span>
+ </div>
+ )}
+
+ {isDefined(measures.coverage) && (
+ <div className="sw-flex sw-flex-col sw-gap-1">
+ <Note className="it__source-viewer-header-measure-label sw-body-lg">
+ {intl.formatMessage({ id: 'metric.coverage.name' })}
+ </Note>
+
+ <span className="sw-body-lg">
+ {formatMeasure(measures.coverage, MetricType.Percent)}
+ </span>
+ </div>
+ )}
+
+ {isDefined(measures.duplicationDensity) && (
+ <div className="sw-flex sw-flex-col sw-gap-1">
+ <Note className="it__source-viewer-header-measure-label sw-body-lg">
+ {intl.formatMessage({ id: 'duplications' })}
+ </Note>
+
+ <span className="sw-body-lg">
+ {formatMeasure(measures.duplicationDensity, MetricType.Percent)}
+ </span>
+ </div>
+ )}
+
+ {renderIssueMeasures()}
+ </div>
+ )}
+
+ <Dropdown
+ id="source-viewer-header-actions"
+ overlay={
+ <>
+ <ItemLink isExternal to={getCodeUrl(project, branchLike, key)}>
+ {intl.formatMessage({ id: 'component_viewer.new_window' })}
+ </ItemLink>
+
+ {!hidePinOption && (
+ <ItemButton
+ className="it__js-workspace"
+ onClick={() => {
+ openComponent({ branchLike, key });
+ }}
+ >
+ {intl.formatMessage({ id: 'component_viewer.open_in_workspace' })}
+ </ItemButton>
)}
- {this.renderIssueMeasures()}
- </div>
- )}
-
- <Dropdown
- id="source-viewer-header-actions"
- overlay={
- <>
- <ItemLink
- isExternal
- to={getCodeUrl(this.props.sourceViewerFile.project, this.props.branchLike, key)}
- >
- {translate('component_viewer.new_window')}
- </ItemLink>
-
- {!this.props.hidePinOption && (
- <ItemButton className="it__js-workspace" onClick={this.openInWorkspace}>
- {translate('component_viewer.open_in_workspace')}
- </ItemButton>
- )}
-
- <ItemLink isExternal to={rawSourcesLink}>
- {translate('component_viewer.show_raw_source')}
- </ItemLink>
- </>
- }
- placement={PopupPlacement.BottomRight}
- zLevel={PopupZLevel.Global}
- >
- <InteractiveIcon
- aria-label={translate('component_viewer.action_menu')}
- className="it__js-actions sw-flex-0 sw-ml-4 sw-px-3 sw-py-2"
- Icon={MenuIcon}
- />
- </Dropdown>
- </StyledHeaderContainer>
- );
- }
+ <ItemLink isExternal to={rawSourcesLink}>
+ {intl.formatMessage({ id: 'component_viewer.show_raw_source' })}
+ </ItemLink>
+ </>
+ }
+ placement={PopupPlacement.BottomRight}
+ zLevel={PopupZLevel.Global}
+ >
+ <InteractiveIcon
+ aria-label={intl.formatMessage({ id: 'component_viewer.action_menu' })}
+ className="it__js-actions sw-flex-0 sw-ml-4 sw-px-3 sw-py-2"
+ Icon={MenuIcon}
+ />
+ </Dropdown>
+ </StyledHeaderContainer>
+ );
}
const StyledDrilldownLink = styled(DrilldownLink)`
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
index f29f2d82078..dd75fad448a 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
@@ -23,9 +23,13 @@ import * as React from 'react';
import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock';
import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock';
import UsersServiceMock from '../../../api/mocks/UsersServiceMock';
+import { CCT_SOFTWARE_QUALITY_METRICS } from '../../../helpers/constants';
+import { isDiffMetric } from '../../../helpers/measures';
import { HttpStatus } from '../../../helpers/request';
-import { mockIssue, mockLoggedInUser } from '../../../helpers/testMocks';
+import { mockIssue, mockLoggedInUser, mockMeasure } from '../../../helpers/testMocks';
import { renderComponent } from '../../../helpers/testReactTestingUtils';
+import { byLabelText } from '../../../helpers/testSelector';
+import { MetricKey } from '../../../types/metrics';
import { RestUserDetailed } from '../../../types/users';
import SourceViewer, { Props } from '../SourceViewer';
import loadIssues from '../helpers/loadIssues';
@@ -256,7 +260,7 @@ it('should show issue indicator', async () => {
await user.click(
issueRow.getByRole('button', {
- name: 'source_viewer.issues_on_line.X_issues_of_type_Y.source_viewer.issues_on_line.show.2.issue.type.BUG.plural',
+ name: 'source_viewer.issues_on_line.multiple_issues_same_category.true.2.issue.clean_code_attribute_category.responsible',
}),
);
});
@@ -338,6 +342,43 @@ it('should highlight symbol', async () => {
});
});
+it('should show software quality measures in header', async () => {
+ renderSourceViewer({ componentMeasures: generateMeasures(), showMeasures: true });
+
+ expect(
+ await byLabelText('source_viewer.issue_link_x.3.metric.security_issues.short_name').find(),
+ ).toBeInTheDocument();
+ expect(
+ await byLabelText('source_viewer.issue_link_x.3.metric.reliability_issues.short_name').find(),
+ ).toBeInTheDocument();
+ expect(
+ await byLabelText(
+ 'source_viewer.issue_link_x.3.metric.maintainability_issues.short_name',
+ ).find(),
+ ).toBeInTheDocument();
+});
+
+it('should show old issue measures in header', async () => {
+ renderSourceViewer({
+ componentMeasures: generateMeasures().filter(
+ (m) => !CCT_SOFTWARE_QUALITY_METRICS.includes(m.metric as MetricKey),
+ ),
+ showMeasures: true,
+ });
+
+ expect(
+ await byLabelText('source_viewer.issue_link_x.1.metric.security_issues.short_name').find(),
+ ).toBeInTheDocument();
+ expect(
+ await byLabelText('source_viewer.issue_link_x.1.metric.reliability_issues.short_name').find(),
+ ).toBeInTheDocument();
+ expect(
+ await byLabelText(
+ 'source_viewer.issue_link_x.1.metric.maintainability_issues.short_name',
+ ).find(),
+ ).toBeInTheDocument();
+});
+
it('should show correct message when component is not asscessible', async () => {
componentsHandler.setFailLoadingComponentStatus(HttpStatus.Forbidden);
renderSourceViewer();
@@ -353,6 +394,32 @@ it('should show correct message when component does not exist', async () => {
expect(await screen.findByText('component_viewer.no_component')).toBeInTheDocument();
});
+function generateMeasures(qualitiesValue = '3.0', overallValue = '1.0', newValue = '2.0') {
+ return [
+ ...[
+ MetricKey.security_issues,
+ MetricKey.reliability_issues,
+ MetricKey.maintainability_issues,
+ ].map((metric) =>
+ mockMeasure({ metric, value: JSON.stringify({ total: qualitiesValue }), period: undefined }),
+ ),
+ ...[
+ MetricKey.ncloc,
+ MetricKey.new_lines,
+ MetricKey.bugs,
+ MetricKey.vulnerabilities,
+ MetricKey.code_smells,
+ MetricKey.security_hotspots,
+ MetricKey.coverage,
+ MetricKey.new_coverage,
+ ].map((metric) =>
+ isDiffMetric(metric)
+ ? mockMeasure({ metric, period: { index: 1, value: newValue } })
+ : mockMeasure({ metric, value: overallValue, period: undefined }),
+ ),
+ ];
+}
+
function renderSourceViewer(override?: Partial<Props>) {
const { rerender } = renderComponent(
<SourceViewer
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.tsx
index 84fea1e6b14..c4d4d50c824 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.tsx
@@ -20,9 +20,8 @@
import { IssueIndicatorButton, LineIssuesIndicatorIcon, LineMeta } from 'design-system';
import { uniq } from 'lodash';
import * as React from 'react';
+import { useIntl } from 'react-intl';
import Tooltip from '../../../components/controls/Tooltip';
-import { sortByType } from '../../../helpers/issues';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Issue, SourceLine } from '../../../types/types';
const MOUSE_LEAVE_DELAY = 0.25;
@@ -37,33 +36,32 @@ export interface LineIssuesIndicatorProps {
export function LineIssuesIndicator(props: LineIssuesIndicatorProps) {
const { issues, issuesOpen, line } = props;
const hasIssues = issues.length > 0;
+ const intl = useIntl();
if (!hasIssues) {
return <LineMeta />;
}
- const mostImportantIssue = sortByType(issues)[0];
- const issueTypes = uniq(issues.map((i) => i.type));
-
- const tooltipShowHide = translate('source_viewer.issues_on_line', issuesOpen ? 'hide' : 'show');
+ const issueAttributeCategories = uniq(issues.map((issue) => issue.cleanCodeAttributeCategory));
let tooltipContent;
- if (issueTypes.length > 1) {
- tooltipContent = translateWithParameters(
- 'source_viewer.issues_on_line.multiple_issues',
- tooltipShowHide,
- );
- } else if (issues.length === 1) {
- tooltipContent = translateWithParameters(
- 'source_viewer.issues_on_line.issue_of_type_X',
- tooltipShowHide,
- translate('issue.type', mostImportantIssue.type),
+
+ if (issueAttributeCategories.length > 1) {
+ tooltipContent = intl.formatMessage(
+ { id: 'source_viewer.issues_on_line.multiple_issues' },
+ { show: !issuesOpen },
);
} else {
- tooltipContent = translateWithParameters(
- 'source_viewer.issues_on_line.X_issues_of_type_Y',
- tooltipShowHide,
- issues.length,
- translate('issue.type', mostImportantIssue.type, 'plural'),
+ tooltipContent = intl.formatMessage(
+ { id: 'source_viewer.issues_on_line.multiple_issues_same_category' },
+ {
+ show: !issuesOpen,
+ count: issues.length,
+ category: intl
+ .formatMessage({
+ id: `issue.clean_code_attribute_category.${issueAttributeCategories[0]}`,
+ })
+ .toLowerCase(),
+ },
);
}
@@ -75,10 +73,7 @@ export function LineIssuesIndicator(props: LineIssuesIndicatorProps) {
aria-expanded={issuesOpen}
onClick={props.onClick}
>
- <LineIssuesIndicatorIcon
- issuesCount={issues.length}
- mostImportantIssueType={mostImportantIssue.type}
- />
+ <LineIssuesIndicatorIcon issuesCount={issues.length} />
</IssueIndicatorButton>
</Tooltip>
</LineMeta>
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts
index f22797dcaf8..820f4a6636b 100644
--- a/server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts
+++ b/server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts
@@ -19,10 +19,16 @@
*/
import { MetricKey, MetricType } from '../../types/metrics';
import { Dict } from '../../types/types';
+import { CCT_SOFTWARE_QUALITY_METRICS } from '../constants';
import { getMessages } from '../l10nBundle';
-import { enhanceConditionWithMeasure, formatMeasure, isPeriodBestValue } from '../measures';
+import {
+ areCCTMeasuresComputed,
+ enhanceConditionWithMeasure,
+ formatMeasure,
+ isPeriodBestValue,
+} from '../measures';
import { mockQualityGateStatusCondition } from '../mocks/quality-gates';
-import { mockMeasureEnhanced, mockMetric } from '../testMocks';
+import { mockMeasure, mockMeasureEnhanced, mockMetric } from '../testMocks';
jest.unmock('../l10n');
@@ -259,3 +265,21 @@ describe('#formatMeasure()', () => {
expect(formatMeasure(undefined, MetricType.Integer)).toBe('');
});
});
+
+describe('areCCTMeasuresComputed', () => {
+ it('returns true when measures include maintainability_,security_,reliability_issues', () => {
+ expect(
+ areCCTMeasuresComputed(CCT_SOFTWARE_QUALITY_METRICS.map((metric) => mockMeasure({ metric }))),
+ ).toBe(true);
+ });
+
+ it('returns false otherwise', () => {
+ expect(areCCTMeasuresComputed([mockMeasure()])).toBe(false);
+ expect(
+ areCCTMeasuresComputed([
+ mockMeasure(),
+ mockMeasure({ metric: CCT_SOFTWARE_QUALITY_METRICS[0] }),
+ ]),
+ ).toBe(false);
+ });
+});
diff --git a/server/sonar-web/src/main/js/helpers/constants.ts b/server/sonar-web/src/main/js/helpers/constants.ts
index a6ee2cb6fd8..63de008d849 100644
--- a/server/sonar-web/src/main/js/helpers/constants.ts
+++ b/server/sonar-web/src/main/js/helpers/constants.ts
@@ -86,6 +86,24 @@ export const ISSUE_TYPES: IssueType[] = [
IssueType.SecurityHotspot,
];
+export const CCT_SOFTWARE_QUALITY_METRICS = [
+ MetricKey.security_issues,
+ MetricKey.reliability_issues,
+ MetricKey.maintainability_issues,
+];
+
+export const OLD_TAXONOMY_METRICS = [
+ MetricKey.vulnerabilities,
+ MetricKey.bugs,
+ MetricKey.code_smells,
+];
+
+export const OLD_TO_NEW_TAXONOMY_METRICS_MAP: { [key in MetricKey]?: MetricKey } = {
+ [MetricKey.vulnerabilities]: MetricKey.security_issues,
+ [MetricKey.bugs]: MetricKey.reliability_issues,
+ [MetricKey.code_smells]: MetricKey.maintainability_issues,
+};
+
export const RESOLUTIONS = [
IssueResolution.Unresolved,
IssueResolution.FalsePositive,
diff --git a/server/sonar-web/src/main/js/helpers/issues.ts b/server/sonar-web/src/main/js/helpers/issues.ts
index c62a9b29222..204a54d606b 100644
--- a/server/sonar-web/src/main/js/helpers/issues.ts
+++ b/server/sonar-web/src/main/js/helpers/issues.ts
@@ -19,12 +19,12 @@
*/
import { BugIcon, CodeSmellIcon, SecurityHotspotIcon, VulnerabilityIcon } from 'design-system';
import { flatten, sortBy } from 'lodash';
+import { SoftwareQuality } from '../types/clean-code-taxonomy';
import { IssueType, RawIssue } from '../types/issues';
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 {}
diff --git a/server/sonar-web/src/main/js/helpers/measures.ts b/server/sonar-web/src/main/js/helpers/measures.ts
index 21361041699..f8cef7ad0ae 100644
--- a/server/sonar-web/src/main/js/helpers/measures.ts
+++ b/server/sonar-web/src/main/js/helpers/measures.ts
@@ -23,7 +23,7 @@ import {
QualityGateStatusConditionEnhanced,
} from '../types/quality-gates';
import { Dict, Measure, MeasureEnhanced, Metric } from '../types/types';
-import { ONE_SECOND } from './constants';
+import { CCT_SOFTWARE_QUALITY_METRICS, ONE_SECOND } from './constants';
import { translate, translateWithParameters } from './l10n';
import { getCurrentLocale } from './l10nBundle';
import { isDefined } from './types';
@@ -79,6 +79,18 @@ export function findMeasure(measures: MeasureEnhanced[], metric: MetricKey | str
return measures.find((measure) => measure.metric.key === metric);
}
+export function areCCTMeasuresComputed(measures?: Measure[] | MeasureEnhanced[]) {
+ return CCT_SOFTWARE_QUALITY_METRICS.every((metric) =>
+ measures?.find((measure) =>
+ isMeasureEnhanced(measure) ? measure.metric.key === metric : measure.metric === metric,
+ ),
+ );
+}
+
+function isMeasureEnhanced(measure: Measure | MeasureEnhanced): measure is MeasureEnhanced {
+ return (measure.metric as Metric)?.key !== undefined;
+}
+
const HOURS_IN_DAY = 8;
type Formatter = (value: string | number, options?: Dict<unknown>) => 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 1134b4e62b7..75a97dcabbd 100644
--- a/server/sonar-web/src/main/js/helpers/mocks/metrics.ts
+++ b/server/sonar-web/src/main/js/helpers/mocks/metrics.ts
@@ -910,6 +910,15 @@ export const DEFAULT_METRICS: Dict<Metric> = {
qualitative: true,
hidden: true,
},
+ reliability_issues: {
+ key: 'reliability_issues',
+ type: 'INT',
+ name: 'Reliability',
+ description: 'Reliability issues',
+ direction: -1,
+ qualitative: true,
+ hidden: false,
+ },
reliability_rating: {
key: 'reliability_rating',
type: 'RATING',
@@ -1012,6 +1021,15 @@ export const DEFAULT_METRICS: Dict<Metric> = {
hidden: false,
decimalScale: 1,
},
+ security_issues: {
+ key: 'security_issues',
+ type: 'INT',
+ name: 'Security',
+ description: 'Security issues',
+ direction: -1,
+ qualitative: true,
+ hidden: false,
+ },
security_rating: {
key: 'security_rating',
type: 'RATING',
@@ -1172,6 +1190,15 @@ export const DEFAULT_METRICS: Dict<Metric> = {
qualitative: false,
hidden: false,
},
+ maintainability_issues: {
+ key: 'maintainability_issues',
+ type: 'INT',
+ name: 'Maintainability',
+ description: 'Maintainability issues',
+ direction: -1,
+ qualitative: true,
+ hidden: false,
+ },
sqale_index: {
key: 'sqale_index',
type: 'WORK_DUR',