diff options
author | stanislavh <stanislav.honcharov@sonarsource.com> | 2024-08-09 15:15:03 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-08-26 20:03:06 +0000 |
commit | cb110bccb888b7089a12ffc6e03ed41f9b70749d (patch) | |
tree | a844f0cd93e39d6c0f6d207a4666e57812a23eb0 /server/sonar-web/src/main/js/apps | |
parent | 4131eba5b8113c3cd5fdd23fcad871c128a5c297 (diff) | |
download | sonarqube-cb110bccb888b7089a12ffc6e03ed41f9b70749d.tar.gz sonarqube-cb110bccb888b7089a12ffc6e03ed41f9b70749d.zip |
SONAR-22719 Measures page reflects new ratings
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
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={ |