import { IssueData } from './issues';
import { listAllComponent, listAllComponentTrees } from './utils';
+const MAX_RATING = 5;
export type MeasureRecords = Record<string, Record<string, Measure>>;
export function mockFullMeasureData(tree: ComponentTree, issueList: IssueData[]) {
}),
});
+ case MetricKey.new_security_issues:
+ return mockMeasure({
+ metric: metricKey,
+ period: {
+ index: 1,
+ value: JSON.stringify({
+ total: 3,
+ [SoftwareImpactSeverity.High]: 2,
+ [SoftwareImpactSeverity.Medium]: 0,
+ [SoftwareImpactSeverity.Low]: 1,
+ }),
+ },
+ value: undefined,
+ });
+
case MetricKey.reliability_issues:
return mockMeasure({
metric: metricKey,
}),
});
+ case MetricKey.new_reliability_issues:
+ return mockMeasure({
+ metric: metricKey,
+ period: {
+ index: 1,
+ value: JSON.stringify({
+ total: 2,
+ [SoftwareImpactSeverity.High]: 0,
+ [SoftwareImpactSeverity.Medium]: 1,
+ [SoftwareImpactSeverity.Low]: 1,
+ }),
+ },
+ value: undefined,
+ });
+
case MetricKey.maintainability_issues:
return mockMeasure({
metric: metricKey,
[SoftwareImpactSeverity.Low]: 1,
}),
});
+
+ case MetricKey.new_maintainability_issues:
+ return mockMeasure({
+ metric: metricKey,
+ period: {
+ index: 1,
+ value: JSON.stringify({
+ total: 5,
+ [SoftwareImpactSeverity.High]: 2,
+ [SoftwareImpactSeverity.Medium]: 2,
+ [SoftwareImpactSeverity.Low]: 1,
+ }),
+ },
+ value: undefined,
+ });
}
const issues = issueList
export function getMetricTypeFromKey(metricKey: string) {
if (/(coverage|duplication)$/.test(metricKey)) {
return MetricType.Percent;
- } else if (/_rating$/.test(metricKey)) {
+ } else if (metricKey.includes('_rating')) {
return MetricType.Rating;
} else if (
[
MetricKey.reliability_issues,
+ MetricKey.new_reliability_issues,
MetricKey.security_issues,
+ MetricKey.new_security_issues,
MetricKey.maintainability_issues,
+ MetricKey.new_maintainability_issues,
].includes(metricKey as MetricKey)
) {
return MetricType.Data;
* ratio to the LOC. But using the number will suffice as an approximation in our tests.
*/
function computeRating(issues: RawIssue[], type: IssueType) {
- const value = Math.max(Math.min(issues.filter((i) => i.type === type).length, 5), 1);
+ const value = Math.max(Math.min(issues.filter((i) => i.type === type).length, MAX_RATING), 1);
return {
value: `${value}.0`,
bestValue: value === 1,
// Check one of the domains.
await user.click(ui.maintainabilityDomainBtn.get());
[
- 'New Code Smells 8',
+ 'component_measures.metric.new_maintainability_issues.name 5',
'Added Technical Debt work_duration.x_minutes.1',
'Technical Debt Ratio on New Code 1.0%',
'Maintainability Rating on New Code metric.has_rating_X.E',
- 'Code Smells 8',
+ 'component_measures.metric.maintainability_issues.name 2',
'Technical Debt work_duration.x_minutes.1',
'Technical Debt Ratio 1.0%',
'Maintainability Rating metric.has_rating_X.E',
});
});
+ it('should correctly revert to old measures when analysis is missing', async () => {
+ measuresHandler.deleteComponentMeasure('foo', MetricKey.maintainability_issues);
+ measuresHandler.deleteComponentMeasure('foo', MetricKey.new_maintainability_issues);
+
+ const { ui, user } = getPageObject();
+ renderMeasuresApp();
+ await ui.appLoaded();
+
+ // Check one of the domains.
+ await user.click(ui.maintainabilityDomainBtn.get());
+ [
+ 'component_measures.metric.new_code_smells.name 8',
+ 'Added Technical Debt work_duration.x_minutes.1',
+ 'Technical Debt Ratio on New Code 1.0%',
+ 'Maintainability Rating on New Code metric.has_rating_X.E',
+ 'component_measures.metric.code_smells.name 8',
+ 'Technical Debt work_duration.x_minutes.1',
+ 'Technical Debt Ratio 1.0%',
+ 'Maintainability Rating metric.has_rating_X.E',
+ 'Effort to Reach Maintainability Rating A work_duration.x_minutes.1',
+ ].forEach((measure) => {
+ expect(ui.measureBtn(measure).get()).toBeInTheDocument();
+ });
+ expect(screen.getByText('overview.missing_project_data.TRK')).toBeInTheDocument();
+ });
+
it('should correctly render a list view', async () => {
const { ui } = getPageObject();
renderMeasuresApp('component_measures?id=foo&metric=code_smells&view=list');
renderMeasuresApp('component_measures?id=foo&metric=open_issues');
await ui.appLoaded();
- expect(screen.getAllByText('Issues').length).toBeGreaterThan(1);
+ expect(screen.getAllByText('Issues').length).toEqual(1);
+ [
+ 'component_measures.metric.new_violations.name 1',
+ 'component_measures.metric.violations.name 1',
+ 'component_measures.metric.confirmed_issues.name 1',
+ 'component_measures.metric.accepted_issues.name 1',
+ 'component_measures.metric.new_accepted_issues.name 1',
+ 'component_measures.metric.false_positive_issues.name 1',
+ ].forEach((measure) => {
+ expect(ui.measureBtn(measure).get()).toBeInTheDocument();
+ });
});
it('should render correctly if there are no measures', async () => {
it('should correctly render a link to the activity page', async () => {
const { ui, user } = getPageObject();
- renderMeasuresApp('component_measures?id=foo&metric=new_code_smells');
+ renderMeasuresApp('component_measures?id=foo&metric=new_maintainability_issues');
await ui.appLoaded();
expect(ui.goToActivityLink.query()).not.toBeInTheDocument();
- await user.click(ui.measureBtn('Code Smells 8').get());
+ await user.click(
+ ui.measureBtn('component_measures.metric.maintainability_issues.name 2').get(),
+ );
expect(ui.goToActivityLink.get()).toHaveAttribute(
'href',
- '/project/activity?id=foo&graph=custom&custom_metrics=code_smells',
+ '/project/activity?id=foo&graph=custom&custom_metrics=maintainability_issues',
);
});
// Drilldown to the file level.
await user.click(ui.maintainabilityDomainBtn.get());
- await user.click(ui.measureBtn('Code Smells 8').get());
+ await user.click(
+ ui.measureBtn('component_measures.metric.maintainability_issues.name 2').get(),
+ );
expect(
- within(ui.measuresRow('folderA').get()).getByRole('cell', { name: '3' }),
+ within(ui.measuresRow('folderA').get()).getByRole('cell', { name: '2' }),
).toBeInTheDocument();
expect(
within(ui.measuresRow('test1.js').get()).getByRole('cell', { name: '2' }),
await user.click(ui.fileLink('folderA').get());
expect(
- within(ui.measuresRow('out.tsx').get()).getByRole('cell', { name: '1' }),
+ within(ui.measuresRow('out.tsx').get()).getByRole('cell', { name: '2' }),
).toBeInTheDocument();
expect(
within(ui.measuresRow('in.tsx').get()).getByRole('cell', { name: '2' }),
await ui.appLoaded();
await user.click(ui.maintainabilityDomainBtn.get());
- await user.click(ui.measureBtn('Code Smells 8').get());
+ await user.click(
+ ui.measureBtn('component_measures.metric.maintainability_issues.name 2').get(),
+ );
await waitFor(() => ui.changeViewToList());
expect(
- within(await ui.measuresRow('out.tsx').find()).getByRole('cell', { name: '1' }),
+ within(await ui.measuresRow('out.tsx').find()).getByRole('cell', { name: '2' }),
).toBeInTheDocument();
expect(
within(ui.measuresRow('test1.js').get()).getByRole('cell', { name: '2' }),
// Drilldown to the file level.
await user.click(ui.maintainabilityDomainBtn.get());
- await user.click(ui.measureBtn('Code Smells 8').get());
+ await user.click(
+ ui.measureBtn('component_measures.metric.maintainability_issues.name 2').get(),
+ );
await ui.arrowDown(); // Select the 1st element ("folderA")
await ui.arrowRight(); // Open "folderA"
expect(
- within(ui.measuresRow('out.tsx').get()).getByRole('cell', { name: '1' }),
+ within(ui.measuresRow('out.tsx').get()).getByRole('cell', { name: '2' }),
).toBeInTheDocument();
expect(
within(ui.measuresRow('in.tsx').get()).getByRole('cell', { name: '2' }),
await ui.arrowLeft(); // Close "folderA"
expect(
- within(ui.measuresRow('folderA').get()).getByRole('cell', { name: '3' }),
+ within(ui.measuresRow('folderA').get()).getByRole('cell', { name: '2' }),
).toBeInTheDocument();
await ui.arrowRight(); // Open "folderA"
});
it('should redirect old metric route', async () => {
+ measuresHandler.deleteComponentMeasure('foo', MetricKey.maintainability_issues);
+ measuresHandler.deleteComponentMeasure('foo', MetricKey.new_maintainability_issues);
+
const { ui } = getPageObject();
renderMeasuresApp('component_measures/metric/bugs?id=foo');
await ui.appLoaded();
- expect(ui.measureBtn('Bugs 0').get()).toHaveAttribute('aria-current', 'true');
+ expect(ui.measureBtn('component_measures.metric.bugs.name 0').get()).toHaveAttribute(
+ 'aria-current',
+ 'true',
+ );
+ });
+
+ it('should redirect old metric route for software qualities', async () => {
+ const { ui } = getPageObject();
+ renderMeasuresApp('component_measures/metric/security_issues?id=foo');
+ await ui.appLoaded();
+ expect(ui.measureBtn('component_measures.metric.security_issues.name 1').get()).toHaveAttribute(
+ 'aria-current',
+ 'true',
+ );
});
it('should redirect old domain route', async () => {
+ measuresHandler.deleteComponentMeasure('foo', MetricKey.maintainability_issues);
+ measuresHandler.deleteComponentMeasure('foo', MetricKey.new_maintainability_issues);
+
const { ui } = getPageObject();
renderMeasuresApp('component_measures/domain/bugs?id=foo');
await ui.appLoaded();
expect(ui.reliabilityDomainBtn.get()).toHaveAttribute('aria-expanded', 'true');
});
+
+ it('should redirect old domain route for software qualities', async () => {
+ const { ui } = getPageObject();
+ renderMeasuresApp('component_measures/domain/reliability_issues?id=foo');
+ await ui.appLoaded();
+ expect(ui.reliabilityDomainBtn.get()).toHaveAttribute('aria-expanded', 'true');
+ });
});
it('should allow to load more components', async () => {
import styled from '@emotion/styled';
import { Spinner } from '@sonarsource/echoes-react';
import {
+ FlagMessage,
LargeCenteredLayout,
Note,
PageContentFontWrapper,
import { getMeasuresWithPeriod } from '../../../api/measures';
import { getAllMetrics } from '../../../api/metrics';
import { ComponentContext } from '../../../app/components/componentContext/ComponentContext';
+import HelpTooltip from '../../../components/controls/HelpTooltip';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
import { enhanceMeasure } from '../../../components/measure/utils';
import '../../../components/search-navigator.css';
+import AnalysisMissingInfoMessage from '../../../components/shared/AnalysisMissingInfoMessage';
import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../../helpers/branch-like';
import { translate } from '../../../helpers/l10n';
+import { areLeakAndOverallCCTMeasuresComputed } from '../../../helpers/measures';
import { WithBranchLikesProps, useBranchesQuery } from '../../../queries/branch';
import { ComponentQualifier, isPortfolioLike } from '../../../types/component';
import { MeasurePageView } from '../../../types/measures';
fetchMeasures(metrics: State['metrics']) {
const { branchLike } = this.props;
const query = parseQuery(this.props.location.query);
- const componentKey = query.selected || this.props.component.key;
+ const componentKey =
+ query.selected !== undefined && query.selected !== ''
+ ? query.selected
+ : this.props.component.key;
const filteredKeys = getMeasuresPageMetricKeys(metrics, branchLike);
{measures.length > 0 ? (
<div className="sw-grid sw-grid-cols-12 sw-w-full">
<Sidebar
- canBrowseAllChildProjects={!!canBrowseAllChildProjects}
measures={measures}
- qualifier={qualifier}
selectedMetric={metric ? metric.key : query.metric}
showFullMeasures={showFullMeasures}
updateQuery={this.updateQuery}
/>
<div className="sw-col-span-9 sw-ml-12">
+ {!canBrowseAllChildProjects && isPortfolioLike(qualifier) && (
+ <FlagMessage className="sw-mb-4 it__portfolio_warning" variant="warning">
+ {translate('component_measures.not_all_measures_are_shown')}
+ <HelpTooltip
+ className="sw-ml-2"
+ overlay={translate('component_measures.not_all_measures_are_shown.help')}
+ />
+ </FlagMessage>
+ )}
+ {!areLeakAndOverallCCTMeasuresComputed(measures) && (
+ <AnalysisMissingInfoMessage className="sw-mb-4" qualifier={qualifier} />
+ )}
{this.renderContent(displayOverview, query, metric)}
</div>
</div>
import { getComponentMeasureUniqueKey } from '../../../helpers/component';
import { KeyboardKeys } from '../../../helpers/keycodes';
import { translate } from '../../../helpers/l10n';
-import { isDiffMetric } from '../../../helpers/measures';
+import { getCCTMeasureValue, isDiffMetric } from '../../../helpers/measures';
import { RequestData } from '../../../helpers/request';
import { isDefined } from '../../../helpers/types';
import { getProjectUrl } from '../../../helpers/urls';
}
componentDidUpdate(prevProps: Props) {
- const prevComponentKey = prevProps.selected || prevProps.rootComponent.key;
- const componentKey = this.props.selected || this.props.rootComponent.key;
+ const prevComponentKey =
+ prevProps.selected !== undefined && prevProps.selected !== ''
+ ? prevProps.selected
+ : prevProps.rootComponent.key;
+ const componentKey =
+ this.props.selected !== undefined && this.props.selected !== ''
+ ? this.props.selected
+ : this.props.rootComponent.key;
if (
prevComponentKey !== componentKey ||
!isSameBranchLike(prevProps.branchLike, this.props.branchLike) ||
const { metricKeys, opts, strategy } = this.getComponentRequestParams(view, requestedMetric, {
...(asc !== undefined && { asc }),
});
- const componentKey = selected || rootComponent.key;
+ const componentKey = selected !== undefined && selected !== '' ? selected : rootComponent.key;
const baseComponentMetrics = [requestedMetric.key];
if (requestedMetric.key === MetricKey.ncloc) {
baseComponentMetrics.push(MetricKey.ncloc_language_distribution);
return null;
}
- const measureValue =
+ const rawMeasureValue =
measure && (isDiffMetric(measure.metric) ? measure.period?.value : measure.value);
+ const measureValue = getCCTMeasureValue(metric.key, rawMeasureValue);
+
const isFileComponent = isFile(baseComponent.qualifier);
const selectedIdx = this.getSelectedIndex();
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { LinkStandalone } from '@sonarsource/echoes-react';
import classNames from 'classnames';
-import { Link, MetricsLabel, MetricsRatingBadge } from 'design-system';
+import { MetricsLabel, MetricsRatingBadge } from 'design-system';
import * as React from 'react';
import LanguageDistribution from '../../../components/charts/LanguageDistribution';
import Tooltip from '../../../components/controls/Tooltip';
import { ComponentQualifier } from '../../../types/component';
import { MetricKey, MetricType } from '../../../types/metrics';
import { ComponentMeasure, Metric, Period, Measure as TypeMeasure } from '../../../types/types';
-import { hasFullMeasures } from '../utils';
+import { getMetricSubnavigationName, hasFullMeasures } from '../utils';
import LeakPeriodLegend from './LeakPeriodLegend';
interface Props {
secondaryMeasure?: TypeMeasure;
}
-export default function MeasureHeader(props: Props) {
+export default function MeasureHeader(props: Readonly<Props>) {
const { branchLike, component, leakPeriod, measureValue, metric, secondaryMeasure } = props;
const isDiff = isDiffMetric(metric.key);
const hasHistory =
ComponentQualifier.Project,
].includes(component.qualifier as ComponentQualifier) && hasFullMeasures(branchLike);
const displayLeak = hasFullMeasures(branchLike);
+ const title = getMetricSubnavigationName(metric, getLocalizedMetricName, isDiff);
+
return (
<div className="sw-mb-4">
<div className="sw-flex sw-items-center sw-justify-between sw-gap-4">
<div className="it__measure-details-metric sw-flex sw-items-center sw-gap-1">
- <strong className="sw-body-md-highlight">{getLocalizedMetricName(metric)}</strong>
+ <strong className="sw-body-md-highlight">{title}</strong>
<div className="sw-flex sw-items-center sw-ml-2">
<Measure
{!isDiff && hasHistory && (
<Tooltip overlay={translate('component_measures.show_metric_history')}>
<span className="sw-ml-4">
- <Link
+ <LinkStandalone
className="it__show-history-link sw-font-semibold"
to={getMeasureHistoryUrl(component.key, metric.key, branchLike)}
>
{translate('component_measures.see_metric_history')}
- </Link>
+ </LinkStandalone>
</span>
</Tooltip>
)}
[domain: string]: { categories?: string[]; order: string[] };
}
+const NEW_CODE_CATEGORY = 'new_code_category';
+const OVERALL_CATEGORY = 'overall_category';
+
export const domains: Domains = {
Reliability: {
- categories: ['new_code_category', 'overall_category'],
+ categories: [NEW_CODE_CATEGORY, OVERALL_CATEGORY],
order: [
- 'new_code_category',
+ NEW_CODE_CATEGORY,
+ MetricKey.new_reliability_issues,
MetricKey.new_bugs,
MetricKey.new_reliability_rating,
MetricKey.new_reliability_remediation_effort,
- 'overall_category',
+ OVERALL_CATEGORY,
+ MetricKey.reliability_issues,
MetricKey.bugs,
MetricKey.reliability_rating,
MetricKey.reliability_remediation_effort,
},
Security: {
- categories: ['new_code_category', 'overall_category'],
+ categories: [NEW_CODE_CATEGORY, OVERALL_CATEGORY],
order: [
- 'new_code_category',
+ NEW_CODE_CATEGORY,
+ MetricKey.new_security_issues,
MetricKey.new_vulnerabilities,
MetricKey.new_security_rating,
MetricKey.new_security_remediation_effort,
- 'overall_category',
+ OVERALL_CATEGORY,
+ MetricKey.security_issues,
MetricKey.vulnerabilities,
MetricKey.security_rating,
MetricKey.security_remediation_effort,
},
SecurityReview: {
- categories: ['new_code_category', 'overall_category'],
+ categories: [NEW_CODE_CATEGORY, OVERALL_CATEGORY],
order: [
- 'new_code_category',
+ NEW_CODE_CATEGORY,
MetricKey.new_security_hotspots,
MetricKey.new_security_review_rating,
MetricKey.new_security_hotspots_reviewed,
- 'overall_category',
+ OVERALL_CATEGORY,
MetricKey.security_hotspots,
MetricKey.security_review_rating,
MetricKey.security_hotspots_reviewed,
},
Maintainability: {
- categories: ['new_code_category', 'overall_category'],
+ categories: [NEW_CODE_CATEGORY, OVERALL_CATEGORY],
order: [
- 'new_code_category',
+ NEW_CODE_CATEGORY,
+ MetricKey.new_maintainability_issues,
MetricKey.new_code_smells,
MetricKey.new_technical_debt,
MetricKey.new_sqale_debt_ratio,
MetricKey.new_maintainability_rating,
- 'overall_category',
+ OVERALL_CATEGORY,
+ MetricKey.maintainability_issues,
MetricKey.code_smells,
MetricKey.sqale_index,
MetricKey.sqale_debt_ratio,
},
Coverage: {
- categories: ['new_code_category', 'overall_category', 'tests_category'],
+ categories: [NEW_CODE_CATEGORY, OVERALL_CATEGORY, 'tests_category'],
order: [
- 'new_code_category',
+ NEW_CODE_CATEGORY,
MetricKey.new_coverage,
MetricKey.new_lines_to_cover,
MetricKey.new_uncovered_lines,
MetricKey.new_uncovered_conditions,
MetricKey.new_branch_coverage,
- 'overall_category',
+ OVERALL_CATEGORY,
MetricKey.coverage,
MetricKey.lines_to_cover,
MetricKey.uncovered_lines,
},
Duplications: {
- categories: ['new_code_category', 'overall_category'],
+ categories: [NEW_CODE_CATEGORY, OVERALL_CATEGORY],
order: [
- 'new_code_category',
+ NEW_CODE_CATEGORY,
MetricKey.new_duplicated_lines_density,
MetricKey.new_duplicated_lines,
MetricKey.new_duplicated_blocks,
- 'overall_category',
+ OVERALL_CATEGORY,
MetricKey.duplicated_lines_density,
MetricKey.duplicated_lines,
MetricKey.duplicated_blocks,
},
Issues: {
+ categories: [NEW_CODE_CATEGORY, OVERALL_CATEGORY],
order: [
+ NEW_CODE_CATEGORY,
MetricKey.new_violations,
- MetricKey.new_blocker_violations,
- MetricKey.new_critical_violations,
- MetricKey.new_major_violations,
- MetricKey.new_minor_violations,
- MetricKey.new_info_violations,
+ MetricKey.new_accepted_issues,
+ OVERALL_CATEGORY,
MetricKey.violations,
- MetricKey.blocker_violations,
- MetricKey.critical_violations,
- MetricKey.major_violations,
- MetricKey.minor_violations,
- MetricKey.info_violations,
- MetricKey.open_issues,
- MetricKey.reopened_issues,
MetricKey.confirmed_issues,
+ MetricKey.accepted_issues,
MetricKey.false_positive_issues,
],
},
import * as React from 'react';
import Measure from '../../../components/measure/Measure';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
+import { formatMeasure, getCCTMeasureValue, isDiffMetric } from '../../../helpers/measures';
import { MetricType } from '../../../types/metrics';
import { ComponentMeasureEnhanced, MeasureEnhanced, Metric } from '../../../types/types';
const getValue = (item: { leak?: string; value?: string }) =>
isDiffMetric(metric.key) ? item.leak : item.value;
- const value = getValue(measure || component);
+ const rawValue = getValue(measure || component);
+ const value = getCCTMeasureValue(metric.key, rawValue);
return (
<NumericalCell className="sw-py-3">
translate,
} from '../../../helpers/l10n';
import { MeasureEnhanced } from '../../../types/types';
-import { addMeasureCategories, hasBubbleChart, sortMeasures } from '../utils';
+import {
+ addMeasureCategories,
+ getMetricSubnavigationName,
+ hasBubbleChart,
+ sortMeasures,
+} from '../utils';
import DomainSubnavigationItem from './DomainSubnavigationItem';
interface Props {
showFullMeasures: boolean;
}
-export default function DomainSubnavigation(props: Props) {
+export default function DomainSubnavigation(props: Readonly<Props>) {
const { domain, onChange, open, selected, showFullMeasures } = props;
const helperMessageKey = `component_measures.domain_subnavigation.${domain.name}.help`;
const helper = hasMessage(helperMessageKey) ? translate(helperMessageKey) : undefined;
<DomainSubnavigationItem
key={item.metric.key}
measure={item}
- name={translateMetric(item.metric)}
+ name={getMetricSubnavigationName(item.metric, translateMetric)}
onChange={onChange}
selected={selected}
/>
selected: string;
}
-export default function DomainSubnavigationItem({ measure, name, onChange, selected }: Props) {
+export default function DomainSubnavigationItem({
+ measure,
+ name,
+ onChange,
+ selected,
+}: Readonly<Props>) {
const { key } = measure.metric;
return (
<SubnavigationItem active={key === selected} key={key} onClick={onChange} value={key}>
import styled from '@emotion/styled';
import {
BareButton,
- FlagMessage,
LAYOUT_FOOTER_HEIGHT,
LAYOUT_GLOBAL_NAV_HEIGHT,
LAYOUT_PROJECT_NAV_HEIGHT,
} from 'design-system';
import * as React from 'react';
import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
-import HelpTooltip from '../../../components/controls/HelpTooltip';
import { translate } from '../../../helpers/l10n';
import useFollowScroll from '../../../hooks/useFollowScroll';
-import { isPortfolioLike } from '../../../types/component';
import { MeasureEnhanced } from '../../../types/types';
-import { PROJECT_OVERVEW, Query, groupByDomains, isProjectOverview } from '../utils';
+import { PROJECT_OVERVEW, Query, isProjectOverview, populateDomainsFromMeasures } from '../utils';
import DomainSubnavigation from './DomainSubnavigation';
+import { Domain } from '../../../types/measures';
interface Props {
- canBrowseAllChildProjects: boolean;
measures: MeasureEnhanced[];
- qualifier: string;
selectedMetric: string;
showFullMeasures: boolean;
updateQuery: (query: Partial<Query>) => void;
}
-export default function Sidebar(props: Props) {
- const {
- showFullMeasures,
- canBrowseAllChildProjects,
- qualifier,
- updateQuery,
- selectedMetric,
- measures,
- } = props;
+export default function Sidebar(props: Readonly<Props>) {
+ const { showFullMeasures, updateQuery, selectedMetric, measures } = props;
const { top: topScroll, scrolledOnce } = useFollowScroll();
+ const domains = populateDomainsFromMeasures(measures);
const handleChangeMetric = React.useCallback(
(metric: string) => {
)`,
}}
>
- {!canBrowseAllChildProjects && isPortfolioLike(qualifier) && (
- <FlagMessage className="sw-mt-4 it__portfolio_warning" variant="warning">
- {translate('component_measures.not_all_measures_are_shown')}
- <HelpTooltip
- className="sw-ml-2"
- overlay={translate('component_measures.not_all_measures_are_shown.help')}
- />
- </FlagMessage>
- )}
<section
className="sw-flex sw-flex-col sw-gap-4 sw-p-4"
aria-label={translate('component_measures.navigation')}
</SubnavigationItem>
</SubnavigationGroup>
- {groupByDomains(measures).map((domain: Domain) => (
+ {domains.map((domain: Domain) => (
<DomainSubnavigation
domain={domain}
key={domain.name}
);
}
-interface Domain {
- measures: MeasureEnhanced[];
- name: string;
-}
-
function isDomainSelected(selectedMetric: string, domain: Domain) {
return (
selectedMetric === domain.name ||
measure: MeasureEnhanced;
}
-export default function SubnavigationMeasureValue({ measure }: Props) {
+export default function SubnavigationMeasureValue({ measure }: Readonly<Props>) {
const isDiff = isDiffMetric(measure.metric.key);
const value = isDiff ? measure.leak : measure.value;
const formatted = formatMeasure(value, MetricType.Rating);
import { groupBy, memoize, sortBy, toPairs } from 'lodash';
import { enhanceMeasure } from '../../components/measure/utils';
import { isBranch, isPullRequest } from '../../helpers/branch-like';
-import { HIDDEN_METRICS } from '../../helpers/constants';
-import { getLocalizedMetricName } from '../../helpers/l10n';
-import { MEASURES_REDIRECTION, getDisplayMetrics, isDiffMetric } from '../../helpers/measures';
+import {
+ CCT_SOFTWARE_QUALITY_METRICS,
+ HIDDEN_METRICS,
+ LEAK_CCT_SOFTWARE_QUALITY_METRICS,
+ LEAK_OLD_TAXONOMY_METRICS,
+ OLD_TAXONOMY_METRICS,
+} from '../../helpers/constants';
+import { getLocalizedMetricName, translate } from '../../helpers/l10n';
+import {
+ MEASURES_REDIRECTION,
+ areLeakCCTMeasuresComputed,
+ areCCTMeasuresComputed,
+ getDisplayMetrics,
+ isDiffMetric,
+ getCCTMeasureValue,
+} from '../../helpers/measures';
import {
cleanQuery,
parseAsOptionalBoolean,
} from '../../helpers/query';
import { BranchLike } from '../../types/branch-like';
import { ComponentQualifier } from '../../types/component';
-import { MeasurePageView } from '../../types/measures';
+import { Domain, MeasurePageView } from '../../types/measures';
import { MetricKey, MetricType } from '../../types/metrics';
import {
ComponentMeasure,
export const DEFAULT_METRIC = PROJECT_OVERVEW;
export const KNOWN_DOMAINS = [
'Releasability',
- 'Reliability',
'Security',
- 'SecurityReview',
+ 'Reliability',
'Maintainability',
+ 'SecurityReview',
'Coverage',
'Duplications',
'Size',
'Complexity',
];
+const CCT_METRIC_DOMAIN_MAP: Dict<string> = {
+ [MetricKey.security_issues]: 'Security',
+ [MetricKey.new_security_issues]: 'Security',
+ [MetricKey.reliability_issues]: 'Reliability',
+ [MetricKey.new_reliability_issues]: 'Reliability',
+ [MetricKey.maintainability_issues]: 'Maintainability',
+ [MetricKey.new_maintainability_issues]: 'Maintainability',
+};
+
+const DEPRECATED_METRICS = [
+ MetricKey.blocker_violations,
+ MetricKey.new_blocker_violations,
+ MetricKey.critical_violations,
+ MetricKey.new_critical_violations,
+ MetricKey.major_violations,
+ MetricKey.new_major_violations,
+ MetricKey.info_violations,
+ MetricKey.new_info_violations,
+ MetricKey.minor_violations,
+ MetricKey.new_minor_violations,
+ MetricKey.high_impact_accepted_issues,
+];
+
+const ISSUES_METRICS = [
+ MetricKey.accepted_issues,
+ MetricKey.new_accepted_issues,
+ MetricKey.confirmed_issues,
+ MetricKey.false_positive_issues,
+ MetricKey.violations,
+ MetricKey.new_violations,
+];
+
+export const populateDomainsFromMeasures = memoize((measures: MeasureEnhanced[]): Domain[] => {
+ let populatedMeasures = measures
+ .filter((measure) => !DEPRECATED_METRICS.includes(measure.metric.key as MetricKey))
+ .map((measure) => {
+ const isDiff = isDiffMetric(measure.metric.key);
+ const calculatedValue = getCCTMeasureValue(
+ measure.metric.key,
+ isDiff ? measure.leak : measure.value,
+ );
+
+ return {
+ ...measure,
+ metric: {
+ ...measure.metric,
+ domain: CCT_METRIC_DOMAIN_MAP[measure.metric.key] ?? measure.metric.domain,
+ },
+ ...(!isDiff && { value: calculatedValue }),
+ ...(isDiff && { leak: calculatedValue }),
+ };
+ });
+ if (areLeakCCTMeasuresComputed(measures)) {
+ populatedMeasures = populatedMeasures.filter(
+ (measure) => !LEAK_OLD_TAXONOMY_METRICS.includes(measure.metric.key as MetricKey),
+ );
+ }
+ if (areCCTMeasuresComputed(measures)) {
+ populatedMeasures = populatedMeasures.filter(
+ (measure) => !OLD_TAXONOMY_METRICS.includes(measure.metric.key as MetricKey),
+ );
+ }
+
+ return groupByDomains(populatedMeasures);
+});
+
+export function getMetricSubnavigationName(
+ metric: Metric,
+ translateFn: (metric: Metric) => string,
+ isDiff = false,
+) {
+ if (
+ [
+ ...LEAK_CCT_SOFTWARE_QUALITY_METRICS,
+ ...CCT_SOFTWARE_QUALITY_METRICS,
+ ...ISSUES_METRICS,
+ ...OLD_TAXONOMY_METRICS,
+ ...LEAK_OLD_TAXONOMY_METRICS,
+ ].includes(metric.key as MetricKey)
+ ) {
+ return translate(
+ `component_measures.metric.${metric.key}.${isDiff ? 'detailed_name' : 'name'}`,
+ );
+ }
+ return translateFn(metric);
+}
+
export function filterMeasures(measures: MeasureEnhanced[]): MeasureEnhanced[] {
return measures.filter((measure) => !HIDDEN_METRICS.includes(measure.metric.key as MetricKey));
}
MetricKey.maintainability_issues,
];
+export const LEAK_CCT_SOFTWARE_QUALITY_METRICS = [
+ MetricKey.new_security_issues,
+ MetricKey.new_reliability_issues,
+ MetricKey.new_maintainability_issues,
+];
+
export const OLD_TAXONOMY_METRICS = [
MetricKey.vulnerabilities,
MetricKey.bugs,
MetricKey.code_smells,
];
+export const LEAK_OLD_TAXONOMY_METRICS = [
+ MetricKey.new_vulnerabilities,
+ MetricKey.new_bugs,
+ MetricKey.new_code_smells,
+];
+
export const OLD_TO_NEW_TAXONOMY_METRICS_MAP: { [key in MetricKey]?: MetricKey } = {
[MetricKey.vulnerabilities]: MetricKey.security_issues,
[MetricKey.bugs]: MetricKey.reliability_issues,
QualityGateStatusConditionEnhanced,
} from '../types/quality-gates';
import { Dict, Measure, MeasureEnhanced, Metric } from '../types/types';
-import { CCT_SOFTWARE_QUALITY_METRICS, ONE_SECOND } from './constants';
+import {
+ CCT_SOFTWARE_QUALITY_METRICS,
+ LEAK_CCT_SOFTWARE_QUALITY_METRICS,
+ ONE_SECOND,
+} from './constants';
import { translate, translateWithParameters } from './l10n';
import { getCurrentLocale } from './l10nBundle';
import { isDefined } from './types';
}
export function getDisplayMetrics(metrics: Metric[]) {
- return metrics.filter((metric) => !metric.hidden && !['DATA', 'DISTRIB'].includes(metric.type));
+ return metrics.filter(
+ (metric) =>
+ !metric.hidden &&
+ ([...CCT_SOFTWARE_QUALITY_METRICS, ...LEAK_CCT_SOFTWARE_QUALITY_METRICS].includes(
+ metric.key as MetricKey,
+ ) ||
+ ![MetricType.Data, MetricType.Distribution].includes(metric.type as MetricType)),
+ );
}
export function findMeasure(measures: MeasureEnhanced[], metric: MetricKey | string) {
return measures.find((measure) => measure.metric.key === metric);
}
+export function areLeakCCTMeasuresComputed(measures?: Measure[] | MeasureEnhanced[]) {
+ return LEAK_CCT_SOFTWARE_QUALITY_METRICS.every((metric) =>
+ measures?.find((measure) =>
+ isMeasureEnhanced(measure) ? measure.metric.key === metric : measure.metric === metric,
+ ),
+ );
+}
+
export function areCCTMeasuresComputed(measures?: Measure[] | MeasureEnhanced[]) {
return CCT_SOFTWARE_QUALITY_METRICS.every((metric) =>
measures?.find((measure) =>
);
}
+export function areLeakAndOverallCCTMeasuresComputed(measures?: Measure[] | MeasureEnhanced[]) {
+ return areLeakCCTMeasuresComputed(measures) && areCCTMeasuresComputed(measures);
+}
+
function isMeasureEnhanced(measure: Measure | MeasureEnhanced): measure is MeasureEnhanced {
return (measure.metric as Metric)?.key !== undefined;
}
+export const getCCTMeasureValue = (key: string, value?: string) => {
+ if (
+ CCT_SOFTWARE_QUALITY_METRICS.concat(LEAK_CCT_SOFTWARE_QUALITY_METRICS).includes(
+ key as MetricKey,
+ ) &&
+ value !== undefined
+ ) {
+ return JSON.parse(value).total;
+ }
+ return value;
+};
+
const HOURS_IN_DAY = 8;
type Formatter = (value: string | number, options?: Dict<unknown>) => string;
},
reliability_issues: {
key: 'reliability_issues',
- type: 'INT',
+ type: 'DATA',
name: 'Reliability',
description: 'Reliability issues',
- direction: -1,
- qualitative: true,
+ direction: 0,
+ qualitative: false,
+ hidden: false,
+ },
+ new_reliability_issues: {
+ key: 'new_reliability_issues',
+ type: 'DATA',
+ name: 'New Reliability',
+ description: 'New Reliability issues',
+ direction: 0,
+ qualitative: false,
hidden: false,
},
reliability_rating: {
},
security_issues: {
key: 'security_issues',
- type: 'INT',
+ type: 'DATA',
name: 'Security',
description: 'Security issues',
- direction: -1,
- qualitative: true,
+ direction: 0,
+ qualitative: false,
+ hidden: false,
+ },
+ new_security_issues: {
+ key: 'new_security_issues',
+ type: 'DATA',
+ name: 'Security',
+ description: 'New Security issues',
+ domain: 'Issues',
+ direction: 0,
+ qualitative: false,
hidden: false,
},
security_rating: {
},
maintainability_issues: {
key: 'maintainability_issues',
- type: 'INT',
+ type: 'DATA',
name: 'Maintainability',
description: 'Maintainability issues',
- direction: -1,
- qualitative: true,
+ domain: 'Issues',
+ direction: 0,
+ qualitative: false,
+ hidden: false,
+ },
+ new_maintainability_issues: {
+ key: 'new_maintainability_issues',
+ type: 'DATA',
+ name: 'Maintainability',
+ description: 'New Maintainability issues',
+ domain: 'Issues',
+ direction: 0,
+ qualitative: false,
hidden: false,
},
sqale_index: {
qualitative: false,
hidden: false,
},
+ new_accepted_issues: {
+ key: 'new_accepted_issues',
+ type: 'INT',
+ name: 'New Accepted Issues',
+ description: 'New Accepted issues',
+ domain: 'Issues',
+ direction: -1,
+ qualitative: false,
+ hidden: false,
+ },
};
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { ComponentMeasure, Metric, Period, PeriodMeasure } from './types';
+import { ComponentMeasure, MeasureEnhanced, Metric, Period, PeriodMeasure } from './types';
export interface MeasuresForProjects {
component: string;
tree = 'tree',
treemap = 'treemap',
}
+
+export interface Domain {
+ measures: MeasureEnhanced[];
+ name: string;
+}
new_line_coverage = 'new_line_coverage',
new_lines = 'new_lines',
new_lines_to_cover = 'new_lines_to_cover',
+ new_maintainability_issues = 'new_maintainability_issues',
new_maintainability_rating = 'new_maintainability_rating',
new_maintainability_rating_distribution = 'new_maintainability_rating_distribution',
new_major_violations = 'new_major_violations',
new_minor_violations = 'new_minor_violations',
+ new_reliability_issues = 'new_reliability_issues',
new_reliability_rating = 'new_reliability_rating',
new_reliability_remediation_effort = 'new_reliability_remediation_effort',
new_reliability_rating_distribution = 'new_reliability_rating_distribution',
new_security_hotspots = 'new_security_hotspots',
new_security_hotspots_reviewed = 'new_security_hotspots_reviewed',
+ new_security_issues = 'new_security_issues',
new_security_rating = 'new_security_rating',
new_security_rating_distribution = 'new_security_rating_distribution',
new_security_remediation_effort = 'new_security_remediation_effort',
component_measures.not_all_measures_are_shown=Not all projects and applications are included
component_measures.not_all_measures_are_shown.help=You do not have access to all projects and/or applications. Measures are still computed based on all projects and applications.
+component_measures.metric.new_security_issues.name=Issues
+component_measures.metric.new_security_issues.detailed_name=New Issues
+component_measures.metric.new_vulnerabilities.name=Issues
+component_measures.metric.new_vulnerabilities.detailed_name=New Issues
+component_measures.metric.new_reliability_issues.name=Issues
+component_measures.metric.new_reliability_issues.detailed_name=New Issues
+component_measures.metric.new_maintainability_issues.name=Issues
+component_measures.metric.new_maintainability_issues.detailed_name=New Issues
+component_measures.metric.new_code_smells.name=Issues
+component_measures.metric.new_code_smells.detailed_name=New Issues
+component_measures.metric.new_violations.name=Open Issues
+component_measures.metric.new_violations.detailed_name=New Open Issues
+component_measures.metric.new_accepted_issues.name=Accepted Issues
+component_measures.metric.new_accepted_issues.detailed_name=New Accepted Issues
+component_measures.metric.new_bugs.name=Issues
+component_measures.metric.new_bugs.detailed_name=New Issues
+component_measures.metric.security_issues.name=Issues
+component_measures.metric.vulnerabilities.name=Issues
+component_measures.metric.reliability_issues.name=Issues
+component_measures.metric.bugs.name=Issues
+component_measures.metric.maintainability_issues.name=Issues
+component_measures.metric.code_smells.name=Issues
+component_measures.metric.violations.name=Open Issues
+component_measures.metric.accepted_issues.name=Accepted Issues
+component_measures.metric.confirmed_issues.name=Confirmed Issues
+component_measures.metric.false_positive_issues.name=False Positive Issues
+
#------------------------------------------------------------------------------
#
# DOCS