aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps
diff options
context:
space:
mode:
authorstanislavh <stanislav.honcharov@sonarsource.com>2024-08-09 15:15:03 +0200
committersonartech <sonartech@sonarsource.com>2024-08-26 20:03:06 +0000
commitcb110bccb888b7089a12ffc6e03ed41f9b70749d (patch)
treea844f0cd93e39d6c0f6d207a4666e57812a23eb0 /server/sonar-web/src/main/js/apps
parent4131eba5b8113c3cd5fdd23fcad871c128a5c297 (diff)
downloadsonarqube-cb110bccb888b7089a12ffc6e03ed41f9b70749d.tar.gz
sonarqube-cb110bccb888b7089a12ffc6e03ed41f9b70749d.zip
SONAR-22719 Measures page reflects new ratings
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Component.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx48
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx309
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx593
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx248
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx143
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasuresBreadcrumbs.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/config/bubbles.ts89
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/config/domains.ts13
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChartView.tsx237
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/ColorRatingsLegend.tsx24
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx25
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/hooks.ts42
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigation.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/SubnavigationMeasureValue.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/utils.ts40
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/NewCodeMeasuresPanel.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/OverallCodeMeasuresPanel.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/QualityGateCondition.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureCard.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureRating.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx18
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSidebarHeader.tsx1
29 files changed, 882 insertions, 1011 deletions
diff --git a/server/sonar-web/src/main/js/apps/code/components/Component.tsx b/server/sonar-web/src/main/js/apps/code/components/Component.tsx
index 133f2331c77..3377e14205e 100644
--- a/server/sonar-web/src/main/js/apps/code/components/Component.tsx
+++ b/server/sonar-web/src/main/js/apps/code/components/Component.tsx
@@ -104,7 +104,12 @@ export default function Component(props: Props) {
</ContentCell>
{metrics.map((metric) => (
- <ComponentMeasure component={component} key={metric.key} metric={metric} />
+ <ComponentMeasure
+ component={component}
+ branchLike={branchLike}
+ key={metric.key}
+ metric={metric}
+ />
))}
{showAnalysisDate && (
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 2e76c81919d..ba8d3b50824 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
@@ -34,16 +34,18 @@ import {
areCCTMeasuresComputed as areCCTMeasuresComputedFn,
isDiffMetric,
} from '../../../helpers/measures';
+import { BranchLike } from '../../../types/branch-like';
import { isApplication, isProject } from '../../../types/component';
import { Metric, ComponentMeasure as TypeComponentMeasure } from '../../../types/types';
interface Props {
+ branchLike?: BranchLike;
component: TypeComponentMeasure;
metric: Metric;
}
export default function ComponentMeasure(props: Props) {
- const { component, metric } = props;
+ const { component, metric, branchLike } = props;
const isProjectLike = isProject(component.qualifier) || isApplication(component.qualifier);
const isReleasability = metric.key === MetricKey.releasability_rating;
@@ -89,13 +91,18 @@ export default function ComponentMeasure(props: Props) {
case MetricType.Rating:
return (
<RatingCell className="sw-whitespace-nowrap">
- <RatingComponent componentKey={component.key} ratingMetric={metric.key as MetricKey} />
+ <RatingComponent
+ branchLike={branchLike}
+ componentKey={component.key}
+ ratingMetric={metric.key as MetricKey}
+ />
</RatingCell>
);
default:
return (
<NumericalCell className="sw-whitespace-nowrap">
<Measure
+ branchLike={branchLike}
componentKey={component.key}
metricKey={finalMetricKey}
metricType={finalMetricType}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx b/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx
index 6cc9c75fe76..50046f5f08d 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx
@@ -27,6 +27,7 @@ import BranchesServiceMock from '../../../api/mocks/BranchesServiceMock';
import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock';
import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock';
import { MeasuresServiceMock } from '../../../api/mocks/MeasuresServiceMock';
+import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
import { mockComponent } from '../../../helpers/mocks/component';
import { mockMeasure, mockMetric } from '../../../helpers/testMocks';
import { renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils';
@@ -53,22 +54,23 @@ const componentsHandler = new ComponentsServiceMock();
const measuresHandler = new MeasuresServiceMock();
const issuesHandler = new IssuesServiceMock();
const branchHandler = new BranchesServiceMock();
+const settingsHandler = new SettingsServiceMock();
afterEach(() => {
componentsHandler.reset();
measuresHandler.reset();
issuesHandler.reset();
branchHandler.reset();
+ settingsHandler.reset();
});
describe('rendering', () => {
it('should correctly render the default overview and navigation', async () => {
const { ui, user } = getPageObject();
renderMeasuresApp();
- await ui.appLoaded();
// Overview.
- expect(ui.seeDataAsListLink.get()).toBeInTheDocument();
+ expect(await ui.seeDataAsListLink.find()).toBeInTheDocument();
expect(ui.overviewDomainLink.get()).toHaveAttribute('aria-current', 'true');
expect(ui.bubbleChart.get()).toBeInTheDocument();
expect(within(ui.bubbleChart.get()).getAllByRole('link')).toHaveLength(8);
@@ -91,11 +93,11 @@ describe('rendering', () => {
'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',
+ 'Maintainability Rating on New Code metric.has_rating_X.E metric.sqale_rating.tooltip.E.0.0%',
'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',
+ 'Maintainability Rating metric.has_rating_X.E metric.sqale_rating.tooltip.E.0.0%',
'Effort to Reach Maintainability Rating A work_duration.x_minutes.1',
].forEach((measure) => {
expect(ui.measureLink(measure).get()).toBeInTheDocument();
@@ -116,11 +118,11 @@ describe('rendering', () => {
'component_measures.metric.new_code_smells.name 9',
'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',
+ 'Maintainability Rating on New Code metric.has_rating_X.E metric.sqale_rating.tooltip.E.0.0%',
'component_measures.metric.code_smells.name 9',
'Technical Debt work_duration.x_minutes.1',
'Technical Debt Ratio 1.0%',
- 'Maintainability Rating metric.has_rating_X.E',
+ 'Maintainability Rating metric.has_rating_X.E metric.sqale_rating.tooltip.E.0.0%',
'Effort to Reach Maintainability Rating A work_duration.x_minutes.1',
].forEach((measure) => {
expect(ui.measureLink(measure).get()).toBeInTheDocument();
@@ -131,27 +133,26 @@ describe('rendering', () => {
it('should correctly render a list view', async () => {
const { ui } = getPageObject();
renderMeasuresApp('component_measures?id=foo&metric=code_smells&view=list');
- await ui.appLoaded();
- expect(ui.measuresTable.get()).toBeInTheDocument();
+ expect(await ui.measuresTable.find()).toBeInTheDocument();
expect(ui.measuresRows.getAll()).toHaveLength(8);
});
it('should correctly render a tree view', async () => {
const { ui } = getPageObject();
renderMeasuresApp('component_measures?id=foo&metric=code_smells&view=tree');
- await ui.appLoaded();
- expect(ui.measuresTable.get()).toBeInTheDocument();
+ expect(await ui.measuresTable.find()).toBeInTheDocument();
expect(ui.measuresRows.getAll()).toHaveLength(7);
});
it('should correctly render a rating treemap view', async () => {
const { ui } = getPageObject();
renderMeasuresApp('component_measures?id=foo&metric=sqale_rating&view=treemap');
- await ui.appLoaded();
- expect(ui.treeMap.byRole('link').getAll()).toHaveLength(7);
+ await waitFor(() => {
+ expect(ui.treeMap.byRole('link').getAll()).toHaveLength(7);
+ });
expect(ui.treeMapCell(/folderA .+ Maintainability Rating: C/).get()).toBeInTheDocument();
expect(ui.treeMapCell(/test1\.js .+ Maintainability Rating: B/).get()).toBeInTheDocument();
expect(ui.treeMapCell(/index\.tsx .+ Maintainability Rating: A/).get()).toBeInTheDocument();
@@ -175,9 +176,10 @@ describe('rendering', () => {
const { ui } = getPageObject();
renderMeasuresApp('component_measures?id=foo&metric=coverage&view=treemap');
- await ui.appLoaded();
- expect(ui.treeMap.byRole('link').getAll()).toHaveLength(7);
+ await waitFor(() => {
+ expect(ui.treeMap.byRole('link').getAll()).toHaveLength(7);
+ });
expect(ui.treeMapCell(/folderA .+ Coverage: 74.2%/).get()).toBeInTheDocument();
expect(ui.treeMapCell(/test1\.js .+ Coverage: —/).get()).toBeInTheDocument();
@@ -246,11 +248,9 @@ describe('rendering', () => {
});
it('should correctly render the language distribution', async () => {
- const { ui } = getPageObject();
renderMeasuresApp('component_measures?id=foo&metric=ncloc');
- await ui.appLoaded();
- expect(screen.getByText('10short_number_suffix.k')).toBeInTheDocument();
+ expect(await screen.findByText('10short_number_suffix.k')).toBeInTheDocument();
expect(screen.getByText('java')).toBeInTheDocument();
expect(screen.getByText('5short_number_suffix.k')).toBeInTheDocument();
expect(screen.getByText('javascript')).toBeInTheDocument();
@@ -294,9 +294,8 @@ describe('rendering', () => {
const { ui, user } = getPageObject();
renderMeasuresApp('component_measures?id=foo&metric=sqale_rating&view=list');
- await ui.appLoaded();
- expect(ui.notShowingAllComponentsTxt.get()).toBeInTheDocument();
+ expect(await ui.notShowingAllComponentsTxt.find()).toBeInTheDocument();
await user.click(ui.showAllBtn.get());
expect(ui.notShowingAllComponentsTxt.query()).not.toBeInTheDocument();
});
@@ -401,7 +400,13 @@ describe('navigation', () => {
await ui.appLoaded();
await user.click(ui.maintainabilityDomainBtn.get());
- await user.click(ui.measureLink('Maintainability Rating metric.has_rating_X.E').get());
+ await user.click(
+ ui
+ .measureLink(
+ 'Maintainability Rating metric.has_rating_X.E metric.sqale_rating.tooltip.E.0.0%',
+ )
+ .get(),
+ );
// Click treemap option in view select
await user.click(ui.viewSelect.get());
@@ -528,8 +533,7 @@ it('should allow to load more components', async () => {
const { ui, user } = getPageObject();
renderMeasuresApp('component_measures?id=foo&metric=code_smells&view=list');
- await ui.appLoaded();
- await user.click(ui.showAllBtn.get());
+ await user.click(await ui.showAllBtn.find());
expect(ui.showingOutOfTxt('500', '1,008').get()).toBeInTheDocument();
await ui.clickLoadMore();
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
index b5c6128f0e7..b3f01a65d3b 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
@@ -28,29 +28,29 @@ import {
themeBorder,
themeColor,
} from 'design-system';
-import { keyBy } from 'lodash';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import HelpTooltip from '~sonar-aligned/components/controls/HelpTooltip';
-import { withRouter } from '~sonar-aligned/components/hoc/withRouter';
+import { useLocation, useRouter } from '~sonar-aligned/components/hoc/withRouter';
import { getBranchLikeQuery, isPullRequest } from '~sonar-aligned/helpers/branch-like';
import { isPortfolioLike } from '~sonar-aligned/helpers/component';
import { ComponentQualifier } from '~sonar-aligned/types/component';
import { MetricKey } from '~sonar-aligned/types/metrics';
-import { Location, Router } from '~sonar-aligned/types/router';
-import { getMeasuresWithPeriod } from '../../../api/measures';
-import { getAllMetrics } from '../../../api/metrics';
import { ComponentContext } from '../../../app/components/componentContext/ComponentContext';
+import { useMetrics } from '../../../app/components/metrics/withMetricsContext';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import { enhanceMeasure } from '../../../components/measure/utils';
import '../../../components/search-navigator.css';
import AnalysisMissingInfoMessage from '../../../components/shared/AnalysisMissingInfoMessage';
-import { isSameBranchLike } from '../../../helpers/branch-like';
import { translate } from '../../../helpers/l10n';
-import { areCCTMeasuresComputed } from '../../../helpers/measures';
-import { WithBranchLikesProps, useBranchesQuery } from '../../../queries/branch';
+import {
+ areCCTMeasuresComputed,
+ areSoftwareQualityRatingsComputed,
+} from '../../../helpers/measures';
+import { useBranchesQuery } from '../../../queries/branch';
+import { useMeasuresComponentQuery } from '../../../queries/measures';
import { MeasurePageView } from '../../../types/measures';
-import { ComponentMeasure, Dict, MeasureEnhanced, Metric, Period } from '../../../types/types';
+import { useBubbleChartMetrics } from '../hooks';
import Sidebar from '../sidebar/Sidebar';
import {
Query,
@@ -67,123 +67,54 @@ import {
sortMeasures,
} from '../utils';
import MeasureContent from './MeasureContent';
-import MeasureOverviewContainer from './MeasureOverviewContainer';
+import MeasureOverview from './MeasureOverview';
import MeasuresEmpty from './MeasuresEmpty';
-interface Props extends WithBranchLikesProps {
- component: ComponentMeasure;
- location: Location;
- router: Router;
-}
-
-interface State {
- leakPeriod?: Period;
- loading: boolean;
- measures: MeasureEnhanced[];
- metrics: Dict<Metric>;
-}
-
-class ComponentMeasuresApp extends React.PureComponent<Props, State> {
- mounted = false;
- state: State;
-
- constructor(props: Props) {
- super(props);
-
- this.state = {
- loading: true,
- measures: [],
- metrics: {},
- };
- }
-
- componentDidMount() {
- this.mounted = true;
-
- getAllMetrics().then(
- (metrics) => {
- const byKey = keyBy(metrics, 'key');
- this.setState({ metrics: byKey });
- },
- () => {},
+export default function ComponentMeasuresApp() {
+ const { component } = React.useContext(ComponentContext);
+ const { data: { branchLike } = {} } = useBranchesQuery(component);
+ const { query: rawQuery, pathname } = useLocation();
+ const query = parseQuery(rawQuery);
+ const router = useRouter();
+ const metrics = useMetrics();
+ const filteredMetrics = getMeasuresPageMetricKeys(metrics, branchLike);
+ const componentKey =
+ query.selected !== undefined && query.selected !== '' ? query.selected : component?.key ?? '';
+
+ const { data: { component: componentWithMeasures, period } = {}, isLoading } =
+ useMeasuresComponentQuery(
+ { componentKey, metricKeys: filteredMetrics, branchLike },
+ { enabled: Boolean(componentKey) },
);
- }
-
- componentDidUpdate(prevProps: Props, prevState: State) {
- const prevQuery = parseQuery(prevProps.location.query);
- const query = parseQuery(this.props.location.query);
-
- const hasSelectedQueryChanged = prevQuery.selected !== query.selected;
-
- const hasBranchChanged = !isSameBranchLike(prevProps.branchLike, this.props.branchLike);
-
- const isBranchReady =
- isPortfolioLike(this.props.component.qualifier) || this.props.branchLike !== undefined;
- const haveMetricsChanged =
- Object.keys(this.state.metrics).length !== Object.keys(prevState.metrics).length;
-
- const areMetricsReady = Object.keys(this.state.metrics).length > 0;
-
- if (
- areMetricsReady &&
- isBranchReady &&
- (haveMetricsChanged || hasBranchChanged || hasSelectedQueryChanged)
- ) {
- this.fetchMeasures(this.state.metrics);
- }
+ const measures = (
+ componentWithMeasures
+ ? filterMeasures(
+ banQualityGateMeasure(componentWithMeasures).map((measure) =>
+ enhanceMeasure(measure, metrics),
+ ),
+ )
+ : []
+ ).filter((measure) => measure.value !== undefined || measure.leak !== undefined);
+ const bubblesByDomain = useBubbleChartMetrics(measures);
+
+ const leakPeriod =
+ componentWithMeasures?.qualifier === ComponentQualifier.Project ? period : undefined;
+ const displayOverview = hasBubbleChart(bubblesByDomain, query.metric);
+
+ if (!component) {
+ return null;
}
- componentWillUnmount() {
- this.mounted = false;
- }
-
- fetchMeasures(metrics: State['metrics']) {
- const { branchLike } = this.props;
- const query = parseQuery(this.props.location.query);
- const componentKey =
- query.selected !== undefined && query.selected !== ''
- ? query.selected
- : this.props.component.key;
-
- const filteredKeys = getMeasuresPageMetricKeys(metrics, branchLike);
-
- getMeasuresWithPeriod(componentKey, filteredKeys, getBranchLikeQuery(branchLike)).then(
- ({ component, period }) => {
- if (this.mounted) {
- const measures = filterMeasures(
- banQualityGateMeasure(component).map((measure) => enhanceMeasure(measure, metrics)),
- );
- const leakPeriod =
- component.qualifier === ComponentQualifier.Project ? period : undefined;
-
- this.setState({
- loading: false,
- leakPeriod,
- measures: measures.filter(
- (measure) => measure.value !== undefined || measure.leak !== undefined,
- ),
- });
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- },
- );
- }
-
- getSelectedMetric = (query: Query, displayOverview: boolean) => {
+ const getSelectedMetric = (query: Query, displayOverview: boolean) => {
if (displayOverview) {
return undefined;
}
- const metric = this.state.metrics[query.metric];
+ const metric = metrics[query.metric];
if (!metric) {
- const domainMeasures = groupByDomains(this.state.measures);
-
+ const domainMeasures = groupByDomains(measures);
const firstMeasure =
domainMeasures[0] && sortMeasures(domainMeasures[0].name, domainMeasures[0].measures)[0];
@@ -194,10 +125,11 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> {
return metric;
};
- updateQuery = (newQuery: Partial<Query>) => {
- const query: Query = { ...parseQuery(this.props.location.query), ...newQuery };
+ const metric = getSelectedMetric(query, displayOverview);
- const metric = this.getSelectedMetric(query, false);
+ const updateQuery = (newQuery: Partial<Query>) => {
+ const nextQuery: Query = { ...parseQuery(query), ...newQuery };
+ const metric = getSelectedMetric(nextQuery, false);
if (metric) {
if (query.view === MeasurePageView.treemap && !hasTreemap(metric.key, metric.type)) {
@@ -207,32 +139,27 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> {
}
}
- this.props.router.push({
- pathname: this.props.location.pathname,
+ router.push({
+ pathname,
query: {
- ...serializeQuery(query),
- ...getBranchLikeQuery(this.props.branchLike),
- id: this.props.component.key,
+ ...serializeQuery(nextQuery),
+ ...getBranchLikeQuery(branchLike),
+ id: component?.key,
},
});
};
- renderContent = (displayOverview: boolean, query: Query, metric?: Metric) => {
- const { branchLike, component } = this.props;
- const { leakPeriod } = this.state;
+ const showFullMeasures = hasFullMeasures(branchLike);
+ const renderContent = () => {
if (displayOverview) {
return (
<StyledMain className="sw-rounded-1 sw-mb-4">
- <MeasureOverviewContainer
- branchLike={branchLike}
- domain={query.metric}
+ <MeasureOverview
+ bubblesByDomain={bubblesByDomain}
leakPeriod={leakPeriod}
- metrics={this.state.metrics}
rootComponent={component}
- router={this.props.router}
- selected={query.selected}
- updateQuery={this.updateQuery}
+ updateQuery={updateQuery}
/>
</StyledMain>
);
@@ -261,90 +188,62 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> {
return (
<StyledMain className="sw-rounded-1 sw-mb-4">
<MeasureContent
- asc={query.asc}
- branchLike={branchLike}
leakPeriod={leakPeriod}
- metrics={this.state.metrics}
requestedMetric={metric}
rootComponent={component}
- router={this.props.router}
- selected={query.selected}
- updateQuery={this.updateQuery}
- view={query.view}
+ updateQuery={updateQuery}
/>
</StyledMain>
);
};
- render() {
- const { branchLike } = this.props;
- const { measures } = this.state;
- const { canBrowseAllChildProjects, qualifier, key } = this.props.component;
- const query = parseQuery(this.props.location.query);
- const showFullMeasures = hasFullMeasures(branchLike);
- const displayOverview = hasBubbleChart(query.metric);
- const metric = this.getSelectedMetric(query, displayOverview);
-
- return (
- <LargeCenteredLayout id="component-measures" className="sw-pt-8">
- <Suggestions suggestionGroup="component_measures" />
- <Helmet defer={false} title={translate('layout.measures')} />
- <PageContentFontWrapper className="sw-body-sm">
- <Spinner isLoading={this.state.loading} />
-
- {measures.length > 0 ? (
- <div className="sw-grid sw-grid-cols-12 sw-w-full">
- <Sidebar
- componentKey={key}
- measures={measures}
- 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>
- )}
- {!areCCTMeasuresComputed(measures) && (
- <AnalysisMissingInfoMessage className="sw-mb-4" qualifier={qualifier} />
- )}
- {this.renderContent(displayOverview, query, metric)}
- </div>
+ return (
+ <LargeCenteredLayout id="component-measures" className="sw-pt-8">
+ <Suggestions suggestionGroup="component_measures" />
+ <Helmet defer={false} title={translate('layout.measures')} />
+ <PageContentFontWrapper className="sw-body-sm">
+ <Spinner isLoading={isLoading} />
+
+ {measures.length > 0 ? (
+ <div className="sw-grid sw-grid-cols-12 sw-w-full">
+ <Sidebar
+ componentKey={componentKey}
+ measures={measures}
+ selectedMetric={metric ? metric.key : query.metric}
+ showFullMeasures={showFullMeasures}
+ updateQuery={updateQuery}
+ />
+
+ <div className="sw-col-span-9 sw-ml-12">
+ {!component?.canBrowseAllChildProjects && isPortfolioLike(component?.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>
+ )}
+ {(!areCCTMeasuresComputed(measures) ||
+ !areSoftwareQualityRatingsComputed(measures)) && (
+ <AnalysisMissingInfoMessage
+ className="sw-mb-4"
+ qualifier={component?.qualifier as ComponentQualifier}
+ />
+ )}
+ {renderContent()}
</div>
- ) : (
- <StyledMain className="sw-rounded-1 sw-p-6 sw-mb-4">
- <MeasuresEmpty />
- </StyledMain>
- )}
- </PageContentFontWrapper>
- </LargeCenteredLayout>
- );
- }
+ </div>
+ ) : (
+ <StyledMain className="sw-rounded-1 sw-p-6 sw-mb-4">
+ <MeasuresEmpty />
+ </StyledMain>
+ )}
+ </PageContentFontWrapper>
+ </LargeCenteredLayout>
+ );
}
-/*
- * This needs to be refactored: the issue
- * is that we can't use the usual withComponentContext HOC, because the type
- * of `component` isn't the same. It probably used to work because of the lazy loading
- */
-const WrappedApp = withRouter(ComponentMeasuresApp);
-
-function AppWithComponentContext() {
- const { component } = React.useContext(ComponentContext);
- const { data: { branchLike } = {} } = useBranchesQuery(component);
-
- return <WrappedApp branchLike={branchLike} component={component as ComponentMeasure} />;
-}
-
-export default AppWithComponentContext;
-
const StyledMain = withTheme(styled.main`
background-color: ${themeColor('pageBlock')};
border: ${themeBorder('default', 'pageBlockBorder')};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
index e9b232a4268..dd252561131 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
@@ -17,321 +17,191 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { keepPreviousData } from '@tanstack/react-query';
import { Highlight, KeyboardHint } from 'design-system';
import * as React from 'react';
import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
import { MetricKey } from '~sonar-aligned/types/metrics';
-import { Router } from '~sonar-aligned/types/router';
-import { getComponentTree } from '../../../api/components';
-import { getMeasures } from '../../../api/measures';
+import { useMetrics } from '../../../app/components/metrics/withMetricsContext';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
import FilesCounter from '../../../components/ui/FilesCounter';
-import { isSameBranchLike } from '../../../helpers/branch-like';
import { getComponentMeasureUniqueKey } from '../../../helpers/component';
+import { SOFTWARE_QUALITY_RATING_METRICS_MAP } from '../../../helpers/constants';
import { KeyboardKeys } from '../../../helpers/keycodes';
import { translate } from '../../../helpers/l10n';
import { getCCTMeasureValue, isDiffMetric } from '../../../helpers/measures';
import { RequestData } from '../../../helpers/request';
import { isDefined } from '../../../helpers/types';
import { getProjectUrl } from '../../../helpers/urls';
+import { useBranchesQuery } from '../../../queries/branch';
+import { useComponentTreeQuery, useMeasuresComponentQuery } from '../../../queries/measures';
+import { useIsLegacyCCTMode } from '../../../queries/settings';
+import { useLocation, useRouter } from '../../../sonar-aligned/components/hoc/withRouter';
import { BranchLike } from '../../../types/branch-like';
import { isApplication, isFile, isView } from '../../../types/component';
import { MeasurePageView } from '../../../types/measures';
import {
- ComponentMeasure,
+ Component,
ComponentMeasureEnhanced,
ComponentMeasureIntern,
- Dict,
- Measure,
Metric,
- Paging,
Period,
} from '../../../types/types';
import { complementary } from '../config/complementary';
import FilesView from '../drilldown/FilesView';
import TreeMapView from '../drilldown/TreeMapView';
-import { Query, enhanceComponent } from '../utils';
+import { Query, enhanceComponent, parseQuery } from '../utils';
import MeasureContentHeader from './MeasureContentHeader';
import MeasureHeader from './MeasureHeader';
import MeasureViewSelect from './MeasureViewSelect';
import MeasuresBreadcrumbs from './MeasuresBreadcrumbs';
interface Props {
- asc?: boolean;
- branchLike?: BranchLike;
leakPeriod?: Period;
- metrics: Dict<Metric>;
requestedMetric: Pick<Metric, 'key' | 'direction'>;
- rootComponent: ComponentMeasure;
- router: Router;
- selected?: string;
+ rootComponent: Component;
updateQuery: (query: Partial<Query>) => void;
- view: MeasurePageView;
}
-interface State {
- baseComponent?: ComponentMeasure;
- components: ComponentMeasureEnhanced[];
- loadingMoreComponents: boolean;
- measure?: Measure;
- metric?: Metric;
- paging?: Paging;
- secondaryMeasure?: Measure;
- selectedComponent?: ComponentMeasureIntern;
-}
-
-export default class MeasureContent extends React.PureComponent<Props, State> {
- container?: HTMLElement | null;
- mounted = false;
- state: State = {
- components: [],
- loadingMoreComponents: false,
- };
-
- componentDidMount() {
- this.mounted = true;
- this.fetchComponentTree();
+export default function MeasureContent(props: Readonly<Props>) {
+ const { leakPeriod, requestedMetric, rootComponent, updateQuery } = props;
+ const metrics = useMetrics();
+ const { query: rawQuery } = useLocation();
+ const { data: { branchLike } = {} } = useBranchesQuery();
+ const router = useRouter();
+ const query = parseQuery(rawQuery);
+ const { data: isLegacy } = useIsLegacyCCTMode();
+ const { selected, asc, view } = query;
+
+ const containerRef = React.useRef<HTMLDivElement>(null);
+ // if asc is undefined we dont want to pass it inside options
+ const { metricKeys, opts, strategy } = getComponentRequestParams(
+ view,
+ requestedMetric,
+ branchLike,
+ {
+ ...(asc !== undefined && { asc }),
+ },
+ );
+ const componentKey = selected !== undefined && selected !== '' ? selected : rootComponent.key;
+ const {
+ data: treeData,
+ isFetchingNextPage: fetchingMoreComponents,
+ fetchNextPage,
+ } = useComponentTreeQuery(
+ {
+ strategy,
+ component: componentKey,
+ metrics: metricKeys,
+ additionalData: opts,
+ },
+ {
+ placeholderData: keepPreviousData,
+ },
+ );
+
+ const baseComponentMetrics = [requestedMetric.key];
+
+ if (requestedMetric.key === MetricKey.ncloc) {
+ baseComponentMetrics.push(MetricKey.ncloc_language_distribution);
}
-
- componentDidUpdate(prevProps: Props) {
- 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) ||
- prevProps.requestedMetric !== this.props.requestedMetric ||
- prevProps.view !== this.props.view
- ) {
- this.fetchComponentTree();
- }
+ if (SOFTWARE_QUALITY_RATING_METRICS_MAP[requestedMetric.key]) {
+ baseComponentMetrics.push(SOFTWARE_QUALITY_RATING_METRICS_MAP[requestedMetric.key]);
}
- componentWillUnmount() {
- this.mounted = false;
- }
+ const { data: measuresData } = useMeasuresComponentQuery(
+ { componentKey, metricKeys: baseComponentMetrics, branchLike },
+ { enabled: Boolean(componentKey) },
+ );
- fetchComponentTree = () => {
- const { asc, branchLike, metrics, requestedMetric, rootComponent, selected, view } = this.props;
- // if asc is undefined we dont want to pass it inside options
- const { metricKeys, opts, strategy } = this.getComponentRequestParams(view, requestedMetric, {
- ...(asc !== undefined && { asc }),
- });
- const componentKey = selected !== undefined && selected !== '' ? selected : rootComponent.key;
- const baseComponentMetrics = [requestedMetric.key];
- if (requestedMetric.key === MetricKey.ncloc) {
- baseComponentMetrics.push(MetricKey.ncloc_language_distribution);
- }
- Promise.all([
- getComponentTree(strategy, componentKey, metricKeys, opts),
- getMeasures({
- component: componentKey,
- metricKeys: baseComponentMetrics.join(),
- ...getBranchLikeQuery(branchLike),
- }),
- ]).then(
- ([tree, measures]) => {
- if (this.mounted) {
- const metric = tree.metrics.find((m) => m.key === requestedMetric.key);
- if (metric !== undefined) {
- metric.direction = requestedMetric.direction;
- }
-
- const components = tree.components.map((component) =>
- enhanceComponent(component, metric, metrics),
- );
+ const [selectedComponent, setSelectedComponent] = React.useState<ComponentMeasureEnhanced>();
- const measure = measures.find((m) => m.metric === requestedMetric.key);
- const secondaryMeasure = measures.find((m) => m.metric !== requestedMetric.key);
+ const metric = metrics[requestedMetric.key];
+ metric.direction = requestedMetric.direction;
- this.setState(({ selectedComponent }) => ({
- baseComponent: tree.baseComponent,
- components,
- measure,
- metric,
- paging: tree.paging,
- secondaryMeasure,
- selectedComponent:
- components.length > 0 &&
- components.find(
- (c) =>
- getComponentMeasureUniqueKey(c) ===
- getComponentMeasureUniqueKey(selectedComponent),
- )
- ? selectedComponent
- : undefined,
- }));
- }
- },
- () => {
- /* noop */
- },
- );
- };
+ const baseComponent = treeData?.pages[0].baseComponent;
+ if (!baseComponent) {
+ return null;
+ }
- fetchMoreComponents = () => {
- const { metrics, view, asc } = this.props;
- const { baseComponent, metric, paging } = this.state;
- if (!baseComponent || !paging || !metric) {
- return;
- }
- // if asc is undefined we dont want to pass it inside options
- const { metricKeys, opts, strategy } = this.getComponentRequestParams(view, metric, {
- p: paging.pageIndex + 1,
- ...(asc !== undefined && { asc }),
- });
- this.setState({ loadingMoreComponents: true });
- getComponentTree(strategy, baseComponent.key, metricKeys, opts).then(
- (r) => {
- if (this.mounted && metric.key === this.props.requestedMetric.key) {
- this.setState((state) => ({
- components: [
- ...state.components,
- ...r.components.map((component) => enhanceComponent(component, metric, metrics)),
- ],
- loadingMoreComponents: false,
- paging: r.paging,
- }));
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ loadingMoreComponents: false });
- }
- },
+ const components =
+ treeData?.pages
+ .flatMap((p) => p.components)
+ .map((component) => enhanceComponent(component, metric, metrics)) ?? [];
+
+ const measures = measuresData?.component.measures ?? [];
+ const measure = measures.find((m) => m.metric === requestedMetric.key);
+ const secondaryMeasure = measures.find((m) => m.metric !== requestedMetric.key);
+ const rawMeasureValue =
+ measure && (isDiffMetric(measure.metric) ? measure.period?.value : measure.value);
+ const measureValue = getCCTMeasureValue(metric.key, rawMeasureValue);
+ const isFileComponent = isFile(baseComponent.qualifier);
+
+ const paging = treeData?.pages[treeData?.pages.length - 1].paging;
+ const totalComponents = treeData?.pages[0].paging.total;
+
+ const getSelectedIndex = () => {
+ const componentKey = isFile(baseComponent?.qualifier)
+ ? getComponentMeasureUniqueKey(baseComponent)
+ : getComponentMeasureUniqueKey(selectedComponent);
+ const index = components.findIndex(
+ (component) => getComponentMeasureUniqueKey(component) === componentKey,
);
+ return index !== -1 ? index : undefined;
};
+ const selectedIdx = getSelectedIndex();
- getComponentRequestParams(
- view: MeasurePageView,
- metric: Pick<Metric, 'key' | 'direction'>,
- options: Object = {},
- ) {
- const strategy = view === MeasurePageView.list ? 'leaves' : 'children';
- const metricKeys = [metric.key];
- const opts: RequestData = {
- ...getBranchLikeQuery(this.props.branchLike),
- additionalFields: 'metrics',
- ps: 500,
- };
-
- const setMetricSort = () => {
- const isDiff = isDiffMetric(metric.key);
- opts.s = isDiff ? 'metricPeriod' : 'metric';
- opts.metricSortFilter = 'withMeasuresOnly';
- if (isDiff) {
- opts.metricPeriodSort = 1;
- }
- };
-
- const isDiff = isDiffMetric(metric.key);
- if (view === MeasurePageView.tree) {
- metricKeys.push(...(complementary[metric.key] || []));
- opts.asc = true;
- opts.s = 'qualifier,name';
- } else if (view === MeasurePageView.list) {
- metricKeys.push(...(complementary[metric.key] || []));
- opts.asc = metric.direction === 1;
- opts.metricSort = metric.key;
- setMetricSort();
- } else if (view === MeasurePageView.treemap) {
- const sizeMetric = isDiff ? MetricKey.new_lines : MetricKey.ncloc;
- metricKeys.push(sizeMetric);
- opts.asc = false;
- opts.metricSort = sizeMetric;
- setMetricSort();
- }
-
- return { metricKeys, opts: { ...opts, ...options }, strategy };
- }
-
- updateSelected = (component: string) => {
- this.props.updateQuery({
- selected: component !== this.props.rootComponent.key ? component : undefined,
+ const updateSelected = (component: string) => {
+ updateQuery({
+ selected: component !== rootComponent.key ? component : undefined,
});
};
- updateView = (view: MeasurePageView) => {
- this.props.updateQuery({ view });
- };
-
- onOpenComponent = (component: ComponentMeasureIntern) => {
- if (isView(this.props.rootComponent.qualifier)) {
- const comp = this.state.components.find(
+ const onOpenComponent = (component: ComponentMeasureIntern) => {
+ if (isView(rootComponent.qualifier)) {
+ const comp = components.find(
(c) =>
c.refKey === component.key ||
getComponentMeasureUniqueKey(c) === getComponentMeasureUniqueKey(component),
);
if (comp) {
- this.props.router.push(getProjectUrl(comp.refKey || comp.key, component.branch));
+ router.push(getProjectUrl(comp.refKey ?? comp.key, component.branch));
}
return;
}
- this.setState((state) => ({ selectedComponent: state.baseComponent }));
- this.updateSelected(component.key);
- if (this.container) {
- this.container.focus();
+ updateSelected(component.key);
+ if (containerRef.current) {
+ containerRef.current.focus();
}
};
- onSelectComponent = (component: ComponentMeasureIntern) => {
- this.setState({ selectedComponent: component });
- };
-
- getSelectedIndex = () => {
- const componentKey = isFile(this.state.baseComponent?.qualifier)
- ? getComponentMeasureUniqueKey(this.state.baseComponent)
- : getComponentMeasureUniqueKey(this.state.selectedComponent);
- const index = this.state.components.findIndex(
- (component) => getComponentMeasureUniqueKey(component) === componentKey,
- );
- return index !== -1 ? index : undefined;
+ const handleSelectRow = (component: ComponentMeasureEnhanced) => {
+ setSelectedComponent(component);
};
- getDefaultShowBestMeasures() {
- const { asc, view } = this.props;
- if ((asc !== undefined && view === MeasurePageView.list) || view === MeasurePageView.tree) {
- return true;
- }
- return false;
- }
-
- renderMeasure() {
- const { view } = this.props;
- const { metric } = this.state;
- if (!metric) {
- return null;
- }
+ const renderMeasure = () => {
if (view === MeasurePageView.list || view === MeasurePageView.tree) {
- const selectedIdx = this.getSelectedIndex();
return (
<FilesView
- branchLike={this.props.branchLike}
- components={this.state.components}
- defaultShowBestMeasures={this.getDefaultShowBestMeasures()}
- fetchMore={this.fetchMoreComponents}
- handleOpen={this.onOpenComponent}
- handleSelect={this.onSelectComponent}
- loadingMore={this.state.loadingMoreComponents}
+ branchLike={branchLike}
+ components={components}
+ defaultShowBestMeasures={
+ (asc !== undefined && view === MeasurePageView.list) || view === MeasurePageView.tree
+ }
+ fetchMore={fetchNextPage}
+ handleOpen={onOpenComponent}
+ handleSelect={handleSelectRow}
+ loadingMore={fetchingMoreComponents}
metric={metric}
- metrics={this.props.metrics}
- paging={this.state.paging}
- rootComponent={this.props.rootComponent}
+ metrics={metrics}
+ paging={paging}
+ rootComponent={rootComponent}
selectedIdx={selectedIdx}
- selectedComponent={
- selectedIdx !== undefined
- ? (this.state.selectedComponent as ComponentMeasureEnhanced)
- : undefined
- }
+ selectedComponent={selectedComponent}
view={view}
/>
);
@@ -339,119 +209,150 @@ export default class MeasureContent extends React.PureComponent<Props, State> {
return (
<TreeMapView
- components={this.state.components}
- handleSelect={this.onOpenComponent}
+ isLegacyMode={Boolean(isLegacy)}
+ components={components}
+ handleSelect={onOpenComponent}
metric={metric}
/>
);
- }
-
- render() {
- const { branchLike, rootComponent, view } = this.props;
- const { baseComponent, measure, metric, paging, secondaryMeasure } = this.state;
-
- if (!baseComponent || !metric) {
- return null;
- }
+ };
- const rawMeasureValue =
- measure && (isDiffMetric(measure.metric) ? measure.period?.value : measure.value);
- const measureValue = getCCTMeasureValue(metric.key, rawMeasureValue);
+ return (
+ <div ref={containerRef}>
+ <A11ySkipTarget anchor="measures_main" />
- const isFileComponent = isFile(baseComponent.qualifier);
- const selectedIdx = this.getSelectedIndex();
+ <MeasureContentHeader
+ left={
+ <MeasuresBreadcrumbs
+ backToFirst={view === MeasurePageView.list}
+ branchLike={branchLike}
+ className="sw-flex-1"
+ component={baseComponent}
+ handleSelect={onOpenComponent}
+ rootComponent={rootComponent}
+ />
+ }
+ right={
+ <div className="sw-flex sw-items-center">
+ {!isFileComponent && metric && (
+ <>
+ {!isApplication(baseComponent.qualifier) && (
+ <>
+ <Highlight className="sw-whitespace-nowrap" id="measures-view-selection-label">
+ {translate('component_measures.view_as')}
+ </Highlight>
+ <MeasureViewSelect
+ className="measure-view-select sw-ml-2 sw-mr-4"
+ handleViewChange={(view) => updateQuery({ view })}
+ metric={metric}
+ view={view}
+ />
+ </>
+ )}
+
+ {view !== MeasurePageView.treemap && (
+ <>
+ <KeyboardHint
+ className="sw-mr-4 sw-ml-6"
+ command={`${KeyboardKeys.DownArrow} ${KeyboardKeys.UpArrow}`}
+ title={translate('component_measures.select_files')}
+ />
- return (
- <div ref={(container) => (this.container = container)}>
- <A11ySkipTarget anchor="measures_main" />
+ <KeyboardHint
+ command={`${KeyboardKeys.LeftArrow} ${KeyboardKeys.RightArrow}`}
+ title={translate('component_measures.navigate')}
+ />
+ </>
+ )}
+
+ {isDefined(totalComponents) && totalComponents > 0 && (
+ <FilesCounter
+ className="sw-min-w-24 sw-text-right"
+ current={
+ isDefined(selectedIdx) && view !== MeasurePageView.treemap
+ ? selectedIdx + 1
+ : undefined
+ }
+ total={totalComponents}
+ />
+ )}
+ </>
+ )}
+ </div>
+ }
+ />
- <MeasureContentHeader
- left={
- <MeasuresBreadcrumbs
- backToFirst={view === MeasurePageView.list}
+ <div className="sw-p-6">
+ <MeasureHeader
+ branchLike={branchLike}
+ component={baseComponent}
+ leakPeriod={leakPeriod}
+ measureValue={measureValue}
+ metric={metric}
+ secondaryMeasure={secondaryMeasure}
+ />
+ {isFileComponent ? (
+ <div>
+ <SourceViewer
+ hideHeader
branchLike={branchLike}
- className="sw-flex-1"
- component={baseComponent}
- handleSelect={this.onOpenComponent}
- rootComponent={rootComponent}
+ component={baseComponent.key}
+ metricKey={metric.key}
/>
- }
- right={
- <div className="sw-flex sw-items-center">
- {!isFileComponent && metric && (
- <>
- {!isApplication(baseComponent.qualifier) && (
- <>
- <Highlight
- className="sw-whitespace-nowrap"
- id="measures-view-selection-label"
- >
- {translate('component_measures.view_as')}
- </Highlight>
- <MeasureViewSelect
- className="measure-view-select sw-ml-2 sw-mr-4"
- handleViewChange={this.updateView}
- metric={metric}
- view={view}
- />
- </>
- )}
-
- {view !== MeasurePageView.treemap && (
- <>
- <KeyboardHint
- className="sw-mr-4 sw-ml-6"
- command={`${KeyboardKeys.DownArrow} ${KeyboardKeys.UpArrow}`}
- title={translate('component_measures.select_files')}
- />
+ </div>
+ ) : (
+ renderMeasure()
+ )}
+ </div>
+ </div>
+ );
+}
- <KeyboardHint
- command={`${KeyboardKeys.LeftArrow} ${KeyboardKeys.RightArrow}`}
- title={translate('component_measures.navigate')}
- />
- </>
- )}
+function getComponentRequestParams(
+ view: MeasurePageView,
+ metric: Pick<Metric, 'key' | 'direction'>,
+ branchLike?: BranchLike,
+ options: Object = {},
+) {
+ const strategy: 'leaves' | 'children' = view === MeasurePageView.list ? 'leaves' : 'children';
+ const metricKeys = [metric.key];
+ const softwareQualityRatingMetric = SOFTWARE_QUALITY_RATING_METRICS_MAP[metric.key];
+ if (softwareQualityRatingMetric) {
+ metricKeys.push(softwareQualityRatingMetric);
+ }
+ const opts: RequestData = {
+ ...getBranchLikeQuery(branchLike),
+ additionalFields: 'metrics',
+ ps: 500,
+ };
- {paging && paging.total > 0 && (
- <FilesCounter
- className="sw-min-w-24 sw-text-right"
- current={
- isDefined(selectedIdx) && view !== MeasurePageView.treemap
- ? selectedIdx + 1
- : undefined
- }
- total={paging.total}
- />
- )}
- </>
- )}
- </div>
- }
- />
+ const setMetricSort = () => {
+ const isDiff = isDiffMetric(metric.key);
+ opts.s = isDiff ? 'metricPeriod' : 'metric';
+ opts.metricSortFilter = 'withMeasuresOnly';
+ if (isDiff) {
+ opts.metricPeriodSort = 1;
+ }
+ };
- <div className="sw-p-6">
- <MeasureHeader
- branchLike={branchLike}
- component={baseComponent}
- leakPeriod={this.props.leakPeriod}
- measureValue={measureValue}
- metric={metric}
- secondaryMeasure={secondaryMeasure}
- />
- {isFileComponent ? (
- <div>
- <SourceViewer
- hideHeader
- branchLike={branchLike}
- component={baseComponent.key}
- metricKey={this.state.metric?.key}
- />
- </div>
- ) : (
- this.renderMeasure()
- )}
- </div>
- </div>
- );
+ const isDiff = isDiffMetric(metric.key);
+ if (view === MeasurePageView.tree) {
+ metricKeys.push(...(complementary[metric.key] || []));
+ opts.asc = true;
+ opts.s = 'qualifier,name';
+ } else if (view === MeasurePageView.list) {
+ metricKeys.push(...(complementary[metric.key] || []));
+ opts.asc = metric.direction === 1;
+ opts.metricSort = metric.key;
+ setMetricSort();
+ } else if (view === MeasurePageView.treemap) {
+ const sizeMetric = isDiff ? MetricKey.new_lines : MetricKey.ncloc;
+ metricKeys.push(...(complementary[metric.key] || []));
+ metricKeys.push(sizeMetric);
+ opts.asc = false;
+ opts.metricSort = sizeMetric;
+ setMetricSort();
}
+
+ return { metricKeys, opts: { ...opts, ...options }, strategy };
}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx
index ad7ace64e8b..ccece093bb7 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx
@@ -63,6 +63,7 @@ export default function MeasureHeader(props: Readonly<Props>) {
<div className="sw-flex sw-items-center sw-ml-2">
<Measure
+ branchLike={branchLike}
componentKey={component.key}
className={classNames('it__measure-details-value sw-body-md')}
metricKey={metric.key}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx
index 16950620500..98ce0aa3f81 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx
@@ -17,163 +17,145 @@
* 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 'design-system';
+import { Spinner } from '@sonarsource/echoes-react';
import * as React from 'react';
-import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
-import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
-import { getComponentLeaves } from '../../../api/components';
+import { useMetrics } from '../../../app/components/metrics/withMetricsContext';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
-import { isSameBranchLike } from '../../../helpers/branch-like';
-import { BranchLike } from '../../../types/branch-like';
-import { isFile } from '../../../types/component';
-import {
- ComponentMeasure,
- ComponentMeasureEnhanced,
- ComponentMeasureIntern,
- Dict,
- Metric,
- Paging,
- Period,
-} from '../../../types/types';
+import { getProjectUrl } from '../../../helpers/urls';
+import { useBranchesQuery } from '../../../queries/branch';
+import { useComponentDataQuery } from '../../../queries/component';
+import { useComponentTreeQuery } from '../../../queries/measures';
+import A11ySkipTarget from '../../../sonar-aligned/components/a11y/A11ySkipTarget';
+import { useLocation, useRouter } from '../../../sonar-aligned/components/hoc/withRouter';
+import { getBranchLikeQuery } from '../../../sonar-aligned/helpers/branch-like';
+import { isFile, isView } from '../../../types/component';
+import { Component, ComponentMeasureIntern, Period } from '../../../types/types';
+import { BubblesByDomain } from '../config/bubbles';
import BubbleChartView from '../drilldown/BubbleChartView';
-import { BUBBLES_FETCH_LIMIT, enhanceComponent, getBubbleMetrics, hasFullMeasures } from '../utils';
+import {
+ BUBBLES_FETCH_LIMIT,
+ enhanceComponent,
+ getBubbleMetrics,
+ hasFullMeasures,
+ parseQuery,
+ Query,
+} from '../utils';
import LeakPeriodLegend from './LeakPeriodLegend';
import MeasureContentHeader from './MeasureContentHeader';
import MeasuresBreadcrumbs from './MeasuresBreadcrumbs';
interface Props {
- branchLike?: BranchLike;
- className?: string;
- component: ComponentMeasure;
- domain: string;
+ bubblesByDomain: BubblesByDomain;
leakPeriod?: Period;
- loading: boolean;
- metrics: Dict<Metric>;
- rootComponent: ComponentMeasure;
- updateLoading: (param: Dict<boolean>) => void;
- updateSelected: (component: ComponentMeasureIntern) => void;
-}
-
-interface State {
- components: ComponentMeasureEnhanced[];
- paging?: Paging;
+ rootComponent: Component;
+ updateQuery: (query: Partial<Query>) => void;
}
-export default class MeasureOverview extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { components: [] };
+export default function MeasureOverview(props: Readonly<Props>) {
+ const { leakPeriod, updateQuery, rootComponent, bubblesByDomain } = props;
+ const metrics = useMetrics();
+ const { data: { branchLike } = {} } = useBranchesQuery();
+ const router = useRouter();
+ const { query } = useLocation();
+ const { selected, metric: domain } = parseQuery(query);
+ // eslint-disable-next-line local-rules/no-implicit-coercion
+ const componentKey = selected || rootComponent.key;
+ const { data: componentData, isLoading: loadingComponent } = useComponentDataQuery(
+ {
+ ...getBranchLikeQuery(branchLike),
+ component: componentKey,
+ },
+ { enabled: Boolean(componentKey) },
+ );
- componentDidMount() {
- this.mounted = true;
- this.fetchComponents();
+ const component = componentData?.component;
+ const { x, y, size, colors } = getBubbleMetrics(bubblesByDomain, domain, metrics);
+ const metricsKey = [x.key, y.key, size.key];
+ if (colors) {
+ metricsKey.push(...colors.map((metric) => metric.key));
}
+ const { data: bubblesData, isLoading: loadingBubbles } = useComponentTreeQuery(
+ {
+ strategy: 'leaves',
+ metrics: metricsKey,
+ component: component?.key ?? '',
+ additionalData: {
+ ...getBranchLikeQuery(branchLike),
+ s: 'metric',
+ metricSort: size.key,
+ asc: false,
+ ps: BUBBLES_FETCH_LIMIT,
+ },
+ },
+ {
+ enabled: Boolean(component),
+ },
+ );
- componentDidUpdate(prevProps: Props) {
- if (
- prevProps.component !== this.props.component ||
- !isSameBranchLike(prevProps.branchLike, this.props.branchLike) ||
- prevProps.metrics !== this.props.metrics ||
- prevProps.domain !== this.props.domain
- ) {
- this.fetchComponents();
- }
- }
+ const components = (bubblesData?.pages?.[0]?.components ?? []).map((c) =>
+ enhanceComponent(c, undefined, metrics),
+ );
+ const paging = bubblesData?.pages?.[0]?.paging;
- componentWillUnmount() {
- this.mounted = false;
+ if (!component) {
+ return null;
}
- fetchComponents = () => {
- const { branchLike, component, domain, metrics } = this.props;
- if (isFile(component.qualifier)) {
- this.setState({ components: [], paging: undefined });
- return;
- }
- const { x, y, size, colors } = getBubbleMetrics(domain, metrics);
- const metricsKey = [x.key, y.key, size.key];
- if (colors) {
- metricsKey.push(...colors.map((metric) => metric.key));
- }
- const options = {
- ...getBranchLikeQuery(branchLike),
- s: 'metric',
- metricSort: size.key,
- asc: false,
- ps: BUBBLES_FETCH_LIMIT,
- };
+ const loading = loadingComponent || loadingBubbles;
- this.props.updateLoading({ bubbles: true });
- getComponentLeaves(component.key, metricsKey, options).then(
- (r) => {
- if (domain === this.props.domain) {
- if (this.mounted) {
- this.setState({
- components: r.components.map((c) => enhanceComponent(c, undefined, metrics)),
- paging: r.paging,
- });
- }
- this.props.updateLoading({ bubbles: false });
- }
- },
- () => this.props.updateLoading({ bubbles: false }),
- );
+ const updateSelected = (component: ComponentMeasureIntern) => {
+ if (component && isView(component.qualifier)) {
+ router.push(getProjectUrl(component.refKey || component.key, component.branch));
+ } else {
+ updateQuery({
+ selected: component.key !== rootComponent.key ? component.key : undefined,
+ });
+ }
};
+ const displayLeak = hasFullMeasures(branchLike);
+ const isFileComponent = isFile(component.qualifier);
- renderContent(isFile: boolean) {
- const { branchLike, component, domain, metrics } = this.props;
- const { paging } = this.state;
-
- if (isFile) {
- return (
- <div className="measure-details-viewer">
- <SourceViewer hideHeader branchLike={branchLike} component={component.key} />
- </div>
- );
- }
+ return (
+ <div>
+ <A11ySkipTarget anchor="measures_main" />
- return (
- <BubbleChartView
- component={component}
- branchLike={branchLike}
- components={this.state.components}
- domain={domain}
- metrics={metrics}
- paging={paging}
- updateSelected={this.props.updateSelected}
+ <MeasureContentHeader
+ left={
+ <MeasuresBreadcrumbs
+ backToFirst
+ branchLike={branchLike}
+ component={component}
+ handleSelect={updateSelected}
+ rootComponent={rootComponent}
+ />
+ }
+ right={
+ leakPeriod &&
+ displayLeak && <LeakPeriodLegend component={component} period={leakPeriod} />
+ }
/>
- );
- }
- render() {
- const { branchLike, className, component, leakPeriod, loading, rootComponent } = this.props;
- const displayLeak = hasFullMeasures(branchLike);
- const isFileComponent = isFile(component.qualifier);
-
- return (
- <div className={className}>
- <A11ySkipTarget anchor="measures_main" />
-
- <MeasureContentHeader
- left={
- <MeasuresBreadcrumbs
- backToFirst
- branchLike={branchLike}
+ <div className="sw-p-6">
+ <Spinner isLoading={loading}>
+ {isFileComponent && (
+ <div className="measure-details-viewer">
+ <SourceViewer hideHeader branchLike={branchLike} component={component.key} />
+ </div>
+ )}
+ {!isFileComponent && (
+ <BubbleChartView
+ bubblesByDomain={bubblesByDomain}
component={component}
- handleSelect={this.props.updateSelected}
- rootComponent={rootComponent}
+ branchLike={branchLike}
+ components={components}
+ domain={domain}
+ metrics={metrics}
+ paging={paging}
+ updateSelected={updateSelected}
/>
- }
- right={
- leakPeriod &&
- displayLeak && <LeakPeriodLegend component={component} period={leakPeriod} />
- }
- />
-
- <div className="sw-p-6">
- <Spinner loading={loading} />
- {!loading && this.renderContent(isFileComponent)}
- </div>
+ )}
+ </Spinner>
</div>
- );
- }
+ </div>
+ );
}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx
deleted file mode 100644
index 9455a77c612..00000000000
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * 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 * as React from 'react';
-import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
-import { Router } from '~sonar-aligned/types/router';
-import { getComponentShow } from '../../../api/components';
-import { isSameBranchLike } from '../../../helpers/branch-like';
-import { getProjectUrl } from '../../../helpers/urls';
-import { BranchLike } from '../../../types/branch-like';
-import { isView } from '../../../types/component';
-import {
- ComponentMeasure,
- ComponentMeasureIntern,
- Dict,
- Metric,
- Period,
-} from '../../../types/types';
-import { Query } from '../utils';
-import MeasureOverview from './MeasureOverview';
-
-interface Props {
- branchLike?: BranchLike;
- className?: string;
- domain: string;
- leakPeriod?: Period;
- metrics: Dict<Metric>;
- rootComponent: ComponentMeasure;
- router: Router;
- selected?: string;
- updateQuery: (query: Partial<Query>) => void;
-}
-
-interface LoadingState {
- bubbles: boolean;
- component: boolean;
-}
-
-interface State {
- component?: ComponentMeasure;
- loading: LoadingState;
-}
-
-export default class MeasureOverviewContainer extends React.PureComponent<Props, State> {
- mounted = false;
-
- state: State = {
- loading: { bubbles: false, component: false },
- };
-
- componentDidMount() {
- this.mounted = true;
- this.fetchComponent();
- }
-
- componentDidUpdate(prevProps: Props) {
- const prevComponentKey = prevProps.selected || prevProps.rootComponent.key;
- const componentKey = this.props.selected || this.props.rootComponent.key;
- if (
- prevComponentKey !== componentKey ||
- !isSameBranchLike(prevProps.branchLike, this.props.branchLike) ||
- prevProps.domain !== this.props.domain
- ) {
- this.fetchComponent();
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- fetchComponent = () => {
- const { branchLike, rootComponent, selected } = this.props;
- if (!selected || rootComponent.key === selected) {
- this.setState({ component: rootComponent });
- this.updateLoading({ component: false });
- return;
- }
- this.updateLoading({ component: true });
- getComponentShow({ component: selected, ...getBranchLikeQuery(branchLike) }).then(
- ({ component }) => {
- if (this.mounted) {
- this.setState({ component });
- this.updateLoading({ component: false });
- }
- },
- () => this.updateLoading({ component: false }),
- );
- };
-
- updateLoading = (loading: Partial<LoadingState>) => {
- if (this.mounted) {
- this.setState((state) => ({ loading: { ...state.loading, ...loading } }));
- }
- };
-
- updateSelected = (component: ComponentMeasureIntern) => {
- if (this.state.component && isView(this.state.component.qualifier)) {
- this.props.router.push(getProjectUrl(component.refKey || component.key, component.branch));
- } else {
- this.props.updateQuery({
- selected: component.key !== this.props.rootComponent.key ? component.key : undefined,
- });
- }
- };
-
- render() {
- if (!this.state.component) {
- return null;
- }
-
- return (
- <MeasureOverview
- branchLike={this.props.branchLike}
- className={this.props.className}
- component={this.state.component}
- domain={this.props.domain}
- leakPeriod={this.props.leakPeriod}
- loading={this.state.loading.component || this.state.loading.bubbles}
- metrics={this.props.metrics}
- rootComponent={this.props.rootComponent}
- updateLoading={this.updateLoading}
- updateSelected={this.updateSelected}
- />
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasuresBreadcrumbs.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasuresBreadcrumbs.tsx
index 0284d8bc4a1..28648e7acd1 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasuresBreadcrumbs.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasuresBreadcrumbs.tsx
@@ -29,7 +29,7 @@ import { translate } from '../../../helpers/l10n';
import { collapsePath, limitComponentName } from '../../../helpers/path';
import { BranchLike } from '../../../types/branch-like';
import { isProject } from '../../../types/component';
-import { ComponentMeasure, ComponentMeasureIntern } from '../../../types/types';
+import { Component, ComponentMeasure, ComponentMeasureIntern } from '../../../types/types';
interface Props {
backToFirst: boolean;
@@ -37,7 +37,7 @@ interface Props {
className?: string;
component: ComponentMeasure;
handleSelect: (component: ComponentMeasureIntern) => void;
- rootComponent: ComponentMeasure;
+ rootComponent: Component;
}
interface State {
diff --git a/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.ts b/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.ts
index f660bda32db..a8e55997174 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.ts
+++ b/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.ts
@@ -19,15 +19,96 @@
*/
import { MetricKey } from '~sonar-aligned/types/metrics';
-export const bubbles: {
- [domain: string]: {
+export type BubblesByDomain = Record<
+ string,
+ {
colors?: string[];
size: string;
x: string;
y: string;
yDomain?: [number, number];
- };
-} = {
+ }
+>;
+
+export const newTaxonomyBubbles: BubblesByDomain = {
+ Reliability: {
+ x: MetricKey.ncloc,
+ y: MetricKey.reliability_remediation_effort,
+ size: MetricKey.reliability_issues,
+ colors: [MetricKey.reliability_rating_new],
+ },
+ Security: {
+ x: MetricKey.ncloc,
+ y: MetricKey.security_remediation_effort,
+ size: MetricKey.security_issues,
+ colors: [MetricKey.security_rating_new],
+ },
+ Maintainability: {
+ x: MetricKey.ncloc,
+ y: MetricKey.sqale_index,
+ size: MetricKey.maintainability_issues,
+ colors: [MetricKey.sqale_rating_new],
+ },
+ Coverage: {
+ x: MetricKey.complexity,
+ y: MetricKey.coverage,
+ size: MetricKey.uncovered_lines,
+ yDomain: [100, 0],
+ },
+ Duplications: {
+ x: MetricKey.ncloc,
+ y: MetricKey.duplicated_lines,
+ size: MetricKey.duplicated_blocks,
+ },
+ project_overview: {
+ x: MetricKey.sqale_index,
+ y: MetricKey.coverage,
+ size: MetricKey.ncloc,
+ colors: [MetricKey.reliability_rating_new, MetricKey.security_rating_new],
+ yDomain: [100, 0],
+ },
+};
+
+export const newTaxonomyWithoutRatingsBubbles: BubblesByDomain = {
+ Reliability: {
+ x: MetricKey.ncloc,
+ y: MetricKey.reliability_remediation_effort,
+ size: MetricKey.reliability_issues,
+ colors: [MetricKey.reliability_rating],
+ },
+ Security: {
+ x: MetricKey.ncloc,
+ y: MetricKey.security_remediation_effort,
+ size: MetricKey.security_issues,
+ colors: [MetricKey.security_rating],
+ },
+ Maintainability: {
+ x: MetricKey.ncloc,
+ y: MetricKey.sqale_index,
+ size: MetricKey.maintainability_issues,
+ colors: [MetricKey.sqale_rating],
+ },
+ Coverage: {
+ x: MetricKey.complexity,
+ y: MetricKey.coverage,
+ size: MetricKey.uncovered_lines,
+ yDomain: [100, 0],
+ },
+ Duplications: {
+ x: MetricKey.ncloc,
+ y: MetricKey.duplicated_lines,
+ size: MetricKey.duplicated_blocks,
+ },
+ project_overview: {
+ x: MetricKey.sqale_index,
+ y: MetricKey.coverage,
+ size: MetricKey.ncloc,
+ colors: [MetricKey.reliability_rating, MetricKey.security_rating],
+ yDomain: [100, 0],
+ },
+};
+
+export const legacyBubbles: BubblesByDomain = {
Reliability: {
x: MetricKey.ncloc,
y: MetricKey.reliability_remediation_effort,
diff --git a/server/sonar-web/src/main/js/apps/component-measures/config/domains.ts b/server/sonar-web/src/main/js/apps/component-measures/config/domains.ts
index 6eb98c969f2..a98910ded13 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/config/domains.ts
+++ b/server/sonar-web/src/main/js/apps/component-measures/config/domains.ts
@@ -34,13 +34,17 @@ export const domains: Domains = {
MetricKey.new_reliability_issues,
MetricKey.new_bugs,
MetricKey.new_reliability_rating,
+ MetricKey.new_reliability_rating_new,
MetricKey.new_reliability_remediation_effort,
+ MetricKey.new_reliability_remediation_effort_new,
OVERALL_CATEGORY,
MetricKey.reliability_issues,
MetricKey.bugs,
MetricKey.reliability_rating,
+ MetricKey.reliability_rating_new,
MetricKey.reliability_remediation_effort,
+ MetricKey.reliability_remediation_effort_new,
],
},
@@ -51,13 +55,17 @@ export const domains: Domains = {
MetricKey.new_security_issues,
MetricKey.new_vulnerabilities,
MetricKey.new_security_rating,
+ MetricKey.new_security_rating_new,
MetricKey.new_security_remediation_effort,
+ MetricKey.new_security_remediation_effort_new,
OVERALL_CATEGORY,
MetricKey.security_issues,
MetricKey.vulnerabilities,
MetricKey.security_rating,
+ MetricKey.security_rating_new,
MetricKey.security_remediation_effort,
+ MetricKey.security_remediation_effort_new,
],
},
@@ -67,11 +75,13 @@ export const domains: Domains = {
NEW_CODE_CATEGORY,
MetricKey.new_security_hotspots,
MetricKey.new_security_review_rating,
+ MetricKey.new_security_review_rating_new,
MetricKey.new_security_hotspots_reviewed,
OVERALL_CATEGORY,
MetricKey.security_hotspots,
MetricKey.security_review_rating,
+ MetricKey.security_review_rating_new,
MetricKey.security_hotspots_reviewed,
],
},
@@ -85,6 +95,7 @@ export const domains: Domains = {
MetricKey.new_technical_debt,
MetricKey.new_sqale_debt_ratio,
MetricKey.new_maintainability_rating,
+ MetricKey.new_maintainability_rating_new,
OVERALL_CATEGORY,
MetricKey.maintainability_issues,
@@ -92,7 +103,9 @@ export const domains: Domains = {
MetricKey.sqale_index,
MetricKey.sqale_debt_ratio,
MetricKey.sqale_rating,
+ MetricKey.sqale_rating_new,
MetricKey.effort_to_reach_maintainability_rating_a,
+ MetricKey.effort_to_reach_maintainability_rating_a_new,
],
},
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChartView.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChartView.tsx
index aac6a045d29..1e434ec3079 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChartView.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChartView.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 { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
BubbleColorVal,
@@ -25,6 +26,7 @@ import {
Link,
BubbleChart as OriginalBubbleChart,
themeColor,
+ themeContrast,
} from 'design-system';
import * as React from 'react';
import HelpTooltip from '~sonar-aligned/components/controls/HelpTooltip';
@@ -36,9 +38,10 @@ import {
translate,
translateWithParameters,
} from '../../../helpers/l10n';
-import { isDiffMetric } from '../../../helpers/measures';
+import { getCCTMeasureValue, isDiffMetric } from '../../../helpers/measures';
import { isDefined } from '../../../helpers/types';
import { getComponentDrilldownUrl } from '../../../helpers/urls';
+import { useIsLegacyCCTMode } from '../../../queries/settings';
import { BranchLike } from '../../../types/branch-like';
import { isProject, isView } from '../../../types/component';
import {
@@ -49,6 +52,7 @@ import {
Metric,
Paging,
} from '../../../types/types';
+import { BubblesByDomain } from '../config/bubbles';
import {
BUBBLES_FETCH_LIMIT,
getBubbleMetrics,
@@ -62,6 +66,7 @@ const HEIGHT = 500;
interface Props {
branchLike?: BranchLike;
+ bubblesByDomain: BubblesByDomain;
component: ComponentMeasureI;
components: ComponentMeasureEnhanced[];
domain: string;
@@ -70,84 +75,35 @@ interface Props {
updateSelected: (component: ComponentMeasureIntern) => void;
}
-interface State {
- ratingFilters: { [rating: number]: boolean };
-}
-
-export default class BubbleChartView extends React.PureComponent<Props, State> {
- state: State = {
- ratingFilters: {},
- };
+export default function BubbleChartView(props: Readonly<Props>) {
+ const {
+ metrics,
+ domain,
+ components,
+ updateSelected,
+ paging,
+ component,
+ branchLike,
+ bubblesByDomain,
+ } = props;
+ const theme = useTheme();
+ const { data: isLegacy } = useIsLegacyCCTMode();
+ const bubbleMetrics = getBubbleMetrics(bubblesByDomain, domain, metrics);
+ const [ratingFilters, setRatingFilters] = React.useState<{ [rating: number]: boolean }>({});
- getMeasureVal = (component: ComponentMeasureEnhanced, metric: Metric) => {
- const measure = component.measures.find((measure) => measure.metric.key === metric.key);
- if (!measure) {
- return undefined;
- }
- return Number(isDiffMetric(metric.key) ? measure.leak : measure.value);
- };
-
- getTooltip(
- component: ComponentMeasureEnhanced,
- values: { colors?: Array<number | undefined>; size: number; x: number; y: number },
- metrics: { colors?: Metric[]; size: Metric; x: Metric; y: Metric },
- ) {
- const inner = [
- [component.name, isProject(component.qualifier) ? component.branch : undefined]
- .filter((s) => !!s)
- .join(' / '),
- `${metrics.x.name}: ${formatMeasure(values.x, metrics.x.type)}`,
- `${metrics.y.name}: ${formatMeasure(values.y, metrics.y.type)}`,
- `${metrics.size.name}: ${formatMeasure(values.size, metrics.size.type)}`,
- ].filter((s) => !!s);
- const { colors: valuesColors } = values;
- const { colors: metricColors } = metrics;
- if (valuesColors && metricColors) {
- metricColors.forEach((metric, idx) => {
- const colorValue = valuesColors[idx];
- if (colorValue || colorValue === 0) {
- inner.push(`${metric.name}: ${formatMeasure(colorValue, metric.type)}`);
- }
- });
- }
- return (
- <div className="sw-text-left">
- {inner.map((line, index) => (
- <React.Fragment key={index}>
- {line}
- {index < inner.length - 1 && <br />}
- </React.Fragment>
- ))}
- </div>
- );
- }
-
- handleRatingFilterClick = (selection: number) => {
- this.setState(({ ratingFilters }) => {
- return { ratingFilters: { ...ratingFilters, [selection]: !ratingFilters[selection] } };
+ const handleRatingFilterClick = (selection: number) => {
+ setRatingFilters((ratingFilters) => {
+ return { ...ratingFilters, [selection]: !ratingFilters[selection] };
});
};
- handleBubbleClick = (component: ComponentMeasureEnhanced) => this.props.updateSelected(component);
-
- getDescription(domain: string) {
- const description = `component_measures.overview.${domain}.description`;
- const translatedDescription = translate(description);
- if (description === translatedDescription) {
- return null;
- }
- return translatedDescription;
- }
-
- renderBubbleChart(metrics: { colors?: Metric[]; size: Metric; x: Metric; y: Metric }) {
- const { ratingFilters } = this.state;
-
- const items = this.props.components
+ const renderBubbleChart = () => {
+ const items = components
.map((component) => {
- const x = this.getMeasureVal(component, metrics.x);
- const y = this.getMeasureVal(component, metrics.y);
- const size = this.getMeasureVal(component, metrics.size);
- const colors = metrics.colors?.map((metric) => this.getMeasureVal(component, metric));
+ const x = getMeasureVal(component, bubbleMetrics.x);
+ const y = getMeasureVal(component, bubbleMetrics.y);
+ const size = getMeasureVal(component, bubbleMetrics.size);
+ const colors = bubbleMetrics.colors?.map((metric) => getMeasureVal(component, metric));
if ((!x && x !== 0) || (!y && y !== 0) || (!size && size !== 0)) {
return undefined;
}
@@ -163,15 +119,26 @@ export default class BubbleChartView extends React.PureComponent<Props, State> {
x,
y,
size,
- color: (colorRating as BubbleColorVal) ?? 0,
+ backgroundColor: themeColor(
+ `bubble.${isLegacy ? 'legacy.' : ''}${colorRating as BubbleColorVal}`,
+ )({
+ theme,
+ }),
+ borderColor: themeContrast(
+ `bubble.${isLegacy ? 'legacy.' : ''}${colorRating as BubbleColorVal}`,
+ )({
+ theme,
+ }),
data: component,
- tooltip: this.getTooltip(component, { x, y, size, colors }, metrics),
+ tooltip: getTooltip(component, { x, y, size, colors }, bubbleMetrics),
};
})
.filter(isDefined);
- const formatXTick = (tick: string | number | undefined) => formatMeasure(tick, metrics.x.type);
- const formatYTick = (tick: string | number | undefined) => formatMeasure(tick, metrics.y.type);
+ const formatXTick = (tick: string | number | undefined) =>
+ formatMeasure(tick, bubbleMetrics.x.type);
+ const formatYTick = (tick: string | number | undefined) =>
+ formatMeasure(tick, bubbleMetrics.y.type);
let xDomain: [number, number] | undefined;
if (items.reduce((acc, item) => acc + item.x, 0) === 0) {
@@ -188,19 +155,15 @@ export default class BubbleChartView extends React.PureComponent<Props, State> {
formatYTick={formatYTick}
height={HEIGHT}
items={items}
- onBubbleClick={this.handleBubbleClick}
+ onBubbleClick={(component: ComponentMeasureEnhanced) => updateSelected(component)}
padding={[0, 4, 50, 100]}
- yDomain={getBubbleYDomain(this.props.domain)}
+ yDomain={getBubbleYDomain(bubblesByDomain, domain)}
xDomain={xDomain}
/>
);
- }
-
- renderChartHeader(domain: string, sizeMetric: Metric, colorsMetric?: Metric[]) {
- const { ratingFilters } = this.state;
- const { paging, component, branchLike, metrics: propsMetrics } = this.props;
- const metrics = getBubbleMetrics(domain, propsMetrics);
+ };
+ const renderChartHeader = () => {
const title = isProjectOverview(domain)
? translate('component_measures.overview', domain, 'title')
: translateWithParameters(
@@ -213,7 +176,7 @@ export default class BubbleChartView extends React.PureComponent<Props, State> {
<div>
<div className="sw-flex sw-items-center sw-whitespace-nowrap">
<Highlight className="it__measure-overview-bubble-chart-title">{title}</Highlight>
- <HelpTooltip className="sw-ml-2" overlay={this.getDescription(domain)}>
+ <HelpTooltip className="sw-ml-2" overlay={getDescription(domain)}>
<HelperHintIcon />
</HelpTooltip>
</div>
@@ -229,7 +192,7 @@ export default class BubbleChartView extends React.PureComponent<Props, State> {
to={getComponentDrilldownUrl({
componentKey: component.key,
branchLike,
- metric: isProjectOverview(domain) ? MetricKey.violations : metrics.size.key,
+ metric: isProjectOverview(domain) ? MetricKey.violations : bubbleMetrics.size.key,
listView: true,
})}
>
@@ -241,55 +204,105 @@ export default class BubbleChartView extends React.PureComponent<Props, State> {
<div className="sw-flex sw-flex-col sw-items-end">
<div className="sw-text-right">
- {colorsMetric && (
+ {bubbleMetrics.colors && (
<span className="sw-mr-3">
<strong className="sw-body-sm-highlight">
{translate('component_measures.legend.color')}
</strong>{' '}
- {colorsMetric.length > 1
+ {bubbleMetrics.colors.length > 1
? translateWithParameters(
'component_measures.legend.worse_of_x_y',
- ...colorsMetric.map((metric) => getLocalizedMetricName(metric)),
+ ...bubbleMetrics.colors.map((metric) => getLocalizedMetricName(metric)),
)
- : getLocalizedMetricName(colorsMetric[0])}
+ : getLocalizedMetricName(bubbleMetrics.colors[0])}
</span>
)}
<strong className="sw-body-sm-highlight">
{translate('component_measures.legend.size')}
</strong>{' '}
- {getLocalizedMetricName(sizeMetric)}
+ {getLocalizedMetricName(bubbleMetrics.size)}
</div>
- {colorsMetric && (
+ {bubbleMetrics.colors && (
<ColorRatingsLegend
className="sw-mt-2"
filters={ratingFilters}
- onRatingClick={this.handleRatingFilterClick}
+ onRatingClick={handleRatingFilterClick}
/>
)}
</div>
</div>
);
+ };
+
+ if (components.length <= 0) {
+ return <EmptyResult />;
}
- render() {
- if (this.props.components.length <= 0) {
- return <EmptyResult />;
- }
- const { domain } = this.props;
- const metrics = getBubbleMetrics(domain, this.props.metrics);
+ return (
+ <BubbleChartWrapper className="sw-relative sw-body-sm">
+ {renderChartHeader()}
+ {renderBubbleChart()}
+ <div className="sw-text-center">{getLocalizedMetricName(bubbleMetrics.x)}</div>
+ <YAxis className="sw-absolute sw-top-1/2 sw-left-3">
+ {getLocalizedMetricName(bubbleMetrics.y)}
+ </YAxis>
+ </BubbleChartWrapper>
+ );
+}
- return (
- <BubbleChartWrapper className="sw-relative sw-body-sm">
- {this.renderChartHeader(domain, metrics.size, metrics.colors)}
- {this.renderBubbleChart(metrics)}
- <div className="sw-text-center">{getLocalizedMetricName(metrics.x)}</div>
- <YAxis className="sw-absolute sw-top-1/2 sw-left-3">
- {getLocalizedMetricName(metrics.y)}
- </YAxis>
- </BubbleChartWrapper>
- );
+const getDescription = (domain: string) => {
+ const description = `component_measures.overview.${domain}.description`;
+ const translatedDescription = translate(description);
+ if (description === translatedDescription) {
+ return null;
}
-}
+ return translatedDescription;
+};
+
+const getMeasureVal = (component: ComponentMeasureEnhanced, metric: Metric) => {
+ const measure = component.measures.find((measure) => measure.metric.key === metric.key);
+ if (!measure) {
+ return undefined;
+ }
+ return Number(
+ getCCTMeasureValue(metric.key, isDiffMetric(metric.key) ? measure.leak : measure.value),
+ );
+};
+
+const getTooltip = (
+ component: ComponentMeasureEnhanced,
+ values: { colors?: Array<number | undefined>; size: number; x: number; y: number },
+ metrics: { colors?: Metric[]; size: Metric; x: Metric; y: Metric },
+) => {
+ const inner = [
+ [component.name, isProject(component.qualifier) ? component.branch : undefined]
+ .filter((s) => !!s)
+ .join(' / '),
+ `${metrics.x.name}: ${formatMeasure(values.x, metrics.x.type)}`,
+ `${metrics.y.name}: ${formatMeasure(values.y, metrics.y.type)}`,
+ `${metrics.size.name}: ${formatMeasure(values.size, metrics.size.type)}`,
+ ].filter((s) => !!s);
+ const { colors: valuesColors } = values;
+ const { colors: metricColors } = metrics;
+ if (valuesColors && metricColors) {
+ metricColors.forEach((metric, idx) => {
+ const colorValue = valuesColors[idx];
+ if (colorValue || colorValue === 0) {
+ inner.push(`${metric.name}: ${formatMeasure(colorValue, metric.type)}`);
+ }
+ });
+ }
+ return (
+ <div className="sw-text-left">
+ {inner.map((line, index) => (
+ <React.Fragment key={index}>
+ {line}
+ {index < inner.length - 1 && <br />}
+ </React.Fragment>
+ ))}
+ </div>
+ );
+};
const BubbleChartWrapper = styled.div`
color: ${themeColor('pageContentLight')};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ColorRatingsLegend.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ColorRatingsLegend.tsx
index 7633c1cdf0c..5919e1c6842 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ColorRatingsLegend.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ColorRatingsLegend.tsx
@@ -17,11 +17,19 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { ColorFilterOption, ColorsLegend } from 'design-system';
+import { useTheme } from '@emotion/react';
+import {
+ BubbleColorVal,
+ ColorFilterOption,
+ ColorsLegend,
+ themeColor,
+ themeContrast,
+} from 'design-system';
import * as React from 'react';
import { formatMeasure } from '~sonar-aligned/helpers/measures';
import { MetricType } from '~sonar-aligned/types/metrics';
import { translateWithParameters } from '../../../helpers/l10n';
+import { useIsLegacyCCTMode } from '../../../queries/settings';
export interface ColorRatingsLegendProps {
className?: string;
@@ -29,12 +37,14 @@ export interface ColorRatingsLegendProps {
onRatingClick: (selection: number) => void;
}
-const RATINGS = [1, 2, 3, 4, 5];
-
export default function ColorRatingsLegend(props: ColorRatingsLegendProps) {
+ const { data: isLegacy } = useIsLegacyCCTMode();
+ const theme = useTheme();
+ const RATINGS = isLegacy ? [1, 2, 3, 4, 5] : [1, 2, 3, 4];
+
const { className, filters } = props;
- const ratingsColors = RATINGS.map((rating) => {
+ const ratingsColors = RATINGS.map((rating: BubbleColorVal) => {
const formattedMeasure = formatMeasure(rating, MetricType.Rating);
return {
overlay: translateWithParameters('component_measures.legend.help_x', formattedMeasure),
@@ -42,6 +52,12 @@ export default function ColorRatingsLegend(props: ColorRatingsLegendProps) {
label: formattedMeasure,
value: rating,
selected: !filters[rating],
+ backgroundColor: themeColor(isLegacy ? `bubble.legacy.${rating}` : `bubble.${rating}`)({
+ theme,
+ }),
+ borderColor: themeContrast(isLegacy ? `bubble.legacy.${rating}` : `bubble.${rating}`)({
+ theme,
+ }),
};
});
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx
index affaad2fb09..b52c5f101cf 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx
@@ -79,10 +79,11 @@ export default function ComponentsList({ components, metric, metrics, ...props }
view={props.view}
/>
- <MeasureCell component={component} metric={metric} />
+ <MeasureCell branchLike={branchLike} component={component} metric={metric} />
{otherMetrics.map((metric) => (
<MeasureCell
+ branchLike={branchLike}
key={metric.key}
component={component}
measure={component.measures.find((measure) => measure.metric.key === metric.key)}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx
index d358d8c6d1a..2ef61788977 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx
@@ -30,13 +30,7 @@ import { translate, translateWithParameters } from '../../../helpers/l10n';
import { isDiffMetric, isPeriodBestValue } from '../../../helpers/measures';
import { BranchLike } from '../../../types/branch-like';
import { MeasurePageView } from '../../../types/measures';
-import {
- ComponentMeasure,
- ComponentMeasureEnhanced,
- Dict,
- Metric,
- Paging,
-} from '../../../types/types';
+import { Component, ComponentMeasureEnhanced, Dict, Metric, Paging } from '../../../types/types';
import ComponentsList from './ComponentsList';
interface Props {
@@ -50,7 +44,7 @@ interface Props {
metric: Metric;
metrics: Dict<Metric>;
paging?: Paging;
- rootComponent: ComponentMeasure;
+ rootComponent: Component;
selectedComponent?: ComponentMeasureEnhanced;
selectedIdx?: number;
view: MeasurePageView;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.tsx
index 61fbe5405a5..a3de418f7f8 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.tsx
@@ -21,15 +21,17 @@ import { NumericalCell } from 'design-system';
import * as React from 'react';
import Measure from '~sonar-aligned/components/measure/Measure';
import { getCCTMeasureValue, isDiffMetric } from '../../../helpers/measures';
+import { BranchLike } from '../../../types/branch-like';
import { ComponentMeasureEnhanced, MeasureEnhanced, Metric } from '../../../types/types';
interface Props {
+ branchLike?: BranchLike;
component: ComponentMeasureEnhanced;
measure?: MeasureEnhanced;
metric: Metric;
}
-export default function MeasureCell({ component, measure, metric }: Readonly<Props>) {
+export default function MeasureCell({ component, measure, metric, branchLike }: Readonly<Props>) {
const getValue = (item: { leak?: string; value?: string }) =>
isDiffMetric(metric.key) ? item.leak : item.value;
@@ -39,6 +41,7 @@ export default function MeasureCell({ component, measure, metric }: Readonly<Pro
return (
<NumericalCell className="sw-py-3">
<Measure
+ branchLike={branchLike}
componentKey={component.key}
metricKey={metric.key}
metricType={metric.type}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx
index 365eac7dba8..49cf36bb1f1 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx
@@ -46,6 +46,7 @@ import EmptyResult from './EmptyResult';
interface TreeMapViewProps {
components: ComponentMeasureEnhanced[];
handleSelect: (component: ComponentMeasureIntern) => void;
+ isLegacyMode: boolean;
metric: Metric;
}
@@ -56,7 +57,8 @@ interface State {
}
const PERCENT_SCALE_DOMAIN = [0, 25, 50, 75, 100];
-const RATING_SCALE_DOMAIN = [1, 2, 3, 4, 5];
+const RATING_SCALE_DOMAIN = [1, 2, 3, 4];
+const LEGACY_RATING_SCALE_DOMAIN = [1, 2, 3, 4, 5];
const HEIGHT = 500;
const NA_COLORS: [ThemeColors, ThemeColors] = ['treeMap.NA1', 'treeMap.NA2'];
@@ -67,6 +69,13 @@ const TREEMAP_COLORS: ThemeColors[] = [
'treeMap.D',
'treeMap.E',
];
+const TREEMAP_LEGACY_COLORS: ThemeColors[] = [
+ 'treeMap.legacy.A',
+ 'treeMap.legacy.B',
+ 'treeMap.legacy.C',
+ 'treeMap.legacy.D',
+ 'treeMap.legacy.E',
+];
export class TreeMapView extends React.PureComponent<Props, State> {
state: State;
@@ -140,8 +149,10 @@ export class TreeMapView extends React.PureComponent<Props, State> {
};
getMappedThemeColors = (): string[] => {
- const { theme } = this.props;
- return TREEMAP_COLORS.map((c) => themeColor(c)({ theme }));
+ const { theme, isLegacyMode } = this.props;
+ return (isLegacyMode ? TREEMAP_LEGACY_COLORS : TREEMAP_COLORS).map((c) =>
+ themeColor(c)({ theme }),
+ );
};
getLevelColorScale = () =>
@@ -159,8 +170,12 @@ export class TreeMapView extends React.PureComponent<Props, State> {
return color;
};
- getRatingColorScale = () =>
- scaleLinear<string, string>().domain(RATING_SCALE_DOMAIN).range(this.getMappedThemeColors());
+ getRatingColorScale = () => {
+ const { isLegacyMode } = this.props;
+ return scaleLinear<string, string>()
+ .domain(isLegacyMode ? LEGACY_RATING_SCALE_DOMAIN : RATING_SCALE_DOMAIN)
+ .range(this.getMappedThemeColors());
+ };
getColorScale = (metric: Metric) => {
if (metric.type === MetricType.Level) {
diff --git a/server/sonar-web/src/main/js/apps/component-measures/hooks.ts b/server/sonar-web/src/main/js/apps/component-measures/hooks.ts
new file mode 100644
index 00000000000..6cf2f3042bb
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/hooks.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 { areCCTMeasuresComputed, areSoftwareQualityRatingsComputed } from '../../helpers/measures';
+import { useIsLegacyCCTMode } from '../../queries/settings';
+import { MeasureEnhanced } from '../../types/types';
+import {
+ legacyBubbles,
+ newTaxonomyBubbles,
+ newTaxonomyWithoutRatingsBubbles,
+} from './config/bubbles';
+
+export function useBubbleChartMetrics(measures: MeasureEnhanced[]) {
+ const { data: isLegacyFlag } = useIsLegacyCCTMode();
+
+ if (isLegacyFlag || !areCCTMeasuresComputed(measures)) {
+ return legacyBubbles;
+ }
+
+ if (!areSoftwareQualityRatingsComputed(measures)) {
+ return newTaxonomyWithoutRatingsBubbles;
+ }
+
+ return newTaxonomyBubbles;
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigation.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigation.tsx
index a91187072a0..c7a46d59b37 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigation.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigation.tsx
@@ -33,6 +33,7 @@ import {
translate,
} from '../../../helpers/l10n';
import { MeasureEnhanced } from '../../../types/types';
+import { useBubbleChartMetrics } from '../hooks';
import {
addMeasureCategories,
getMetricSubnavigationName,
@@ -44,6 +45,7 @@ import DomainSubnavigationItem from './DomainSubnavigationItem';
interface Props {
componentKey: string;
domain: { measures: MeasureEnhanced[]; name: string };
+ measures: MeasureEnhanced[];
onChange: (metric: string) => void;
open: boolean;
selected: string;
@@ -51,16 +53,17 @@ interface Props {
}
export default function DomainSubnavigation(props: Readonly<Props>) {
- const { componentKey, domain, onChange, open, selected, showFullMeasures } = props;
+ const { componentKey, domain, onChange, open, selected, showFullMeasures, measures } = props;
const helperMessageKey = `component_measures.domain_subnavigation.${domain.name}.help`;
const helper = hasMessage(helperMessageKey) ? translate(helperMessageKey) : undefined;
const items = addMeasureCategories(domain.name, domain.measures);
+ const bubbles = useBubbleChartMetrics(measures);
const hasCategories = items.some((item) => typeof item === 'string');
const translateMetric = hasCategories ? getLocalizedCategoryMetricName : getLocalizedMetricName;
let sortedItems = sortMeasures(domain.name, items);
const hasOverview = (domain: string) => {
- return showFullMeasures && hasBubbleChart(domain);
+ return showFullMeasures && hasBubbleChart(bubbles, domain);
};
// sortedItems contains both measures (type object) and categories (type string)
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx
index 9a3cf5fb6b8..d3df6b22d57 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx
@@ -104,6 +104,7 @@ export default function Sidebar(props: Readonly<Props>) {
domain={domain}
key={domain.name}
onChange={handleChangeMetric}
+ measures={measures}
open={isDomainSelected(selectedMetric, domain)}
selected={selectedMetric}
showFullMeasures={showFullMeasures}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/SubnavigationMeasureValue.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/SubnavigationMeasureValue.tsx
index 3ac980e5bea..d6c55e29188 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/SubnavigationMeasureValue.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/SubnavigationMeasureValue.tsx
@@ -21,6 +21,7 @@ import { Note } from 'design-system';
import React from 'react';
import Measure from '~sonar-aligned/components/measure/Measure';
import { isDiffMetric } from '../../../helpers/measures';
+import { useBranchesQuery } from '../../../queries/branch';
import { MeasureEnhanced } from '../../../types/types';
interface Props {
@@ -30,6 +31,7 @@ interface Props {
export default function SubnavigationMeasureValue({ measure, componentKey }: Readonly<Props>) {
const isDiff = isDiffMetric(measure.metric.key);
+ const { data: { branchLike } = {} } = useBranchesQuery();
const value = isDiff ? measure.leak : measure.value;
return (
@@ -38,6 +40,7 @@ export default function SubnavigationMeasureValue({ measure, componentKey }: Rea
id={`measure-${measure.metric.key}-${isDiff ? 'leak' : 'value'}`}
>
<Measure
+ branchLike={branchLike}
componentKey={componentKey}
badgeSize="xs"
metricKey={measure.metric.key}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/utils.ts b/server/sonar-web/src/main/js/apps/component-measures/utils.ts
index 3f5a422ebfa..3e1177c38f1 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/utils.ts
+++ b/server/sonar-web/src/main/js/apps/component-measures/utils.ts
@@ -28,16 +28,20 @@ import {
HIDDEN_METRICS,
LEAK_CCT_SOFTWARE_QUALITY_METRICS,
LEAK_OLD_TAXONOMY_METRICS,
+ LEAK_OLD_TAXONOMY_RATINGS,
OLD_TAXONOMY_METRICS,
+ OLD_TAXONOMY_RATINGS,
+ SOFTWARE_QUALITY_RATING_METRICS,
} from '../../helpers/constants';
import { getLocalizedMetricName, translate } from '../../helpers/l10n';
import {
- MEASURES_REDIRECTION,
areCCTMeasuresComputed,
areLeakCCTMeasuresComputed,
+ areSoftwareQualityRatingsComputed,
getCCTMeasureValue,
getDisplayMetrics,
isDiffMetric,
+ MEASURES_REDIRECTION,
} from '../../helpers/measures';
import {
cleanQuery,
@@ -55,7 +59,7 @@ import {
MeasureEnhanced,
Metric,
} from '../../types/types';
-import { bubbles } from './config/bubbles';
+import { BubblesByDomain } from './config/bubbles';
import { domains } from './config/domains';
export const BUBBLES_FETCH_LIMIT = 500;
@@ -118,6 +122,16 @@ export const populateDomainsFromMeasures = memoize((measures: MeasureEnhanced[])
(measure) => !LEAK_OLD_TAXONOMY_METRICS.includes(measure.metric.key as MetricKey),
);
}
+
+ // Both new and overall code will exist after next analysis
+ if (areSoftwareQualityRatingsComputed(measures)) {
+ populatedMeasures = populatedMeasures.filter(
+ (measure) =>
+ !OLD_TAXONOMY_RATINGS.includes(measure.metric.key as MetricKey) &&
+ !LEAK_OLD_TAXONOMY_RATINGS.includes(measure.metric.key as MetricKey),
+ );
+ }
+
if (areCCTMeasuresComputed(measures)) {
populatedMeasures = populatedMeasures.filter(
(measure) => !OLD_TAXONOMY_METRICS.includes(measure.metric.key as MetricKey),
@@ -249,8 +263,8 @@ export function hasTreemap(metric: string, type: string): boolean {
);
}
-export function hasBubbleChart(domainName: string): boolean {
- return bubbles[domainName] !== undefined;
+export function hasBubbleChart(bubblesByDomain: BubblesByDomain, domainName: string): boolean {
+ return bubblesByDomain[domainName] !== undefined;
}
export function hasFacetStat(metric: string): boolean {
@@ -262,7 +276,11 @@ export function hasFullMeasures(branch?: BranchLike) {
}
export function getMeasuresPageMetricKeys(metrics: Dict<Metric>, branch?: BranchLike) {
- const metricKeys = getDisplayMetrics(Object.values(metrics)).map((metric) => metric.key);
+ // ToDo rollback once new metrics are available
+ const metricKeys = [
+ ...getDisplayMetrics(Object.values(metrics)).map((metric) => metric.key),
+ ...SOFTWARE_QUALITY_RATING_METRICS,
+ ];
if (isPullRequest(branch)) {
return metricKeys.filter((key) => isDiffMetric(key));
@@ -271,8 +289,12 @@ export function getMeasuresPageMetricKeys(metrics: Dict<Metric>, branch?: Branch
return metricKeys;
}
-export function getBubbleMetrics(domain: string, metrics: Dict<Metric>) {
- const conf = bubbles[domain];
+export function getBubbleMetrics(
+ bubblesByDomain: BubblesByDomain,
+ domain: string,
+ metrics: Dict<Metric>,
+) {
+ const conf = bubblesByDomain[domain];
return {
x: metrics[conf.x],
y: metrics[conf.y],
@@ -281,8 +303,8 @@ export function getBubbleMetrics(domain: string, metrics: Dict<Metric>) {
};
}
-export function getBubbleYDomain(domain: string) {
- return bubbles[domain].yDomain;
+export function getBubbleYDomain(bubblesByDomain: BubblesByDomain, domain: string) {
+ return bubblesByDomain[domain].yDomain;
}
export function isProjectOverview(metric: string) {
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/NewCodeMeasuresPanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/NewCodeMeasuresPanel.tsx
index cd576b12c52..e56bf23087c 100644
--- a/server/sonar-web/src/main/js/apps/overview/branches/NewCodeMeasuresPanel.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/branches/NewCodeMeasuresPanel.tsx
@@ -292,6 +292,7 @@ export default function NewCodeMeasuresPanel(props: Readonly<Props>) {
icon={
newSecurityReviewRating ? (
<RatingComponent
+ branchLike={branch}
componentKey={component.key}
ratingMetric={MetricKey.new_security_review_rating}
size="md"
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/OverallCodeMeasuresPanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/OverallCodeMeasuresPanel.tsx
index a669f5ce1be..dcf7161a700 100644
--- a/server/sonar-web/src/main/js/apps/overview/branches/OverallCodeMeasuresPanel.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/branches/OverallCodeMeasuresPanel.tsx
@@ -195,6 +195,7 @@ export default function OverallCodeMeasuresPanel(props: Readonly<OverallCodeMeas
icon={
securityRating ? (
<RatingComponent
+ branchLike={branch}
componentKey={component.key}
ratingMetric={MetricKey.security_review_rating}
size="md"
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/QualityGateCondition.tsx b/server/sonar-web/src/main/js/apps/overview/branches/QualityGateCondition.tsx
index 3563e8d3865..9a56ff4a132 100644
--- a/server/sonar-web/src/main/js/apps/overview/branches/QualityGateCondition.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/branches/QualityGateCondition.tsx
@@ -145,7 +145,7 @@ export class QualityGateCondition extends React.PureComponent<Props> {
};
render() {
- const { condition, component } = this.props;
+ const { condition, component, branchLike } = this.props;
const { measure } = condition;
const { metric } = measure;
@@ -157,6 +157,7 @@ export class QualityGateCondition extends React.PureComponent<Props> {
return this.wrapWithLink(
<div className="sw-flex sw-items-center sw-p-2">
<MeasureIndicator
+ branchLike={branchLike}
className="sw-flex sw-justify-center sw-w-6 sw-mx-4"
decimals={2}
componentKey={component.key}
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 17800780d9b..c564b1bf1b4 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
@@ -136,6 +136,7 @@ export function SoftwareImpactMeasureCard(props: Readonly<SoftwareImpactBreakdow
<div className="sw-flex-grow sw-flex sw-justify-end">
<SoftwareImpactMeasureRating
+ branch={branch}
softwareQuality={softwareQuality}
componentKey={component.key}
ratingMetricKey={ratingMetricKey}
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureRating.tsx b/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureRating.tsx
index 2444c884047..b5eeb3d48af 100644
--- a/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureRating.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureRating.tsx
@@ -23,16 +23,18 @@ import { useCallback } from 'react';
import { useIntl } from 'react-intl';
import RatingComponent from '../../../app/components/metrics/RatingComponent';
import { MetricKey } from '../../../sonar-aligned/types/metrics';
+import { Branch } from '../../../types/branch-like';
import { SoftwareImpactSeverity, SoftwareQuality } from '../../../types/clean-code-taxonomy';
export interface SoftwareImpactMeasureRatingProps {
+ branch?: Branch;
componentKey: string;
ratingMetricKey: MetricKey;
softwareQuality: SoftwareQuality;
}
export function SoftwareImpactMeasureRating(props: Readonly<SoftwareImpactMeasureRatingProps>) {
- const { ratingMetricKey, componentKey, softwareQuality } = props;
+ const { ratingMetricKey, componentKey, softwareQuality, branch } = props;
const intl = useIntl();
@@ -101,6 +103,7 @@ export function SoftwareImpactMeasureRating(props: Readonly<SoftwareImpactMeasur
return (
<RatingComponent
+ branchLike={branch}
size="md"
className="sw-text-sm"
ratingMetric={ratingMetricKey}
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx
index 13ac9822b97..6358fa67dd9 100644
--- a/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx
@@ -20,11 +20,10 @@
import { BasicSeparator, CenteredLayout, PageContentFontWrapper, Spinner } from 'design-system';
import { uniq } from 'lodash';
import * as React from 'react';
-import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
import { enhanceConditionWithMeasure, enhanceMeasuresWithMetrics } from '../../../helpers/measures';
import { isDefined } from '../../../helpers/types';
import { useBranchStatusQuery } from '../../../queries/branch';
-import { useComponentMeasuresWithMetricsQuery } from '../../../queries/component';
+import { useMeasuresComponentQuery } from '../../../queries/measures';
import { useComponentQualityGateQuery } from '../../../queries/quality-gates';
import { PullRequest } from '../../../types/branch-like';
import { Component } from '../../../types/types';
@@ -55,13 +54,14 @@ export default function PullRequestOverview(props: Readonly<Readonly<Props>>) {
component.key,
);
- const { data: componentMeasures, isLoading: isLoadingMeasures } =
- useComponentMeasuresWithMetricsQuery(
- component.key,
- uniq([...PR_METRICS, ...(conditions?.map((c) => c.metric) ?? [])]),
- getBranchLikeQuery(pullRequest),
- !isLoadingBranchStatusesData,
- );
+ const { data: componentMeasures, isLoading: isLoadingMeasures } = useMeasuresComponentQuery(
+ {
+ componentKey: component.key,
+ metricKeys: uniq([...PR_METRICS, ...(conditions?.map((c) => c.metric) ?? [])]),
+ branchLike: pullRequest,
+ },
+ { enabled: !isLoadingBranchStatusesData },
+ );
const measures = componentMeasures
? enhanceMeasuresWithMetrics(
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSidebarHeader.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSidebarHeader.tsx
index ef2941c81d3..81761c5c5dd 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSidebarHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSidebarHeader.tsx
@@ -83,6 +83,7 @@ function HotspotSidebarHeader(props: SecurityHotspotsAppRendererProps) {
{component && (
<Measure
+ branchLike={branchLike}
className="it__hs-review-percentage sw-body-sm-highlight sw-ml-2"
componentKey={component.key}
metricKey={