]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-23194 Project overview adopts MQR mode
authorIsmail Cherri <ismail.cherri@sonarsource.com>
Wed, 9 Oct 2024 13:20:46 +0000 (15:20 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 16 Oct 2024 20:02:59 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/overview/branches/ActivityPanel.tsx
server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx
server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.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/branches/__tests__/ActivityPanel-it.tsx
server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx
server/sonar-web/src/main/js/components/activity-graph/__tests__/utils-test.ts
server/sonar-web/src/main/js/components/activity-graph/utils.ts
server/sonar-webserver/src/main/java/org/sonar/server/platform/telemetry/TelemetryMQRModePropertyProvider.java
sonar-core/src/main/java/org/sonar/core/config/MQRModeProperties.java

index 9e6b76baf780c4176ac7e6063594ebcf69177bdf..92c9bd59815291b3864d5cc68773c61f7327c639 100644 (file)
@@ -17,7 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { BasicSeparator, Card, Spinner } from 'design-system';
+import { Spinner } from '@sonarsource/echoes-react';
+import { BasicSeparator, Card } from 'design-system';
 import * as React from 'react';
 import { MetricKey } from '~sonar-aligned/types/metrics';
 import GraphsHeader from '../../../components/activity-graph/GraphsHeader';
@@ -32,6 +33,7 @@ import ActivityLink from '../../../components/common/ActivityLink';
 import { parseDate } from '../../../helpers/dates';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { localizeMetric } from '../../../helpers/measures';
+import { useIsLegacyCCTMode } from '../../../queries/settings';
 import { BranchLike } from '../../../types/branch-like';
 import {
   Analysis as AnalysisType,
@@ -70,7 +72,8 @@ export function ActivityPanel(props: ActivityPanelProps) {
     metrics,
   } = props;
 
-  const displayedMetrics = getDisplayedHistoryMetrics(graph, []);
+  const { data: isLegacy = false } = useIsLegacyCCTMode();
+  const displayedMetrics = getDisplayedHistoryMetrics(graph, [], isLegacy);
   const series = generateSeries(measuresHistory, graph, metrics, displayedMetrics);
   const graphs = splitSeriesInGraphs(series, MAX_GRAPH_NB, MAX_SERIES_PER_GRAPH);
   let shownLeakPeriodDate;
@@ -134,7 +137,7 @@ export function ActivityPanel(props: ActivityPanelProps) {
 
         <BasicSeparator className="sw-mb-4 sw-mt-16" />
 
-        <Spinner loading={loading}>
+        <Spinner isLoading={loading}>
           {displayedAnalyses.length === 0 ? (
             <p>{translate('no_results')}</p>
           ) : (
index e7834509ce4a909644a3d8b1d49b58f0ea790da1..3a159842e5f6a7b9011cd14fdfa2f242cb5d1701 100644 (file)
@@ -45,6 +45,7 @@ import {
   useApplicationQualityGateStatus,
   useProjectQualityGateStatus,
 } from '../../../queries/quality-gates';
+import { useIsLegacyCCTMode } from '../../../queries/settings';
 import { ApplicationPeriod } from '../../../types/application';
 import { Branch, BranchLike } from '../../../types/branch-like';
 import { Analysis, GraphType, MeasureHistory } from '../../../types/project-activity';
@@ -68,6 +69,7 @@ const FROM_DATE = toISO8601WithOffsetString(new Date().setFullYear(new Date().ge
 
 export default function BranchOverview(props: Readonly<Props>) {
   const { component, branch, branchesEnabled } = props;
+  const { data: isLegacy = false } = useIsLegacyCCTMode();
   const { graph: initialGraph } = getActivityGraph(
     BRANCH_OVERVIEW_ACTIVITY_GRAPH,
     props.component.key,
@@ -281,7 +283,7 @@ export default function BranchOverview(props: Readonly<Props>) {
   };
 
   const loadHistoryMeasures = React.useCallback(() => {
-    const graphMetrics = getHistoryMetrics(graph, []);
+    const graphMetrics = getHistoryMetrics(graph, [], isLegacy);
     const metrics = uniq([...HISTORY_METRICS_LIST, ...graphMetrics]);
 
     return getAllTimeMachineData({
@@ -375,11 +377,10 @@ export default function BranchOverview(props: Readonly<Props>) {
   }, [branch, loadHistory, loadStatus]);
 
   const projectIsEmpty =
-    loadingStatus === false &&
-    (measures === undefined ||
-      measures.find((measure) =>
-        ([MetricKey.lines, MetricKey.new_lines] as string[]).includes(measure.metric.key),
-      ) === undefined);
+    !loadingStatus &&
+    measures?.find((measure) =>
+      ([MetricKey.lines, MetricKey.new_lines] as string[]).includes(measure.metric.key),
+    ) === undefined;
 
   return (
     <BranchOverviewRenderer
index 6333d82a4ff3aae4351dc9d635f37261a00595c2..e6cd122544ff2daf2fa0aa6aea03627af2a3b8a9 100644 (file)
@@ -24,6 +24,7 @@ import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
 import { useLocation, useRouter } from '~sonar-aligned/components/hoc/withRouter';
 import { isPortfolioLike } from '~sonar-aligned/helpers/component';
 import { ComponentQualifier } from '~sonar-aligned/types/component';
+import { MetricKey } from '~sonar-aligned/types/metrics';
 import { useAvailableFeatures } from '../../../app/components/available-features/withAvailableFeatures';
 import { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext';
 import AnalysisMissingInfoMessage from '../../../components/shared/AnalysisMissingInfoMessage';
@@ -37,7 +38,6 @@ import {
 import { CodeScope } from '../../../helpers/urls';
 import { useProjectAiCodeAssuredQuery } from '../../../queries/ai-code-assurance';
 import { useDismissNoticeMutation } from '../../../queries/users';
-import { MetricKey } from '../../../sonar-aligned/types/metrics';
 import { ApplicationPeriod } from '../../../types/application';
 import { Branch } from '../../../types/branch-like';
 import { isProject } from '../../../types/component';
@@ -83,7 +83,7 @@ export interface BranchOverviewRendererProps {
   qualityGate?: QualityGate;
 }
 
-export default function BranchOverviewRenderer(props: BranchOverviewRendererProps) {
+export default function BranchOverviewRenderer(props: Readonly<BranchOverviewRendererProps>) {
   const {
     analyses,
     appLeak,
index 0f4f9c97e8010f9852e7ac69aacedf33e32e4fd0..3d381c9a414b5765c437e941795514a89329d693 100644 (file)
@@ -18,8 +18,8 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import styled from '@emotion/styled';
-import { LinkHighlight, LinkStandalone, Tooltip } from '@sonarsource/echoes-react';
-import { Badge, TextBold, TextSubdued, themeColor } from 'design-system';
+import { LinkHighlight, LinkStandalone, Text, Tooltip } from '@sonarsource/echoes-react';
+import { Badge, themeColor } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage, useIntl } from 'react-intl';
 import { formatMeasure } from '~sonar-aligned/helpers/measures';
@@ -87,7 +87,8 @@ export function SoftwareImpactMeasureCard(props: Readonly<SoftwareImpactBreakdow
     >
       <div className="sw-flex sw-items-center">
         <ColorBold className="sw-typo-semibold">
-          {intl.formatMessage({ id: `software_quality.${softwareQuality}` })}
+          {!isLegacy && intl.formatMessage({ id: `software_quality.${softwareQuality}` })}
+          {alternativeMeasure && isLegacy && alternativeMeasure.metric.name}
         </ColorBold>
         {failed && (
           <Badge className="sw-h-fit sw-ml-2" variant="deleted">
@@ -99,7 +100,7 @@ export function SoftwareImpactMeasureCard(props: Readonly<SoftwareImpactBreakdow
         <div className="sw-flex sw-mt-4">
           <div className="sw-flex sw-gap-1 sw-items-center">
             {count ? (
-              <Tooltip content={countTooltipOverlay}>
+              <Tooltip content={countTooltipOverlay} isOpen={isLegacy ? false : undefined}>
                 <LinkStandalone
                   data-testid={`overview__software-impact-${softwareQuality}`}
                   aria-label={intl.formatMessage(
@@ -121,11 +122,11 @@ export function SoftwareImpactMeasureCard(props: Readonly<SoftwareImpactBreakdow
                 </LinkStandalone>
               </Tooltip>
             ) : (
-              <StyledDash className="sw-font-bold" name="-" />
+              <StyledDash isHighlighted>-</StyledDash>
             )}
-            <TextSubdued className="sw-self-end sw-typo-default sw-pb-1">
+            <Text isSubdued className="sw-self-end sw-typo-default sw-pb-1">
               {intl.formatMessage({ id: 'overview.measures.software_impact.total_open_issues' })}
-            </TextSubdued>
+            </Text>
           </div>
 
           <div className="sw-flex-grow sw-flex sw-justify-end">
@@ -142,7 +143,7 @@ export function SoftwareImpactMeasureCard(props: Readonly<SoftwareImpactBreakdow
   );
 }
 
-const StyledDash = styled(TextBold)`
+const StyledDash = styled(Text)`
   font-size: 36px;
 `;
 const ColorBold = styled.h2`
index b5eeb3d48af7d176128c651c4d9022319d18024f..c58ff188d1173762a6d910c12cc210c559ebf906 100644 (file)
@@ -21,8 +21,10 @@ import { RatingEnum } from 'design-system/lib';
 import * as React from 'react';
 import { useCallback } from 'react';
 import { useIntl } from 'react-intl';
+import { MetricKey } from '~sonar-aligned/types/metrics';
 import RatingComponent from '../../../app/components/metrics/RatingComponent';
-import { MetricKey } from '../../../sonar-aligned/types/metrics';
+import RatingTooltipContent from '../../../components/measure/RatingTooltipContent';
+import { useIsLegacyCCTMode } from '../../../queries/settings';
 import { Branch } from '../../../types/branch-like';
 import { SoftwareImpactSeverity, SoftwareQuality } from '../../../types/clean-code-taxonomy';
 
@@ -35,15 +37,20 @@ export interface SoftwareImpactMeasureRatingProps {
 
 export function SoftwareImpactMeasureRating(props: Readonly<SoftwareImpactMeasureRatingProps>) {
   const { ratingMetricKey, componentKey, softwareQuality, branch } = props;
+  const { data: isLegacy = false } = useIsLegacyCCTMode();
 
   const intl = useIntl();
 
   const getSoftwareImpactRatingTooltip = useCallback(
-    (rating: RatingEnum) => {
+    (rating: RatingEnum, value: string | undefined) => {
       if (rating === undefined) {
         return null;
       }
 
+      if (isLegacy && value !== undefined) {
+        return <RatingTooltipContent metricKey={ratingMetricKey} value={value} />;
+      }
+
       function ratingToWorseSeverity(rating: string): SoftwareImpactSeverity {
         return (
           {
index 8b49e156574e7610c28c62b90ffb08678a7c6aa2..b250935267b9032a57c632c0c21af6c62677b966 100644 (file)
@@ -18,7 +18,9 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { screen } from '@testing-library/react';
+import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
 import * as React from 'react';
+import SettingsServiceMock from '../../../../api/mocks/SettingsServiceMock';
 import { mockComponent } from '../../../../helpers/mocks/component';
 import {
   mockAnalysis,
@@ -38,10 +40,16 @@ import {
   DefinitionChangeType,
   ProjectAnalysisEventCategory,
 } from '../../../../types/project-activity';
+import { SettingsKey } from '../../../../types/settings';
 import ActivityPanel, { ActivityPanelProps } from '../ActivityPanel';
 
-it('should render correctly', async () => {
-  const user = userEvent.setup();
+const settingsHandler = new SettingsServiceMock();
+
+afterEach(() => {
+  settingsHandler.reset();
+});
+
+async function expectGraphs(user: UserEvent) {
   renderActivityPanel();
 
   expect(await screen.findAllByText('metric.level.ERROR')).toHaveLength(2);
@@ -52,7 +60,7 @@ it('should render correctly', async () => {
   expect(screen.getByText('event.sqUpgrade10.2')).toBeInTheDocument();
 
   // Checking measures variations
-  expect(screen.getAllByText(/% project_activity\.graphs\.coverage$/)).toHaveLength(3);
+  expect(await screen.findAllByText(/% project_activity\.graphs\.coverage$/)).toHaveLength(3);
   expect(screen.getAllByText(/% project_activity\.graphs\.duplications$/)).toHaveLength(3);
   // Analysis 1 (latest)
   expect(screen.getByText(/^-5 project_activity\.graphs\.issues$/)).toBeInTheDocument();
@@ -76,6 +84,10 @@ it('should render correctly', async () => {
     ),
   ).toBeInTheDocument();
 
+  return user;
+}
+
+async function expectQualityGate(user: UserEvent) {
   await user.click(
     screen.getByRole('link', {
       name: 'quality_profiles.page_title_changelog_x.QP-test: 1 new rule, 2 modified rules, and 3 removed rules',
@@ -83,10 +95,60 @@ it('should render correctly', async () => {
   );
 
   expect(await screen.findByText('QP-test java')).toBeInTheDocument();
+}
+
+it('should render correctly', async () => {
+  const user = userEvent.setup();
+  await expectGraphs(user);
+
+  expect(screen.queryByText('metric.code_smells.name')).not.toBeInTheDocument();
+  expect(screen.queryByText('metric.vulnerabilities.name')).not.toBeInTheDocument();
+  expect(screen.queryByText('metric.bugs.name')).not.toBeInTheDocument();
+
+  await expectQualityGate(user);
+});
+
+it('should render correctly for legacy mode', async () => {
+  settingsHandler.set(SettingsKey.MQRMode, 'false');
+  const user = userEvent.setup();
+  await expectGraphs(user);
+
+  expect(screen.getByText('metric.code_smells.name')).toBeInTheDocument();
+  expect(screen.getByText('metric.vulnerabilities.name')).toBeInTheDocument();
+  expect(screen.getByText('metric.bugs.name')).toBeInTheDocument();
+
+  await expectQualityGate(user);
 });
 
 function renderActivityPanel() {
   const mockedMeasureHistory = [
+    mockMeasureHistory({
+      metric: MetricKey.vulnerabilities,
+      history: [
+        mockHistoryItem({ date: parseDate('2018-10-27T10:21:15+0200'), value: '200' }),
+        mockHistoryItem({ date: parseDate('2018-10-27T12:21:15+0200'), value: '200' }),
+        mockHistoryItem({ date: parseDate('2020-10-27T16:33:50+0200'), value: '100' }),
+        mockHistoryItem({ date: parseDate('2020-10-27T18:33:50+0200'), value: '95' }),
+      ],
+    }),
+    mockMeasureHistory({
+      metric: MetricKey.code_smells,
+      history: [
+        mockHistoryItem({ date: parseDate('2018-10-27T10:21:15+0200'), value: '200' }),
+        mockHistoryItem({ date: parseDate('2018-10-27T12:21:15+0200'), value: '200' }),
+        mockHistoryItem({ date: parseDate('2020-10-27T16:33:50+0200'), value: '100' }),
+        mockHistoryItem({ date: parseDate('2020-10-27T18:33:50+0200'), value: '95' }),
+      ],
+    }),
+    mockMeasureHistory({
+      metric: MetricKey.bugs,
+      history: [
+        mockHistoryItem({ date: parseDate('2018-10-27T10:21:15+0200'), value: '200' }),
+        mockHistoryItem({ date: parseDate('2018-10-27T12:21:15+0200'), value: '200' }),
+        mockHistoryItem({ date: parseDate('2020-10-27T16:33:50+0200'), value: '100' }),
+        mockHistoryItem({ date: parseDate('2020-10-27T18:33:50+0200'), value: '95' }),
+      ],
+    }),
     mockMeasureHistory({
       metric: MetricKey.violations,
       history: [
index 78d22c7c9eccf6407bef2fbf6c00bf6627cd75b6..35d1150b9cddbf4af354bf626d51b730b67261e4 100644 (file)
@@ -37,7 +37,7 @@ interface Props {
   selectedMetrics?: string[];
 }
 
-export default function GraphsHeader(props: Props) {
+export default function GraphsHeader(props: Readonly<Props>) {
   const {
     className,
     graph,
index 4cfdf423674c0b67c208bee76f041473e3a14bab..a289f2bae52a8c81a5557243e4346970946d4ed5 100644 (file)
@@ -105,6 +105,13 @@ describe('getDisplayedHistoryMetrics', () => {
       customMetrics,
     );
   });
+  it('should return Legacy graphs', () => {
+    expect(utils.getDisplayedHistoryMetrics(GraphType.issues, [], true)).toEqual([
+      MetricKey.bugs,
+      MetricKey.code_smells,
+      MetricKey.vulnerabilities,
+    ]);
+  });
 });
 
 describe('getHistoryMetrics', () => {
@@ -123,6 +130,16 @@ describe('getHistoryMetrics', () => {
     ]);
     expect(utils.getHistoryMetrics(GraphType.custom, customMetrics)).toEqual(customMetrics);
   });
+  it('should return legacy metrics', () => {
+    expect(utils.getHistoryMetrics(utils.DEFAULT_GRAPH, [], true)).toEqual([
+      MetricKey.bugs,
+      MetricKey.code_smells,
+      MetricKey.vulnerabilities,
+      MetricKey.reliability_rating,
+      MetricKey.security_rating,
+      MetricKey.sqale_rating,
+    ]);
+  });
 });
 
 describe('hasHistoryData', () => {
index 54dbc17e42c1696964233d56015b5a0ba4bf870d..b1768446382477e268fa05892c4b58e68c044640 100644 (file)
@@ -37,6 +37,11 @@ const GRAPHS_METRICS_DISPLAYED: Dict<string[]> = {
   [GraphType.duplications]: [MetricKey.ncloc, MetricKey.duplicated_lines],
 };
 
+const LEGACY_GRAPHS_METRICS_DISPLAYED: Dict<string[]> = {
+  ...GRAPHS_METRICS_DISPLAYED,
+  [GraphType.issues]: [MetricKey.bugs, MetricKey.code_smells, MetricKey.vulnerabilities],
+};
+
 const GRAPHS_METRICS: Dict<string[]> = {
   [GraphType.issues]: GRAPHS_METRICS_DISPLAYED[GraphType.issues].concat([
     MetricKey.reliability_rating,
@@ -50,6 +55,15 @@ const GRAPHS_METRICS: Dict<string[]> = {
   ],
 };
 
+const LEGACY_GRAPHS_METRICS: Dict<string[]> = {
+  ...GRAPHS_METRICS,
+  [GraphType.issues]: LEGACY_GRAPHS_METRICS_DISPLAYED[GraphType.issues].concat([
+    MetricKey.reliability_rating,
+    MetricKey.security_rating,
+    MetricKey.sqale_rating,
+  ]),
+};
+
 export const LINE_CHART_DASHES = [0, 3, 7];
 
 export function isCustomGraph(graph: GraphType) {
@@ -74,12 +88,23 @@ export function getSeriesMetricType(series: Serie[]) {
   return series.length > 0 ? series[0].type : MetricType.Integer;
 }
 
-export function getDisplayedHistoryMetrics(graph: GraphType, customMetrics: string[]) {
-  return isCustomGraph(graph) ? customMetrics : GRAPHS_METRICS_DISPLAYED[graph];
+export function getDisplayedHistoryMetrics(
+  graph: GraphType,
+  customMetrics: string[],
+  isLegacy = false,
+) {
+  if (isCustomGraph(graph)) {
+    return customMetrics;
+  }
+
+  return isLegacy ? LEGACY_GRAPHS_METRICS_DISPLAYED[graph] : GRAPHS_METRICS_DISPLAYED[graph];
 }
 
-export function getHistoryMetrics(graph: GraphType, customMetrics: string[]) {
-  return isCustomGraph(graph) ? customMetrics : GRAPHS_METRICS[graph];
+export function getHistoryMetrics(graph: GraphType, customMetrics: string[], isLegacy = false) {
+  if (isCustomGraph(graph)) {
+    return customMetrics;
+  }
+  return isLegacy ? LEGACY_GRAPHS_METRICS[graph] : GRAPHS_METRICS[graph];
 }
 
 export function hasHistoryDataValue(series: Serie[]) {
index ed1e6ab34fe4f84fc198d073b6d2453e54442bb3..b827a02f27858a104e7dba98175b5d8a74ab8a3b 100644 (file)
@@ -62,6 +62,6 @@ public class TelemetryMQRModePropertyProvider implements TelemetryDataProvider<B
   @Override
   public Optional<Boolean> getValue() {
     PropertyDto property = dbClient.propertiesDao().selectGlobalProperty(MULTI_QUALITY_MODE_ENABLED);
-    return Optional.of(property != null && Boolean.parseBoolean(property.getValue()));
+    return property == null ? Optional.of(true) : Optional.of(Boolean.valueOf(property.getValue()));
   }
 }
index aee71806cf811eb870a4aa4ef77acc1367398fe8..536aeae5cbb090f9371cd374bba1b07b0b6df4e2 100644 (file)
@@ -36,7 +36,7 @@ public final class MQRModeProperties {
   public static List<PropertyDefinition> all() {
     return Collections.singletonList(
       PropertyDefinition.builder(MULTI_QUALITY_MODE_ENABLED)
-        .defaultValue(Boolean.FALSE.toString())
+        .defaultValue(Boolean.TRUE.toString())
         .name("Enable Multi-Quality Rule Mode")
         .description("Aims to more accurately represent the impact software has on all software qualities. " +
                 "It does this by mapping rules to every software quality they can impact, not just the one " +