]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22719 Measures page reflects new ratings
authorstanislavh <stanislav.honcharov@sonarsource.com>
Fri, 9 Aug 2024 13:15:03 +0000 (15:15 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 26 Aug 2024 20:03:06 +0000 (20:03 +0000)
45 files changed:
server/sonar-web/design-system/src/components/BubbleChart.tsx
server/sonar-web/design-system/src/components/ColorsLegend.tsx
server/sonar-web/design-system/src/theme/light.ts
server/sonar-web/src/main/js/api/components.ts
server/sonar-web/src/main/js/api/measures.ts
server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts
server/sonar-web/src/main/js/api/mocks/MeasuresServiceMock.ts
server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts
server/sonar-web/src/main/js/app/components/metrics/RatingComponent.tsx
server/sonar-web/src/main/js/apps/code/components/Component.tsx
server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx
server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx
server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx
server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx
server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx [deleted file]
server/sonar-web/src/main/js/apps/component-measures/components/MeasuresBreadcrumbs.tsx
server/sonar-web/src/main/js/apps/component-measures/config/bubbles.ts
server/sonar-web/src/main/js/apps/component-measures/config/domains.ts
server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChartView.tsx
server/sonar-web/src/main/js/apps/component-measures/drilldown/ColorRatingsLegend.tsx
server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx
server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx
server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.tsx
server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx
server/sonar-web/src/main/js/apps/component-measures/hooks.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigation.tsx
server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx
server/sonar-web/src/main/js/apps/component-measures/sidebar/SubnavigationMeasureValue.tsx
server/sonar-web/src/main/js/apps/component-measures/utils.ts
server/sonar-web/src/main/js/apps/overview/branches/NewCodeMeasuresPanel.tsx
server/sonar-web/src/main/js/apps/overview/branches/OverallCodeMeasuresPanel.tsx
server/sonar-web/src/main/js/apps/overview/branches/QualityGateCondition.tsx
server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureCard.tsx
server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureRating.tsx
server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSidebarHeader.tsx
server/sonar-web/src/main/js/components/measure/MeasureIndicator.tsx
server/sonar-web/src/main/js/helpers/constants.ts
server/sonar-web/src/main/js/helpers/measures.ts
server/sonar-web/src/main/js/queries/component.ts
server/sonar-web/src/main/js/queries/measures.ts
server/sonar-web/src/main/js/sonar-aligned/components/measure/Measure.tsx
server/sonar-web/src/main/js/sonar-aligned/types/metrics.ts

index 108527dbb93fce4392629ccfe23010068813bdab..bdca3c8f6a7ea4f03963019b362ee9385b9f4a87 100644 (file)
@@ -17,7 +17,6 @@
  * 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 classNames from 'classnames';
 import { max, min } from 'd3-array';
@@ -31,13 +30,13 @@ import tw from 'twin.macro';
 import { themeColor, themeContrast } from '../helpers';
 import { ButtonSecondary } from '../sonar-aligned/components/buttons';
 import { Note } from '../sonar-aligned/components/typography';
-import { BubbleColorVal } from '../types/charts';
 import { Tooltip } from './Tooltip';
 
 const TICKS_COUNT = 5;
 
 interface BubbleItem<T> {
-  color?: BubbleColorVal;
+  backgroundColor?: string;
+  borderColor?: string;
   data?: T;
   key?: string;
   size: number;
@@ -312,7 +311,8 @@ export function BubbleChart<T>(props: BubbleChartProps<T>) {
     const bubbles = sortBy(items, (b) => -b.size).map((item, index) => {
       return (
         <Bubble
-          color={item.color}
+          backgroundColor={item.backgroundColor}
+          borderColor={item.borderColor}
           data={item.data}
           key={item.key ?? index}
           onClick={props.onBubbleClick}
@@ -388,7 +388,8 @@ export function BubbleChart<T>(props: BubbleChartProps<T>) {
 }
 
 interface BubbleProps<T> {
-  color?: BubbleColorVal;
+  backgroundColor?: string;
+  borderColor?: string;
   data?: T;
   onClick?: (ref?: T) => void;
   r: number;
@@ -399,8 +400,7 @@ interface BubbleProps<T> {
 }
 
 function Bubble<T>(props: BubbleProps<T>) {
-  const theme = useTheme();
-  const { color, data, onClick, r, scale, tooltip, x, y } = props;
+  const { backgroundColor, borderColor, data, onClick, r, scale, tooltip, x, y } = props;
   const handleClick = React.useCallback(
     (event: React.MouseEvent<HTMLAnchorElement>) => {
       event.stopPropagation();
@@ -415,8 +415,8 @@ function Bubble<T>(props: BubbleProps<T>) {
       <BubbleStyled
         r={r}
         style={{
-          fill: color ? themeColor(`bubble.${color}`)({ theme }) : '',
-          stroke: color ? themeContrast(`bubble.${color}`)({ theme }) : '',
+          fill: backgroundColor ?? '',
+          stroke: borderColor ?? '',
         }}
         transform={`translate(${x}, ${y}) scale(${scale})`}
       />
index 43f7c415241db652f97db80ca2f87bc903e3b2e4..5974a136379e8e6f65aa588971bf32f5dd0a133b 100644 (file)
@@ -21,7 +21,7 @@ import { useTheme } from '@emotion/react';
 import styled from '@emotion/styled';
 import tw from 'twin.macro';
 import { themeBorder, themeColor, themeContrast } from '../helpers';
-import { BubbleColorVal } from '../types/charts';
+import { BubbleColorVal } from '../types';
 import { Tooltip } from './Tooltip';
 import { Checkbox } from './input/Checkbox';
 
@@ -63,11 +63,15 @@ export function ColorsLegend(props: ColorLegendProps) {
                     color.selected
                       ? {
                           backgroundColor:
-                            color.borderColor ??
-                            themeColor(`bubble.${(idx + 1) as BubbleColorVal}`)({ theme }),
-                          borderColor:
                             color.backgroundColor ??
-                            themeContrast(`bubble.${(idx + 1) as BubbleColorVal}`)({ theme }),
+                            themeColor(`bubble.legacy.${(idx + 1) as BubbleColorVal}`)({
+                              theme,
+                            }),
+                          borderColor:
+                            color.borderColor ??
+                            themeContrast(`bubble.legacy.${(idx + 1) as BubbleColorVal}`)({
+                              theme,
+                            }),
                         }
                       : {}
                   }
index cd17f9674f5736e38cc355ee9bd9a2488ce2ac03..098ca5ce763d8d46dbd7e85b40844be9194ddede 100644 (file)
@@ -629,18 +629,31 @@ export const lightTheme = {
     // bubble charts
     bubbleChartLine: COLORS.grey[50],
     bubbleDefault: [...COLORS.blue[500], 0.3],
+    'bubble.legacy.1': [...COLORS.green[500], 0.3],
+    'bubble.legacy.2': [...COLORS.yellowGreen[500], 0.3],
+    'bubble.legacy.3': [...COLORS.yellow[500], 0.3],
+    'bubble.legacy.4': [...COLORS.orange[500], 0.3],
+    'bubble.legacy.5': [...COLORS.red[500], 0.3],
+
     'bubble.1': [...COLORS.green[500], 0.3],
     'bubble.2': [...COLORS.yellowGreen[500], 0.3],
     'bubble.3': [...COLORS.yellow[500], 0.3],
-    'bubble.4': [...COLORS.orange[500], 0.3],
+    'bubble.4': [...COLORS.red[500], 0.3],
     'bubble.5': [...COLORS.red[500], 0.3],
 
     // TreeMap Colors
+    'treeMap.legacy.A': COLORS.green[500],
+    'treeMap.legacy.B': COLORS.yellowGreen[500],
+    'treeMap.legacy.C': COLORS.yellow[500],
+    'treeMap.legacy.D': COLORS.orange[500],
+    'treeMap.legacy.E': COLORS.red[500],
+
     'treeMap.A': COLORS.green[500],
     'treeMap.B': COLORS.yellowGreen[500],
     'treeMap.C': COLORS.yellow[500],
-    'treeMap.D': COLORS.orange[500],
+    'treeMap.D': COLORS.red[500],
     'treeMap.E': COLORS.red[500],
+
     'treeMap.NA1': COLORS.blueGrey[300],
     'treeMap.NA2': COLORS.blueGrey[200],
     treeMapCellTextColor: COLORS.blueGrey[900],
@@ -907,10 +920,16 @@ export const lightTheme = {
 
     // bubble charts
     bubbleDefault: COLORS.blue[500],
+    'bubble.legacy.1': COLORS.green[500],
+    'bubble.legacy.2': COLORS.yellowGreen[500],
+    'bubble.legacy.3': COLORS.yellow[500],
+    'bubble.legacy.4': COLORS.orange[500],
+    'bubble.legacy.5': COLORS.red[500],
+
     'bubble.1': COLORS.green[500],
     'bubble.2': COLORS.yellowGreen[500],
     'bubble.3': COLORS.yellow[500],
-    'bubble.4': COLORS.orange[500],
+    'bubble.4': COLORS.red[500],
     'bubble.5': COLORS.red[500],
 
     // news bar
index 654b004e92f44c8a914af63dedeee1f0b479d7b1..7c3d752adda5d283a57adaf46178f87fa229c21a 100644 (file)
@@ -91,14 +91,6 @@ export function getComponentTree(
   return getJSON(url, data).catch(throwGlobalError);
 }
 
-export function getComponentLeaves(
-  component: string,
-  metrics: string[] = [],
-  additional: RequestData = {},
-) {
-  return getComponentTree('leaves', component, metrics, additional);
-}
-
 export function getComponent(
   data: { component: string; metricKeys: string } & BranchParameters,
 ): Promise<{ component: ComponentMeasure }> {
index b7e1a89cf84216160b934b7a1e578e3cd79094d2..2cc7089cdcaa307ced5996ca6650f957e87d9566 100644 (file)
@@ -35,32 +35,6 @@ export function getMeasures(
   return getJSON(COMPONENT_URL, data).then((r) => r.component.measures, throwGlobalError);
 }
 
-export function getMeasuresWithMetrics(
-  component: string,
-  metrics: string[],
-  branchParameters?: BranchParameters,
-): Promise<MeasuresAndMetaWithMetrics> {
-  return getJSON(COMPONENT_URL, {
-    additionalFields: 'metrics',
-    component,
-    metricKeys: metrics.join(','),
-    ...branchParameters,
-  }).catch(throwGlobalError);
-}
-
-export function getMeasuresWithPeriod(
-  component: string,
-  metrics: string[],
-  branchParameters?: BranchParameters,
-): Promise<MeasuresAndMetaWithPeriod> {
-  return getJSON(COMPONENT_URL, {
-    additionalFields: 'period',
-    component,
-    metricKeys: metrics.join(','),
-    ...branchParameters,
-  }).catch(throwGlobalError);
-}
-
 export function getMeasuresWithPeriodAndMetrics(
   component: string,
   metrics: string[],
index 0a322db27d6f5714fdfb4b577c10b43a15545436..1d1ed348c38913a840b8d983f1a38fa62dd5870f 100644 (file)
@@ -43,7 +43,6 @@ import {
   getComponent,
   getComponentData,
   getComponentForSourceViewer,
-  getComponentLeaves,
   getComponentTree,
   getDuplications,
   getSources,
@@ -103,7 +102,6 @@ export default class ComponentsServiceMock {
     jest.mocked(getDuplications).mockImplementation(this.handleGetDuplications);
     jest.mocked(getSources).mockImplementation(this.handleGetSources);
     jest.mocked(changeKey).mockImplementation(this.handleChangeKey);
-    jest.mocked(getComponentLeaves).mockImplementation(this.handleGetComponentLeaves);
     jest.mocked(getBreadcrumbs).mockImplementation(this.handleGetBreadcrumbs);
     jest.mocked(setProjectTags).mockImplementation(this.handleSetProjectTags);
     jest.mocked(setApplicationTags).mockImplementation(this.handleSetApplicationTags);
@@ -383,19 +381,6 @@ export default class ComponentsServiceMock {
     return Promise.reject({ status: 404, message: 'Component not found' });
   };
 
-  handleGetComponentLeaves = (
-    component: string,
-    metrics: string[] = [],
-    data: RequestData = {},
-  ): Promise<{
-    baseComponent: ComponentMeasure;
-    components: ComponentMeasure[];
-    metrics: Metric[];
-    paging: Paging;
-  }> => {
-    return this.handleGetComponentTree('leaves', component, metrics, data);
-  };
-
   handleGetBreadcrumbs = ({ component: key }: { component: string } & BranchParameters) => {
     const base = this.findComponentTree(key);
     if (base === undefined) {
index 6c9fb99a6c2be7965bfa323034cf30130d0c47cb..45a5dedc2b006314ff3c0210c87b49acbff19acd 100644 (file)
@@ -22,7 +22,7 @@ import { BranchParameters } from '~sonar-aligned/types/branch-like';
 import { MetricKey } from '~sonar-aligned/types/metrics';
 import { mockMetric, mockPeriod } from '../../helpers/testMocks';
 import { Metric, Period } from '../../types/types';
-import { getMeasures, getMeasuresWithPeriod, getMeasuresWithPeriodAndMetrics } from '../measures';
+import { getMeasures, getMeasuresWithPeriodAndMetrics } from '../measures';
 import { ComponentTree, mockFullComponentTree } from './data/components';
 import { mockIssuesList } from './data/issues';
 import { MeasureRecords, getMetricTypeFromKey, mockFullMeasureData } from './data/measures';
@@ -51,7 +51,6 @@ export class MeasuresServiceMock {
     };
 
     jest.mocked(getMeasures).mockImplementation(this.handleGetMeasures);
-    jest.mocked(getMeasuresWithPeriod).mockImplementation(this.handleGetMeasuresWithPeriod);
     jest
       .mocked(getMeasuresWithPeriodAndMetrics)
       .mockImplementation(this.handleGetMeasuresWithPeriodAndMetrics);
@@ -107,23 +106,6 @@ export class MeasuresServiceMock {
     return this.reply(measures);
   };
 
-  handleGetMeasuresWithPeriod = (
-    component: string,
-    metrics: string[],
-    _branchParameters?: BranchParameters,
-  ) => {
-    const entry = this.findComponentTree(component);
-    const measures = this.filterMeasures(entry.component.key, metrics);
-
-    return this.reply({
-      component: {
-        ...entry.component,
-        measures,
-      },
-      period: this.#period,
-    });
-  };
-
   handleGetMeasuresWithPeriodAndMetrics = (componentKey: string, metricKeys: string[]) => {
     const { component } = this.findComponentTree(componentKey);
     const measures = this.filterMeasures(component.key, metricKeys);
index cd33aaa83e1604b7f2d927caf9d4cf72745909c4..49d90bfbdf5dcf79ce5ad31891e2801d66026044 100644 (file)
@@ -137,6 +137,10 @@ export default class SettingsServiceMock {
       key: SettingsKey.QPAdminCanDisableInheritedRules,
       value: 'true',
     },
+    {
+      key: 'sonar.old_world',
+      value: 'false',
+    },
   ];
 
   #settingValues: SettingValue[] = cloneDeep(this.#defaultValues);
index 5f3990dd8a7cde3608dc431d117323b6393329d6..69132914ef846a5beffad8e7785e2ec50555b8a4 100644 (file)
@@ -26,8 +26,10 @@ import { getLeakValue } from '../../../components/measure/utils';
 import { isDiffMetric } from '../../../helpers/measures';
 import { useMeasureQuery } from '../../../queries/measures';
 import { useIsLegacyCCTMode } from '../../../queries/settings';
+import { BranchLike } from '../../../types/branch-like';
 
 interface Props {
+  branchLike?: BranchLike;
   className?: string;
   componentKey: string;
   getLabel?: (rating: RatingEnum) => string;
@@ -43,8 +45,17 @@ type RatingMetricKeys =
   | MetricKey.security_review_rating
   | MetricKey.releasability_rating;
 
+function isNewRatingMetric(metricKey: MetricKey) {
+  return metricKey.includes('_new');
+}
+
 const useGetMetricKeyForRating = (ratingMetric: RatingMetricKeys): MetricKey | null => {
   const { data: isLegacy, isLoading } = useIsLegacyCCTMode();
+
+  if (isNewRatingMetric(ratingMetric)) {
+    return ratingMetric;
+  }
+
   if (isLoading) {
     return null;
   }
@@ -52,16 +63,18 @@ const useGetMetricKeyForRating = (ratingMetric: RatingMetricKeys): MetricKey | n
 };
 
 export default function RatingComponent(props: Readonly<Props>) {
-  const { componentKey, ratingMetric, size, className, getLabel, getTooltip } = props;
+  const { componentKey, ratingMetric, size, className, getLabel, branchLike, getTooltip } = props;
+
   const metricKey = useGetMetricKeyForRating(ratingMetric as RatingMetricKeys);
   const { data: isLegacy } = useIsLegacyCCTMode();
   const { data: targetMeasure, isLoading: isLoadingTargetMeasure } = useMeasureQuery(
-    { componentKey, metricKey: metricKey ?? '' },
+    { componentKey, metricKey: metricKey ?? '', branchLike },
     { enabled: !!metricKey },
   );
+
   const { data: oldMeasure, isLoading: isLoadingOldMeasure } = useMeasureQuery(
-    { componentKey, metricKey: ratingMetric },
-    { enabled: !isLegacy && targetMeasure === null },
+    { componentKey, metricKey: ratingMetric, branchLike },
+    { enabled: !isLegacy && !isNewRatingMetric(ratingMetric) && targetMeasure === null },
   );
 
   const isLoading = isLoadingTargetMeasure || isLoadingOldMeasure;
index 133f2331c77b8a7745e9a6f1cb486006bffaa067..3377e14205ebf5981b114a70409f20e7cef50337 100644 (file)
@@ -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 && (
index 2e76c81919df9543d13717d482e23cbb2a0b9b76..ba8d3b50824e36b65be9b7dc1b66a65ccf1a6bc2 100644 (file)
@@ -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}
index 6cc9c75fe769daad50f7812e7ac462a2ae8aa003..50046f5f08dd3f853dd99e6469ced45ced825df3 100644 (file)
@@ -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();
index b5c6128f0e75ee97f32cf0b8fc321ca5fae6f409..b3f01a65d3b8dbb08f28a322335122dac26f4dd9 100644 (file)
@@ -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')};
index e9b232a4268b08e5632f1888792d67a00704210e..dd252561131b96f675a08151344f932f6f1fae7f 100644 (file)
  * 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 };
 }
index ad7ace64e8ba7dea23dcd7551fc12e0201b47cb0..ccece093bb736c92ede95da59e7f7fc8c5ab063d 100644 (file)
@@ -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}
index 16950620500420e7487250ed70db5bd215665999..98ce0aa3f81a34948cfa112366339aba557c06d5 100644 (file)
  * 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 (file)
index 9455a77..0000000
+++ /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}
-      />
-    );
-  }
-}
index 0284d8bc4a136510fb398288b8574e38f4c99418..28648e7acd15016496af9dd487f6e13136d43b74 100644 (file)
@@ -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 {
index f660bda32dbeeb99780c47c103f1c04614575e68..a8e559971743cac5ae2bd38b187ecdb7dff0c1dd 100644 (file)
  */
 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,
index 6eb98c969f22b7720bbd9e7fdb84d81f6dbb4da8..a98910ded13511832b1c4f915229c647e09907a0 100644 (file)
@@ -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,
     ],
   },
 
index aac6a045d291ff66589e0b9c01233f83b85ae443..1e434ec3079c5b65fe571288becfd9beafdecff1 100644 (file)
@@ -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')};
index 7633c1cdf0ca5000433c7bf36e0e4e8881f1324f..5919e1c6842a329a25a15b4bfe01edd9a843eed6 100644 (file)
  * 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,
+      }),
     };
   });
 
index affaad2fb09a7b2d29644438a374a5cd42210003..b52c5f101cfa880f7730de0a58ea35e7ec0b841d 100644 (file)
@@ -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)}
index d358d8c6d1a84686b460851486832e750e3ccc04..2ef617889773cd19ff759b04e91705659cc92ff7 100644 (file)
@@ -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;
index 61fbe5405a516df1c24f53c3ec17dd7c9b3a3b5f..a3de418f7f807834ce196ab077694f2139df4f9e 100644 (file)
@@ -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}
index 365eac7dba82dc752f526da6ed2a5f949965dbe6..49cf36bb1f1a32d5d5fc1eeeeeea77e9afad0382 100644 (file)
@@ -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 (file)
index 0000000..6cf2f30
--- /dev/null
@@ -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;
+}
index a91187072a0186688b3e859e9954b0cc910fcb69..c7a46d59b372d93f2a2a96cf4823d61794cf1e7f 100644 (file)
@@ -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)
index 9a3cf5fb6b8c890c5d2639c2b171626ede75959b..d3df6b22d572fd450f745eab11e69417ff79761d 100644 (file)
@@ -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}
index 3ac980e5bea9f70f16f40a6081fb3b7b9551d98c..d6c55e29188f6fba3ca76053f9683bad1581b0ce 100644 (file)
@@ -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}
index 3f5a422ebfa0feddd7ebd12d7ba15b60e7a0f4bc..3e1177c38f1023348067b42e6abbf06fe9e11843 100644 (file)
@@ -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) {
index cd576b12c52f5e09f2043ba8b12dbee434b576f6..e56bf23087c80b3d11726712c29490650321f8f8 100644 (file)
@@ -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"
index a669f5ce1be286311848c93aa2e511a29e1b8510..dcf7161a7004a1ae718d2076424901c6a46169a4 100644 (file)
@@ -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"
index 3563e8d38651e2e0c9702e8505464afe07e89453..9a56ff4a132a96798ad088225de70d05897fb006 100644 (file)
@@ -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}
index 17800780d9b11b470eafc1d2659e2c3f3c7285a0..c564b1bf1b4cdb832da711a8722293fa0890cd0e 100644 (file)
@@ -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}
index 2444c884047e4d576c7a900cd46f7b28fdda9b86..b5eeb3d48af7d176128c651c4d9022319d18024f 100644 (file)
@@ -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}
index 13ac9822b9700fd1b7604084b042271a79ef604d..6358fa67dd9b8c95b56c58ff236706e95e4f923b 100644 (file)
 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(
index ef2941c81d3e26fa7e2febaa26dc68979f9c4187..81761c5c5ddd201cb84eae41203c15ff993204f2 100644 (file)
@@ -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={
index e6f9b7b166829c6763fe1827d1872aa48b9955e7..2048193ac0b86c3a05099769690b8e91742e3d1d 100644 (file)
@@ -21,9 +21,11 @@ import { CoverageIndicator, DuplicationsIndicator } from 'design-system';
 import * as React from 'react';
 import Measure from '~sonar-aligned/components/measure/Measure';
 import { MetricKey, MetricType } from '~sonar-aligned/types/metrics';
+import { BranchLike } from '../../types/branch-like';
 import { duplicationRatingConverter } from './utils';
 
 interface Props {
+  branchLike?: BranchLike;
   className?: string;
   componentKey: string;
   decimals?: number;
index 1878e24ba20cbf404c32ab59440918e93a5f8624..96df373e57c46186f9000a6242fe0e6a652b54b1 100644 (file)
@@ -111,6 +111,20 @@ export const LEAK_OLD_TAXONOMY_METRICS = [
   MetricKey.new_code_smells,
 ];
 
+export const OLD_TAXONOMY_RATINGS = [
+  MetricKey.sqale_rating,
+  MetricKey.security_rating,
+  MetricKey.reliability_rating,
+  MetricKey.security_review_rating,
+];
+
+export const LEAK_OLD_TAXONOMY_RATINGS = [
+  MetricKey.new_maintainability_rating,
+  MetricKey.new_security_rating,
+  MetricKey.new_reliability_rating,
+  MetricKey.new_security_review_rating,
+];
+
 export const OLD_TO_NEW_TAXONOMY_METRICS_MAP: { [key in MetricKey]?: MetricKey } = {
   [MetricKey.vulnerabilities]: MetricKey.security_issues,
   [MetricKey.bugs]: MetricKey.reliability_issues,
@@ -166,6 +180,30 @@ export const DEPRECATED_ACTIVITY_METRICS = [
   MetricKey.confirmed_issues,
 ];
 
+export const SOFTWARE_QUALITY_RATING_METRICS_MAP: Record<string, MetricKey> = {
+  [MetricKey.sqale_rating]: MetricKey.sqale_rating_new,
+  [MetricKey.security_rating]: MetricKey.security_rating_new,
+  [MetricKey.reliability_rating]: MetricKey.reliability_rating_new,
+  [MetricKey.security_review_rating]: MetricKey.security_review_rating_new,
+  [MetricKey.releasability_rating]: MetricKey.releasability_rating_new,
+  [MetricKey.new_maintainability_rating]: MetricKey.new_maintainability_rating_new,
+  [MetricKey.new_security_rating]: MetricKey.new_security_rating_new,
+  [MetricKey.new_reliability_rating]: MetricKey.new_reliability_rating_new,
+  [MetricKey.new_security_review_rating]: MetricKey.new_security_review_rating_new,
+};
+
+export const SOFTWARE_QUALITY_RATING_METRICS = [
+  MetricKey.sqale_rating_new,
+  MetricKey.security_rating_new,
+  MetricKey.reliability_rating_new,
+  MetricKey.security_review_rating_new,
+  MetricKey.releasability_rating_new,
+  MetricKey.new_maintainability_rating_new,
+  MetricKey.new_security_rating_new,
+  MetricKey.new_reliability_rating_new,
+  MetricKey.new_security_review_rating_new,
+];
+
 export const PROJECT_KEY_MAX_LEN = 400;
 
 export const IMPORT_COMPATIBLE_ALMS = [
index 4a3e774ded7fbb9bd736d70e5ca09ea792848791..6b0d5cb0687934bdd446ec8731928bc2dc139db4 100644 (file)
@@ -27,6 +27,7 @@ import {
   CCT_SOFTWARE_QUALITY_METRICS,
   LEAK_CCT_SOFTWARE_QUALITY_METRICS,
   LEAK_OLD_TAXONOMY_METRICS,
+  SOFTWARE_QUALITY_RATING_METRICS,
 } from './constants';
 import { translate } from './l10n';
 import { isDefined } from './types';
@@ -114,6 +115,13 @@ export function areCCTMeasuresComputed(measures?: Measure[] | MeasureEnhanced[])
     ),
   );
 }
+export function areSoftwareQualityRatingsComputed(measures?: Measure[] | MeasureEnhanced[]) {
+  return SOFTWARE_QUALITY_RATING_METRICS.every((metric) =>
+    measures?.find((measure) =>
+      isMeasureEnhanced(measure) ? measure.metric.key === metric : measure.metric === metric,
+    ),
+  );
+}
 
 export function areLeakAndOverallCCTMeasuresComputed(measures?: Measure[] | MeasureEnhanced[]) {
   return areLeakCCTMeasuresComputed(measures) && areCCTMeasuresComputed(measures);
index b3d6445a870e1af05a24f95504ad8567459fae81..31596b4fcd89edebac2ffe1e304b4a86d921f164 100644 (file)
@@ -18,7 +18,6 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import {
-  UseQueryResult,
   infiniteQueryOptions,
   queryOptions,
   useQuery,
@@ -33,10 +32,8 @@ import {
   getComponentData,
   getComponentTree,
 } from '../api/components';
-import { getMeasuresWithMetrics } from '../api/measures';
 import { getNextPageParam, getPreviousPageParam } from '../helpers/react-query';
 import { MetricKey } from '../sonar-aligned/types/metrics';
-import { MeasuresAndMetaWithMetrics } from '../types/measures';
 import { Component, Measure } from '../types/types';
 import { StaleTime, createInfiniteQueryHook, createQueryHook } from './common';
 
@@ -75,37 +72,6 @@ export function useTaskForComponentQuery(component: Component) {
   });
 }
 
-export function useComponentMeasuresWithMetricsQuery(
-  key: string,
-  metricKeys: string[],
-  branchParameters: BranchParameters,
-  enabled = true,
-): UseQueryResult<MeasuresAndMetaWithMetrics> {
-  return useQuery({
-    enabled,
-    queryKey: [
-      'component',
-      key,
-      'measures',
-      'with_metrics',
-      {
-        metricKeys,
-        branchParameters,
-      },
-    ] as const,
-    queryFn: ({ queryKey: [, key, , , data] }) => {
-      return (
-        data &&
-        getMeasuresWithMetrics(
-          key,
-          data.metricKeys.filter((m) => !NEW_METRICS.includes(m as MetricKey)),
-          data.branchParameters,
-        )
-      );
-    },
-  });
-}
-
 export const useComponentQuery = createQueryHook(
   ({ component, metricKeys, ...params }: Parameters<typeof getComponent>[0]) => {
     const queryClient = useQueryClient();
index d574084311f80a61a9aaa195911ca680594f7ae1..b7274ac555ee3445d0ac5d28a7efa5616d6f283a 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
-import { queryOptions, useQuery, useQueryClient } from '@tanstack/react-query';
-import { groupBy } from 'lodash';
+import {
+  infiniteQueryOptions,
+  queryOptions,
+  useQuery,
+  useQueryClient,
+} from '@tanstack/react-query';
+import { groupBy, isUndefined, omitBy } from 'lodash';
 import { BranchParameters } from '~sonar-aligned/types/branch-like';
+import { getComponentTree } from '../api/components';
 import {
   getMeasures,
   getMeasuresForProjects,
   getMeasuresWithPeriodAndMetrics,
 } from '../api/measures';
 import { getAllTimeMachineData } from '../api/time-machine';
+import { SOFTWARE_QUALITY_RATING_METRICS } from '../helpers/constants';
+import { getNextPageParam, getPreviousPageParam } from '../helpers/react-query';
+import { getBranchLikeQuery } from '../sonar-aligned/helpers/branch-like';
 import { MetricKey } from '../sonar-aligned/types/metrics';
+import { BranchLike } from '../types/branch-like';
 import { Measure } from '../types/types';
-import { createQueryHook } from './common';
+import {createInfiniteQueryHook, createQueryHook} from './common';
 
 const NEW_METRICS = [
   MetricKey.software_quality_maintainability_rating,
@@ -63,15 +73,125 @@ export function useAllMeasuresHistoryQuery(
   });
 }
 
+export const useMeasuresComponentQuery = createQueryHook(
+  ({
+    componentKey,
+    metricKeys,
+    branchLike,
+  }: {
+    branchLike?: BranchLike;
+    componentKey: string;
+    metricKeys: string[];
+  }) => {
+    const queryClient = useQueryClient();
+    const branchLikeQuery = getBranchLikeQuery(branchLike);
+
+    return queryOptions({
+      queryKey: ['measures', 'component', componentKey, 'branchLike', branchLikeQuery, metricKeys],
+      queryFn: async () => {
+        const data = await getMeasuresWithPeriodAndMetrics(
+          componentKey,
+          metricKeys.filter((m) => !SOFTWARE_QUALITY_RATING_METRICS.includes(m as MetricKey)),
+          branchLikeQuery,
+        );
+        metricKeys.forEach((metricKey) => {
+          const measure =
+            data.component.measures?.find((measure) => measure.metric === metricKey) ?? null;
+          queryClient.setQueryData<Measure | null>(
+            ['measures', 'details', componentKey, 'branchLike', branchLikeQuery, metricKey],
+            measure,
+          );
+        });
+
+        return data;
+      },
+    });
+  },
+);
+
+export const useComponentTreeQuery = createInfiniteQueryHook(
+  ({
+    strategy,
+    component,
+    metrics,
+    additionalData,
+  }: {
+    additionalData: Parameters<typeof getComponentTree>[3];
+    component: Parameters<typeof getComponentTree>[1];
+    metrics: Parameters<typeof getComponentTree>[2];
+    strategy: 'children' | 'leaves';
+  }) => {
+    const branchLikeQuery = omitBy(
+      {
+        branch: additionalData?.branch,
+        pullRequest: additionalData?.pullRequest,
+      },
+      isUndefined,
+    );
+
+    const queryClient = useQueryClient();
+    return infiniteQueryOptions({
+      queryKey: ['component', component, 'tree', strategy, { metrics, additionalData }],
+      queryFn: async ({ pageParam }) => {
+        const result = await getComponentTree(
+          strategy,
+          component,
+          metrics?.filter((m) => !SOFTWARE_QUALITY_RATING_METRICS.includes(m as MetricKey)),
+          { ...additionalData, p: pageParam, ...branchLikeQuery },
+        );
+
+        // const measuresMapByMetricKeyForBaseComponent = groupBy(
+        //   result.baseComponent.measures,
+        //   'metric',
+        // );
+        // metrics?.forEach((metricKey) => {
+        //   const measure = measuresMapByMetricKeyForBaseComponent[metricKey]?.[0] ?? null;
+        //   queryClient.setQueryData<Measure>(
+        //     [
+        //       'measures',
+        //       'details',
+        //       result.baseComponent.key,
+        //       'branchLike',
+        //       branchLikeQuery,
+        //       metricKey,
+        //     ],
+        //     measure,
+        //   );
+        // });
+        result.components.forEach((childComponent) => {
+          const measuresMapByMetricKeyForChildComponent = groupBy(
+            childComponent.measures,
+            'metric',
+          );
+
+          metrics?.forEach((metricKey) => {
+            const measure = measuresMapByMetricKeyForChildComponent[metricKey]?.[0] ?? null;
+            queryClient.setQueryData<Measure>(
+              ['measures', 'details', childComponent.key, 'branchLike', branchLikeQuery, metricKey],
+              measure,
+            );
+          });
+        });
+        return result;
+      },
+      getNextPageParam: (data) => getNextPageParam({ page: data.paging }),
+      getPreviousPageParam: (data) => getPreviousPageParam({ page: data.paging }),
+      initialPageParam: 1,
+      staleTime: 60_000,
+    });
+  },
+);
+
 export const useMeasuresForProjectsQuery = createQueryHook(
   ({ projectKeys, metricKeys }: { metricKeys: string[]; projectKeys: string[] }) => {
     const queryClient = useQueryClient();
+
     return queryOptions({
       queryKey: ['measures', 'list', 'projects', projectKeys, metricKeys],
       queryFn: async () => {
         // TODO remove this once all metrics are supported
         const filteredMetricKeys = metricKeys.filter(
-          (metricKey) => !NEW_METRICS.includes(metricKey as MetricKey),
+          (metricKey) => !SOFTWARE_QUALITY_RATING_METRICS.includes(metricKey as MetricKey),
         );
         const measures = await getMeasuresForProjects(projectKeys, filteredMetricKeys);
         const measuresMapByProjectKey = groupBy(measures, 'component');
@@ -81,7 +201,7 @@ export const useMeasuresForProjectsQuery = createQueryHook(
           metricKeys.forEach((metricKey) => {
             const measure = measuresMapByMetricKey[metricKey]?.[0] ?? null;
             queryClient.setQueryData<Measure>(
-              ['measures', 'details', projectKey, metricKey],
+              ['measures', 'details', projectKey, 'branchLike', {}, metricKey],
               measure,
             );
           });
@@ -130,9 +250,19 @@ export const useMeasuresAndLeakQuery = createQueryHook(
 );
 
 export const useMeasureQuery = createQueryHook(
-  ({ componentKey, metricKey }: { componentKey: string; metricKey: string }) => {
+  ({
+    componentKey,
+    metricKey,
+    branchLike,
+  }: {
+    branchLike?: BranchLike;
+    componentKey: string;
+    metricKey: string;
+  }) => {
+    const branchLikeQuery = getBranchLikeQuery(branchLike);
+
     return queryOptions({
-      queryKey: ['measures', 'details', componentKey, metricKey],
+      queryKey: ['measures', 'details', componentKey, 'branchLike', branchLikeQuery, metricKey],
       queryFn: () =>
         getMeasures({ component: componentKey, metricKeys: metricKey }).then(
           (measures) => measures[0] ?? null,
index 1b57b71cb6448d45daa93d78f068a730a9e90493..c9381dc62d46602fefe85b80d2d345be4a9a0bbf 100644 (file)
@@ -26,9 +26,11 @@ import { Status } from '~sonar-aligned/types/common';
 import { MetricKey, MetricType } from '~sonar-aligned/types/metrics';
 import RatingComponent from '../../../app/components/metrics/RatingComponent';
 import RatingTooltipContent from '../../../components/measure/RatingTooltipContent';
+import { BranchLike } from '../../../types/branch-like';
 
 interface Props {
   badgeSize?: 'xs' | 'sm' | 'md';
+  branchLike?: BranchLike;
   className?: string;
   componentKey: string;
   decimals?: number;
@@ -46,6 +48,7 @@ export default function Measure({
   decimals,
   fontClassName,
   metricKey,
+  branchLike,
   metricType,
   small,
   value,
@@ -106,6 +109,7 @@ export default function Measure({
 
   const rating = (
     <RatingComponent
+      branchLike={branchLike}
       size={badgeSize ?? small ? 'sm' : 'md'}
       getLabel={getLabel}
       getTooltip={getTooltip}
index 572db715de3982190eeedeea88b0a89d0cd941e6..9a3677b6aa6ec5f411424545b5526464246ef4ee 100644 (file)
@@ -112,6 +112,7 @@ export enum MetricKey {
   new_reliability_rating_distribution = 'new_reliability_rating_distribution',
   new_software_quality_reliability_rating_distribution = 'new_software_quality_reliability_rating_distribution',
   new_reliability_remediation_effort = 'new_reliability_remediation_effort',
+  new_reliability_remediation_effort_new = 'new_reliability_remediation_effort_new',
   new_security_hotspots = 'new_security_hotspots',
   new_security_hotspots_reviewed = 'new_security_hotspots_reviewed',
   new_security_issues = 'new_security_issues',
@@ -120,6 +121,7 @@ export enum MetricKey {
   new_security_rating_distribution = 'new_security_rating_distribution',
   new_software_quality_security_rating_distribution = 'new_software_quality_security_rating_distribution',
   new_security_remediation_effort = 'new_security_remediation_effort',
+  new_security_remediation_effort_new = 'new_security_remediation_effort_new',
   new_security_review_rating = 'new_security_review_rating',
   new_software_quality_security_review_rating = 'new_software_quality_security_review_rating',
   new_security_review_rating_distribution = 'new_security_review_rating_distribution',
@@ -152,6 +154,7 @@ export enum MetricKey {
   software_quality_reliability_rating_distribution = 'software_quality_reliability_rating_distribution',
   reliability_rating_effort = 'reliability_rating_effort',
   reliability_remediation_effort = 'reliability_remediation_effort',
+  reliability_remediation_effort_new = 'reliability_remediation_effort_new',
   reopened_issues = 'reopened_issues',
   security_hotspots = 'security_hotspots',
   security_hotspots_reviewed = 'security_hotspots_reviewed',
@@ -162,6 +165,7 @@ export enum MetricKey {
   software_quality_security_rating_distribution = 'software_quality_security_rating_distribution',
   security_rating_effort = 'security_rating_effort',
   security_remediation_effort = 'security_remediation_effort',
+  security_remediation_effort_new = 'security_remediation_effort_new',
   security_review_rating = 'security_review_rating',
   software_quality_security_review_rating = 'software_quality_security_review_rating',
   security_review_rating_distribution = 'security_review_rating_distribution',