]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22717 Change in calculation badge in projects list
authorViktor Vorona <viktor.vorona@sonarsource.com>
Fri, 9 Aug 2024 17:20:24 +0000 (19:20 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 26 Aug 2024 20:03:06 +0000 (20:03 +0000)
server/sonar-web/design-system/src/components/Pill.tsx
server/sonar-web/src/main/js/app/components/ChangeInCalculationPill.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
server/sonar-web/src/main/js/apps/projects/components/project-card/ProjectCard.tsx
server/sonar-web/src/main/js/apps/projects/components/project-card/ProjectCardMeasures.tsx
server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCard-test.tsx
server/sonar-web/src/main/js/queries/settings.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 79d6858912603db3e6c0e3f045787bedb73816fc..acd24669bda7b64c103706d5fede8380a26f9ede 100644 (file)
@@ -17,8 +17,9 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { css } from '@emotion/react';
 import styled from '@emotion/styled';
-import { ReactNode } from 'react';
+import { forwardRef, ReactNode } from 'react';
 import tw from 'twin.macro';
 import { themeColor, themeContrast } from '../helpers/theme';
 import { ThemeColors } from '../types/theme';
@@ -43,34 +44,59 @@ interface PillProps {
   ['aria-label']?: string;
   children: ReactNode;
   className?: string;
+  onClick?: () => void;
   variant: PillVariant;
 }
 
-export function Pill({ children, variant, ...rest }: Readonly<PillProps>) {
-  return (
-    <StyledPill variant={variant} {...rest}>
-      {children}
-    </StyledPill>
-  );
-}
+// eslint-disable-next-line react/display-name
+export const Pill = forwardRef<HTMLButtonElement, Readonly<PillProps>>(
+  ({ children, variant, onClick, ...rest }, ref) => {
+    return onClick ? (
+      <StyledPillButton onClick={onClick} ref={ref} variant={variant} {...rest}>
+        {children}
+      </StyledPillButton>
+    ) : (
+      <StyledPill ref={ref} variant={variant} {...rest}>
+        {children}
+      </StyledPill>
+    );
+  },
+);
 
-const StyledPill = styled.span<{
-  variant: PillVariant;
-}>`
+const reusedStyles = css`
   ${tw`sw-body-xs`};
   ${tw`sw-w-fit`};
   ${tw`sw-inline-block`};
   ${tw`sw-whitespace-nowrap`};
   ${tw`sw-px-[8px] sw-py-[2px]`};
   ${tw`sw-rounded-pill`};
+  border-width: 1px;
+
+  &:empty {
+    ${tw`sw-hidden`}
+  }
+`;
+
+const StyledPill = styled.span<{
+  variant: PillVariant;
+}>`
+  ${reusedStyles};
 
   background-color: ${({ variant }) => themeColor(variantThemeColors[variant])};
   color: ${({ variant }) => themeContrast(variantThemeColors[variant])};
   border-style: ${({ variant }) => (variant === 'accent' ? 'hidden' : 'solid')};
   border-color: ${({ variant }) => themeColor(variantThemeBorderColors[variant])};
-  border-width: 1px;
+`;
 
-  &:empty {
-    ${tw`sw-hidden`}
-  }
+const StyledPillButton = styled.button<{
+  variant: PillVariant;
+}>`
+  ${reusedStyles};
+
+  background-color: ${({ variant }) => themeColor(variantThemeColors[variant])};
+  color: ${({ variant }) => themeContrast(variantThemeColors[variant])};
+  border-style: ${({ variant }) => (variant === 'accent' ? 'hidden' : 'solid')};
+  border-color: ${({ variant }) => themeColor(variantThemeBorderColors[variant])};
+
+  cursor: pointer;
 `;
diff --git a/server/sonar-web/src/main/js/app/components/ChangeInCalculationPill.tsx b/server/sonar-web/src/main/js/app/components/ChangeInCalculationPill.tsx
new file mode 100644 (file)
index 0000000..bdf31ea
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * 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 { Popover } from '@sonarsource/echoes-react';
+import { Pill } from 'design-system';
+import * as React from 'react';
+import DocumentationLink from '../../components/common/DocumentationLink';
+import { DocLink } from '../../helpers/doc-links';
+import { translate } from '../../helpers/l10n';
+import { useIsLegacyCCTMode } from '../../queries/settings';
+import { ComponentQualifier } from '../../sonar-aligned/types/component';
+
+interface Props {
+  qualifier: ComponentQualifier;
+}
+
+export default function ChangeInCalculation({ qualifier }: Readonly<Props>) {
+  const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
+  const { data: isLegacy, isLoading } = useIsLegacyCCTMode();
+
+  if (isLegacy || isLoading) {
+    return null;
+  }
+
+  return (
+    <Popover
+      isOpen={isPopoverOpen}
+      title={translate('projects.awaiting_scan.title')}
+      description={translate(`projects.awaiting_scan.description.${qualifier}`)}
+      footer={
+        <DocumentationLink to={DocLink.CleanCodeIntroduction}>
+          {translate('learn_more')}
+        </DocumentationLink>
+      }
+    >
+      <Pill variant="info" className="sw-ml-2" onClick={() => setIsPopoverOpen(!isPopoverOpen)}>
+        {translate('projects.awaiting_scan')}
+      </Pill>
+    </Popover>
+  );
+}
index 0c19a7436ee410767754bde1f7323d400c7972f3..8bfe5f83eca83e5b1621b4fd3129108c9b2b5ccc 100644 (file)
@@ -34,6 +34,7 @@ import { translateWithParameters } from '../../helpers/l10n';
 import { HttpStatus } from '../../helpers/request';
 import { getPortfolioUrl, getProjectUrl, getPullRequestUrl } from '../../helpers/urls';
 import { useBranchesQuery } from '../../queries/branch';
+import { useIsLegacyCCTMode } from '../../queries/settings';
 import { ProjectAlmBindingConfigurationErrors } from '../../types/alm-settings';
 import { Branch } from '../../types/branch-like';
 import { isFile } from '../../types/component';
@@ -72,6 +73,9 @@ function ComponentContainer({ hasFeature }: Readonly<WithAvailableFeaturesProps>
     fixedInPullRequest ? component : undefined,
   );
 
+  //prefetch isLegacyCCTMode
+  useIsLegacyCCTMode();
+
   const isInTutorials = pathname.includes('tutorials');
 
   const fetchComponent = React.useCallback(
index 8a87dad61c390415058fd94b291593509083cc11..4f31c39eeb9a17af9fef14cd42d01140fbda76ad 100644 (file)
@@ -51,6 +51,10 @@ jest.mock('../../../api/components', () => ({
     .mockResolvedValue({ component: { name: 'component name', analysisDate: '2018-07-30' } }),
 }));
 
+jest.mock('../../../queries/settings', () => ({
+  useIsLegacyCCTMode: jest.fn(),
+}));
+
 jest.mock('../../../api/navigation', () => ({
   getComponentNavigation: jest.fn().mockResolvedValue({
     breadcrumbs: [{ key: 'portfolioKey', name: 'portfolio', qualifier: 'VW' }],
index d0ff2f6e32feca28d0788faf4d258425ccf64b16..2ee4a4d6881d1fa4d50d79626529fc4fb0d2c26c 100644 (file)
@@ -42,6 +42,7 @@ import { formatMeasure } from '~sonar-aligned/helpers/measures';
 import { Status } from '~sonar-aligned/types/common';
 import { ComponentQualifier } from '~sonar-aligned/types/component';
 import { MetricKey, MetricType } from '~sonar-aligned/types/metrics';
+import ChangeInCalculation from '../../../../app/components/ChangeInCalculationPill';
 import Favorite from '../../../../components/controls/Favorite';
 import Tooltip from '../../../../components/controls/Tooltip';
 import DateFromNow from '../../../../components/intl/DateFromNow';
@@ -67,12 +68,18 @@ function renderFirstLine(
   isNewCode: boolean,
 ) {
   const { analysisDate, isFavorite, key, measures, name, qualifier, tags, visibility } = project;
+  const noSoftwareQualityMetrics = [
+    MetricKey.reliability_issues,
+    MetricKey.maintainability_issues,
+    MetricKey.security_issues,
+  ].every((key) => measures[key] === undefined);
+  const noRatingMetrics = [
+    MetricKey.reliability_rating_new,
+    MetricKey.sqale_rating_new,
+    MetricKey.security_rating_new,
+  ].every((key) => measures[key] === undefined);
   const awaitingScan =
-    [
-      MetricKey.reliability_issues,
-      MetricKey.maintainability_issues,
-      MetricKey.security_issues,
-    ].every((key) => measures[key] === undefined) &&
+    (noSoftwareQualityMetrics || noRatingMetrics) &&
     !isNewCode &&
     !isEmpty(analysisDate) &&
     measures.ncloc !== undefined;
@@ -124,13 +131,7 @@ function renderFirstLine(
           </Tooltip>
 
           {awaitingScan && !isNewCode && !isEmpty(analysisDate) && measures.ncloc !== undefined && (
-            <Tooltip content={translate(`projects.awaiting_scan.description.${qualifier}`)}>
-              <span>
-                <Badge variant="new" className="sw-ml-2">
-                  {translate('projects.awaiting_scan')}
-                </Badge>
-              </span>
-            </Tooltip>
+            <ChangeInCalculation qualifier={qualifier} />
           )}
         </div>
 
index f444465e33bf57c437384483cfe1ef16bac0f3dd..0f0b31ef7996c4dfe72b5beb441fd4159d59db00 100644 (file)
@@ -31,6 +31,7 @@ import RatingComponent from '../../../../app/components/metrics/RatingComponent'
 import { duplicationRatingConverter } from '../../../../components/measure/utils';
 import { translate } from '../../../../helpers/l10n';
 import { isDefined } from '../../../../helpers/types';
+import { useIsLegacyCCTMode } from '../../../../queries/settings';
 import { Dict } from '../../../../types/types';
 import ProjectCardMeasure from './ProjectCardMeasure';
 
@@ -115,7 +116,7 @@ function renderDuplication(props: ProjectCardMeasuresProps) {
   );
 }
 
-function renderRatings(props: ProjectCardMeasuresProps) {
+function renderRatings(props: ProjectCardMeasuresProps, isLegacy: boolean) {
   const { isNewCode, measures, componentKey } = props;
 
   const measuresByCodeLeak = isNewCode
@@ -125,27 +126,27 @@ function renderRatings(props: ProjectCardMeasuresProps) {
           iconLabel: translate(`metric.${MetricKey.security_issues}.short_name`),
           noShrink: true,
           metricKey:
-            measures[MetricKey.security_issues] !== undefined
-              ? MetricKey.security_issues
-              : MetricKey.vulnerabilities,
+            isLegacy || measures[MetricKey.security_issues] === undefined
+              ? MetricKey.vulnerabilities
+              : MetricKey.security_issues,
           metricRatingKey: MetricKey.security_rating,
           metricType: MetricType.ShortInteger,
         },
         {
           iconLabel: translate(`metric.${MetricKey.reliability_issues}.short_name`),
           metricKey:
-            measures[MetricKey.reliability_issues] !== undefined
-              ? MetricKey.reliability_issues
-              : MetricKey.bugs,
+            isLegacy || measures[MetricKey.reliability_issues] === undefined
+              ? MetricKey.bugs
+              : MetricKey.reliability_issues,
           metricRatingKey: MetricKey.reliability_rating,
           metricType: MetricType.ShortInteger,
         },
         {
           iconLabel: translate(`metric.${MetricKey.maintainability_issues}.short_name`),
           metricKey:
-            measures[MetricKey.maintainability_issues] !== undefined
-              ? MetricKey.maintainability_issues
-              : MetricKey.code_smells,
+            isLegacy || measures[MetricKey.maintainability_issues] === undefined
+              ? MetricKey.code_smells
+              : MetricKey.maintainability_issues,
           metricRatingKey: MetricKey.sqale_rating,
           metricType: MetricType.ShortInteger,
         },
@@ -195,6 +196,7 @@ function renderRatings(props: ProjectCardMeasuresProps) {
 
 export default function ProjectCardMeasures(props: ProjectCardMeasuresProps) {
   const { isNewCode, measures, componentQualifier } = props;
+  const { data: isLegacy } = useIsLegacyCCTMode();
 
   const { ncloc } = measures;
 
@@ -210,7 +212,7 @@ export default function ProjectCardMeasures(props: ProjectCardMeasuresProps) {
 
   const measureList = [
     renderNewIssues(props),
-    ...renderRatings(props),
+    ...renderRatings(props, !!isLegacy),
     renderCoverage(props),
     renderDuplication(props),
   ].filter(isDefined);
index 3e22f1b4f3524c5980783473d694296d74400cc1..3fb06f240d5d128e2f9b110dab3bb8d339a818f6 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 { screen } from '@testing-library/react';
+import { screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
 import React from 'react';
 import { ComponentQualifier, Visibility } from '~sonar-aligned/types/component';
 import { MetricKey } from '~sonar-aligned/types/metrics';
 import { MeasuresServiceMock } from '../../../../../api/mocks/MeasuresServiceMock';
 import SettingsServiceMock from '../../../../../api/mocks/SettingsServiceMock';
-import { mockCurrentUser, mockLoggedInUser } from '../../../../../helpers/testMocks';
+import { mockComponent } from '../../../../../helpers/mocks/component';
+import { mockCurrentUser, mockLoggedInUser, mockMeasure } from '../../../../../helpers/testMocks';
 import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
 import { CurrentUser } from '../../../../../types/users';
 import { Project } from '../../../types';
@@ -35,6 +37,7 @@ const MEASURES = {
   [MetricKey.reliability_rating]: '1.0',
   [MetricKey.security_rating]: '1.0',
   [MetricKey.sqale_rating]: '1.0',
+  [MetricKey.security_review_rating]: '1.0',
   [MetricKey.new_bugs]: '12',
 };
 
@@ -94,97 +97,247 @@ it('should display applications', () => {
   expect(screen.getAllByText('qualifier.APP')).toHaveLength(2);
 });
 
-it('should not display awaiting analysis badge and do not display old measures', () => {
+it('should display 3 projects', () => {
   renderProjectCard({
     ...PROJECT,
-    measures: {
-      ...MEASURES,
-      [MetricKey.security_issues]: JSON.stringify({ LOW: 0, MEDIUM: 0, HIGH: 1, total: 1 }),
-      [MetricKey.reliability_issues]: JSON.stringify({ LOW: 0, MEDIUM: 2, HIGH: 0, total: 2 }),
-      [MetricKey.maintainability_issues]: JSON.stringify({ LOW: 3, MEDIUM: 0, HIGH: 0, total: 3 }),
-      [MetricKey.code_smells]: '4',
-      [MetricKey.bugs]: '5',
-      [MetricKey.vulnerabilities]: '6',
-    },
+    qualifier: ComponentQualifier.Application,
+    measures: { ...MEASURES, projects: '3' },
   });
-  expect(screen.queryByText('projects.awaiting_scan')).not.toBeInTheDocument();
-  expect(screen.getByText('1')).toBeInTheDocument();
-  expect(screen.getByText('2')).toBeInTheDocument();
-  expect(screen.getByText('3')).toBeInTheDocument();
-  expect(screen.queryByText('4')).not.toBeInTheDocument();
-  expect(screen.queryByText('5')).not.toBeInTheDocument();
-  expect(screen.queryByText('6')).not.toBeInTheDocument();
+  expect(screen.getByText(/x_projects_.3/)).toBeInTheDocument();
 });
 
-it('should display awaiting analysis badge and show the old measures', async () => {
-  renderProjectCard({
-    ...PROJECT,
-    measures: {
-      ...MEASURES,
-      [MetricKey.code_smells]: '4',
-      [MetricKey.bugs]: '5',
-      [MetricKey.vulnerabilities]: '6',
-    },
+describe('upgrade scenario (awaiting scan)', () => {
+  const oldRatings = {
+    [MetricKey.reliability_rating]: mockMeasure({
+      metric: MetricKey.reliability_rating,
+      value: '1',
+    }),
+    [MetricKey.sqale_rating]: mockMeasure({
+      metric: MetricKey.sqale_rating,
+      value: '1',
+    }),
+    [MetricKey.security_rating]: mockMeasure({
+      metric: MetricKey.security_rating,
+      value: '1',
+    }),
+    [MetricKey.security_review_rating]: mockMeasure({
+      metric: MetricKey.security_review_rating,
+      value: '1',
+    }),
+  };
+
+  const newRatings = {
+    [MetricKey.reliability_rating_new]: mockMeasure({
+      metric: MetricKey.reliability_rating_new,
+      value: '2',
+    }),
+    [MetricKey.sqale_rating_new]: mockMeasure({
+      metric: MetricKey.sqale_rating_new,
+      value: '2',
+    }),
+    [MetricKey.security_rating_new]: mockMeasure({
+      metric: MetricKey.security_rating_new,
+      value: '2',
+    }),
+    [MetricKey.security_review_rating_new]: mockMeasure({
+      metric: MetricKey.security_review_rating_new,
+      value: '2',
+    }),
+  };
+  beforeEach(() => {
+    measuresHandler.setComponents({
+      component: mockComponent({ key: PROJECT.key }),
+      ancestors: [],
+      children: [],
+    });
+    measuresHandler.registerComponentMeasures({
+      [PROJECT.key]: oldRatings,
+    });
+  });
+  it('should not display awaiting analysis badge and do not display old measures', async () => {
+    measuresHandler.registerComponentMeasures({
+      [PROJECT.key]: newRatings,
+    });
+    renderProjectCard({
+      ...PROJECT,
+      measures: {
+        ...MEASURES,
+        [MetricKey.security_issues]: JSON.stringify({ LOW: 0, MEDIUM: 0, HIGH: 1, total: 1 }),
+        [MetricKey.reliability_issues]: JSON.stringify({ LOW: 0, MEDIUM: 2, HIGH: 0, total: 2 }),
+        [MetricKey.maintainability_issues]: JSON.stringify({
+          LOW: 3,
+          MEDIUM: 0,
+          HIGH: 0,
+          total: 3,
+        }),
+        [MetricKey.sqale_rating_new]: '2',
+        [MetricKey.reliability_rating_new]: '2',
+        [MetricKey.security_rating_new]: '2',
+        [MetricKey.security_review_rating_new]: '2',
+        [MetricKey.code_smells]: '4',
+        [MetricKey.bugs]: '5',
+        [MetricKey.vulnerabilities]: '6',
+      },
+    });
+    expect(screen.getByText('1')).toBeInTheDocument();
+    expect(screen.getByText('2')).toBeInTheDocument();
+    expect(screen.getByText('3')).toBeInTheDocument();
+    await waitFor(() => expect(screen.getAllByText('B')).toHaveLength(4));
+    expect(screen.queryByText('projects.awaiting_scan')).not.toBeInTheDocument();
+    expect(screen.queryByText('4')).not.toBeInTheDocument();
+    expect(screen.queryByText('5')).not.toBeInTheDocument();
+    expect(screen.queryByText('6')).not.toBeInTheDocument();
+    expect(screen.queryByText('A')).not.toBeInTheDocument();
   });
-  expect(screen.getByText('projects.awaiting_scan')).toBeInTheDocument();
-  await expect(screen.getByText('projects.awaiting_scan')).toHaveATooltipWithContent(
-    'projects.awaiting_scan.description.TRK',
-  );
-  expect(screen.getByText('4')).toBeInTheDocument();
-  expect(screen.getByText('5')).toBeInTheDocument();
-  expect(screen.getByText('6')).toBeInTheDocument();
-});
 
-it('should display awaiting analysis badge and show the old measures for Application', async () => {
-  renderProjectCard({
-    ...PROJECT,
-    qualifier: ComponentQualifier.Application,
-    measures: {
-      ...MEASURES,
-      [MetricKey.code_smells]: '4',
-      [MetricKey.bugs]: '5',
-      [MetricKey.vulnerabilities]: '6',
-    },
+  it('should display awaiting analysis badge and show the old measures', async () => {
+    const user = userEvent.setup();
+    renderProjectCard({
+      ...PROJECT,
+      measures: {
+        ...MEASURES,
+        [MetricKey.code_smells]: '4',
+        [MetricKey.bugs]: '5',
+        [MetricKey.vulnerabilities]: '6',
+      },
+    });
+    expect(await screen.findByText('projects.awaiting_scan')).toBeInTheDocument();
+    await user.click(screen.getByText('projects.awaiting_scan'));
+    await expect(screen.getByText('projects.awaiting_scan.description.TRK')).toBeInTheDocument();
+    expect(screen.getByText('4')).toBeInTheDocument();
+    expect(screen.getByText('5')).toBeInTheDocument();
+    expect(screen.getByText('6')).toBeInTheDocument();
+    expect(screen.getAllByText('A')).toHaveLength(4);
   });
-  expect(screen.getByText('projects.awaiting_scan')).toBeInTheDocument();
-  await expect(screen.getByText('projects.awaiting_scan')).toHaveATooltipWithContent(
-    'projects.awaiting_scan.description.APP',
-  );
-  expect(screen.getByText('4')).toBeInTheDocument();
-  expect(screen.getByText('5')).toBeInTheDocument();
-  expect(screen.getByText('6')).toBeInTheDocument();
-});
 
-it('should not display awaiting analysis badge if project is not analyzed', () => {
-  renderProjectCard({
-    ...PROJECT,
-    analysisDate: undefined,
+  it('should display awaiting analysis badge, show new software qualities, but old ratings', async () => {
+    renderProjectCard({
+      ...PROJECT,
+      measures: {
+        ...MEASURES,
+        [MetricKey.security_issues]: JSON.stringify({ LOW: 0, MEDIUM: 0, HIGH: 1, total: 1 }),
+        [MetricKey.reliability_issues]: JSON.stringify({ LOW: 0, MEDIUM: 2, HIGH: 0, total: 2 }),
+        [MetricKey.maintainability_issues]: JSON.stringify({
+          LOW: 3,
+          MEDIUM: 0,
+          HIGH: 0,
+          total: 3,
+        }),
+        [MetricKey.code_smells]: '4',
+        [MetricKey.bugs]: '5',
+        [MetricKey.vulnerabilities]: '6',
+      },
+    });
+    expect(await screen.findByText('projects.awaiting_scan')).toBeInTheDocument();
+    expect(screen.getByText('1')).toBeInTheDocument();
+    expect(screen.getByText('2')).toBeInTheDocument();
+    expect(screen.getByText('3')).toBeInTheDocument();
+    expect(screen.queryByText('4')).not.toBeInTheDocument();
+    expect(screen.queryByText('5')).not.toBeInTheDocument();
+    expect(screen.queryByText('6')).not.toBeInTheDocument();
+    await waitFor(() => expect(screen.getAllByText('A')).toHaveLength(4));
   });
-  expect(screen.queryByText('projects.awaiting_scan')).not.toBeInTheDocument();
-});
 
-it('should not display awaiting analysis badge if project does not have lines of code', () => {
-  renderProjectCard({
-    ...PROJECT,
-    measures: {
-      ...(({ [MetricKey.ncloc]: _, ...rest }) => rest)(MEASURES),
-    },
+  it('should display awaiting analysis badge and show the old measures for Application', async () => {
+    const user = userEvent.setup();
+    renderProjectCard({
+      ...PROJECT,
+      qualifier: ComponentQualifier.Application,
+      measures: {
+        ...MEASURES,
+        [MetricKey.code_smells]: '4',
+        [MetricKey.bugs]: '5',
+        [MetricKey.vulnerabilities]: '6',
+      },
+    });
+    expect(await screen.findByText('projects.awaiting_scan')).toBeInTheDocument();
+    await user.click(screen.getByText('projects.awaiting_scan'));
+    await expect(screen.getByText('projects.awaiting_scan.description.APP')).toBeInTheDocument();
+    expect(screen.getByText('4')).toBeInTheDocument();
+    expect(screen.getByText('5')).toBeInTheDocument();
+    expect(screen.getByText('6')).toBeInTheDocument();
   });
-  expect(screen.queryByText('projects.awaiting_scan')).not.toBeInTheDocument();
-});
 
-it('should not display awaiting analysis badge if it is a new code filter', () => {
-  renderProjectCard(PROJECT, undefined, 'leak');
-  expect(screen.queryByText('projects.awaiting_scan')).not.toBeInTheDocument();
-});
+  it('should not display awaiting analysis badge if project is not analyzed', () => {
+    renderProjectCard({
+      ...PROJECT,
+      analysisDate: undefined,
+    });
+    expect(screen.queryByText('projects.awaiting_scan')).not.toBeInTheDocument();
+  });
 
-it('should display 3 aplication', () => {
-  renderProjectCard({
-    ...PROJECT,
-    qualifier: ComponentQualifier.Application,
-    measures: { ...MEASURES, projects: '3' },
+  it('should not display awaiting analysis badge if project does not have lines of code', () => {
+    renderProjectCard({
+      ...PROJECT,
+      measures: {
+        ...(({ [MetricKey.ncloc]: _, ...rest }) => rest)(MEASURES),
+      },
+    });
+    expect(screen.queryByText('projects.awaiting_scan')).not.toBeInTheDocument();
+  });
+
+  it('should not display awaiting analysis badge if it is a new code filter', () => {
+    renderProjectCard(PROJECT, undefined, 'leak');
+    expect(screen.queryByText('projects.awaiting_scan')).not.toBeInTheDocument();
+  });
+
+  it('should not display awaiting analysis badge if legacy mode is enabled', async () => {
+    settingsHandler.set('sonar.legacy.ratings.mode.enabled', 'true');
+    renderProjectCard({
+      ...PROJECT,
+      measures: {
+        ...MEASURES,
+        [MetricKey.code_smells]: '4',
+        [MetricKey.bugs]: '5',
+        [MetricKey.vulnerabilities]: '6',
+      },
+    });
+    expect(screen.getByText('4')).toBeInTheDocument();
+    expect(screen.getByText('5')).toBeInTheDocument();
+    expect(screen.getByText('6')).toBeInTheDocument();
+    await waitFor(() => expect(screen.getAllByText('A')).toHaveLength(4));
+    expect(screen.queryByText('projects.awaiting_scan')).not.toBeInTheDocument();
+  });
+
+  it('should not display new values if legacy mode is enabled', async () => {
+    settingsHandler.set('sonar.legacy.ratings.mode.enabled', 'true');
+    measuresHandler.registerComponentMeasures({
+      [PROJECT.key]: {
+        ...newRatings,
+        ...oldRatings,
+      },
+    });
+    renderProjectCard({
+      ...PROJECT,
+      measures: {
+        ...MEASURES,
+        [MetricKey.security_issues]: JSON.stringify({ LOW: 0, MEDIUM: 0, HIGH: 1, total: 1 }),
+        [MetricKey.reliability_issues]: JSON.stringify({ LOW: 0, MEDIUM: 2, HIGH: 0, total: 2 }),
+        [MetricKey.maintainability_issues]: JSON.stringify({
+          LOW: 3,
+          MEDIUM: 0,
+          HIGH: 0,
+          total: 3,
+        }),
+        [MetricKey.sqale_rating_new]: '2',
+        [MetricKey.reliability_rating_new]: '2',
+        [MetricKey.security_rating_new]: '2',
+        [MetricKey.security_review_rating_new]: '2',
+        [MetricKey.code_smells]: '4',
+        [MetricKey.bugs]: '5',
+        [MetricKey.vulnerabilities]: '6',
+      },
+    });
+    expect(await screen.findByText('4')).toBeInTheDocument();
+    expect(screen.getByText('5')).toBeInTheDocument();
+    expect(screen.getByText('6')).toBeInTheDocument();
+    expect(screen.queryByText('1')).not.toBeInTheDocument();
+    expect(screen.queryByText('2')).not.toBeInTheDocument();
+    expect(screen.queryByText('3')).not.toBeInTheDocument();
+    await waitFor(() => expect(screen.getAllByText('A')).toHaveLength(4));
+    expect(screen.queryByText('B')).not.toBeInTheDocument();
+    expect(screen.queryByText('projects.awaiting_scan')).not.toBeInTheDocument();
   });
-  expect(screen.getByText(/x_projects_.3/)).toBeInTheDocument();
 });
 
 function renderProjectCard(project: Project, user: CurrentUser = USER_LOGGED_OUT, type?: string) {
index 2afb4624194081614faf9fb78520a603a9802708..f6534ee40b80e042f4f9a557264f286872f4196a 100644 (file)
@@ -49,7 +49,7 @@ export const useGetValueQuery = createQueryHook(
 export const useIsLegacyCCTMode = () => {
   return useGetValueQuery(
     { key: 'sonar.legacy.ratings.mode.enabled' },
-    { staleTime: Infinity, select: (data) => !!data },
+    { staleTime: Infinity, select: (data) => data?.value === 'true' },
   );
 };
 
index 84d4aaab2670304f92fc9c8dd1dea806ff1a2a96..feb5feb18eb6d377c2fc85b4261996e5aa3d489a 100644 (file)
@@ -1357,8 +1357,10 @@ projects.sort.-size=by size (biggest first)
 projects.show_more=Show more projects
 projects.security_hotspots_reviewed=Hotspots Reviewed
 projects.awaiting_scan=Change in Calculation
-projects.awaiting_scan.description.TRK=The way Security, Reliability, and Maintainability counts are calculated has changed. The values currently displayed may change after the next analysis.
-projects.awaiting_scan.description.APP=The way Security, Reliability, and Maintainability counts are calculated has changed. The values currently displayed may change after all projects in this application have been analyzed.
+projects.awaiting_scan.title=Values will change after the next analysis
+projects.awaiting_scan.description.TRK=The way in which security, reliability, maintainability, and security review counts and ratings are calculated has changed. The values currently displayed will change after the next analysis.
+projects.awaiting_scan.description.APP=The way in which security, reliability, maintainability, and security review counts and ratings are calculated has changed. The values currently displayed will change after all projects in this application have been analyzed.
+projects.awaiting_scan.description.VW=The way in which security, reliability, maintainability, and security review counts and ratings are calculated has changed. The values currently displayed will change after all projects in this portfolio have been analyzed.
 
 
 #------------------------------------------------------------------------------