]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22710 Projects facet
authorViktor Vorona <viktor.vorona@sonarsource.com>
Wed, 14 Aug 2024 12:03:11 +0000 (14:03 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 26 Aug 2024 20:03:07 +0000 (20:03 +0000)
25 files changed:
server/sonar-web/design-system/src/components/__tests__/__snapshots__/HotspotRating-test.tsx.snap
server/sonar-web/design-system/src/sonar-aligned/components/MetricsRatingBadge.tsx
server/sonar-web/design-system/src/theme/light.ts
server/sonar-web/src/main/js/app/components/metrics/RatingComponent.tsx
server/sonar-web/src/main/js/apps/projects/__tests__/utils-test.ts
server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.tsx
server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCard-test.tsx
server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/filters/NewMaintainabilityFilter.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/filters/NewReliabilityFilter.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/filters/NewSecurityFilter.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/filters/RangeFacetBase.tsx
server/sonar-web/src/main/js/apps/projects/filters/RatingFacet.tsx
server/sonar-web/src/main/js/apps/projects/filters/RatingFilter.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/filters/ReliabilityFilter.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/filters/SecurityFilter.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/filters/SecurityReviewFilter.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/query.ts
server/sonar-web/src/main/js/apps/projects/utils.ts
server/sonar-web/src/main/js/queries/settings.ts
server/sonar-web/src/main/js/types/settings.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 741beaababd34821f5df22d5598b9ea106b68686..900f1f226d9777325fde376c00a975addf0b5587 100644 (file)
@@ -74,7 +74,7 @@ exports[`should render HotspotRating with MEDIUM rating 1`] = `
   <circle
     cx="8"
     cy="8"
-    fill="rgb(255,214,175)"
+    fill="rgb(254,205,202)"
     r="7"
   />
   <path
index bf1555b1b939b56af4b6373da010260ee4386998..9276c9b286aaa2ac03a3973fa92c3d839dfee55b 100644 (file)
@@ -26,6 +26,7 @@ import { RatingLabel } from '../types/measures';
 type sizeType = keyof typeof SIZE_MAPPING;
 interface Props extends React.AriaAttributes {
   className?: string;
+  isLegacy?: boolean;
   label?: string;
   rating?: RatingLabel;
   size?: sizeType;
@@ -40,7 +41,10 @@ const SIZE_MAPPING = {
 };
 
 export const MetricsRatingBadge = forwardRef<HTMLDivElement, Props>(
-  ({ className, size = 'sm', label, rating, ...ariaAttrs }: Readonly<Props>, ref) => {
+  (
+    { className, size = 'sm', isLegacy = true, label, rating, ...ariaAttrs }: Readonly<Props>,
+    ref,
+  ) => {
     if (!rating) {
       return (
         <StyledNoRatingBadge
@@ -58,6 +62,7 @@ export const MetricsRatingBadge = forwardRef<HTMLDivElement, Props>(
       <MetricsRatingBadgeStyled
         aria-label={label}
         className={className}
+        isLegacy={isLegacy}
         rating={rating}
         ref={ref}
         size={SIZE_MAPPING[size]}
@@ -91,12 +96,17 @@ const getFontSize = (size: string) => {
   }
 };
 
-const MetricsRatingBadgeStyled = styled.div<{ rating: RatingLabel; size: string }>`
+const MetricsRatingBadgeStyled = styled.div<{
+  isLegacy: boolean;
+  rating: RatingLabel;
+  size: string;
+}>`
   width: ${getProp('size')};
   height: ${getProp('size')};
   color: ${({ rating }) => themeContrast(`rating.${rating}`)};
   font-size: ${({ size }) => getFontSize(size)};
-  background-color: ${({ rating }) => themeColor(`rating.${rating}`)};
+  background-color: ${({ rating, isLegacy }) =>
+    themeColor(`rating.${isLegacy ? 'legacy.' : ''}${rating}`)};
   user-select: none;
 
   display: inline-flex;
index 098ca5ce763d8d46dbd7e85b40844be9194ddede..dce350b51b6b1f96edde23f39e0f39e5e11e95a2 100644 (file)
@@ -464,11 +464,18 @@ export const lightTheme = {
     // size indicators
     sizeIndicator: COLORS.blue[500],
 
+    // rating colors
+    'rating.legacy.A': COLORS.green[200],
+    'rating.legacy.B': COLORS.yellowGreen[200],
+    'rating.legacy.C': COLORS.yellow[200],
+    'rating.legacy.D': COLORS.orange[200],
+    'rating.legacy.E': COLORS.red[200],
+
     // rating colors
     'rating.A': COLORS.green[200],
     'rating.B': COLORS.yellowGreen[200],
     'rating.C': COLORS.yellow[200],
-    'rating.D': COLORS.orange[200],
+    'rating.D': COLORS.red[200],
     'rating.E': COLORS.red[200],
 
     // rating donut outside circle indicators
index d5e2a61644a8a1fd4e7323a3482e17da68033736..03de2c241b6805d5e6e21dc5ab2df7d91f1cde52 100644 (file)
@@ -29,6 +29,8 @@ import { useMeasureQuery } from '../../../queries/measures';
 import { useIsLegacyCCTMode } from '../../../queries/settings';
 import { BranchLike } from '../../../types/branch-like';
 
+type SizeType = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+
 interface Props {
   branchLike?: BranchLike;
   className?: string;
@@ -36,7 +38,7 @@ interface Props {
   getLabel?: (rating: RatingEnum) => string;
   getTooltip?: (rating: RatingEnum) => React.ReactNode;
   ratingMetric: MetricKey;
-  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+  size?: SizeType;
 }
 
 type RatingMetricKeys =
@@ -88,6 +90,7 @@ export default function RatingComponent(props: Readonly<Props>) {
   const badge = (
     <MetricsRatingBadge
       label={getLabel ? getLabel(rating) : value ?? '—'}
+      isLegacy={measure?.metric ? !isNewRatingMetric(measure.metric as MetricKey) : false}
       rating={rating}
       size={size}
       className={className}
index 9728540e2e892f3122cd5452ae6a311496537a31..f245ee977fe9d4455e39357dcc3fc6415834f897 100644 (file)
@@ -84,7 +84,39 @@ describe('formatDuration', () => {
 
 describe('fetchProjects', () => {
   it('correctly converts the passed arguments to the desired query format', async () => {
-    await utils.fetchProjects({ isFavorite: true, query: {} });
+    await utils.fetchProjects({ isFavorite: true, query: {}, isLegacy: true });
+
+    expect(searchProjects).toHaveBeenCalledWith({
+      f: 'analysisDate,leakPeriodDate',
+      facets: utils.LEGACY_FACETS.join(),
+      filter: 'isFavorite',
+      p: undefined,
+      ps: 50,
+    });
+
+    await utils.fetchProjects({
+      isFavorite: false,
+      pageIndex: 3,
+      query: {
+        view: 'leak',
+        new_reliability: 6,
+        incorrect_property: 'should not appear in post data',
+        search: 'foo',
+      },
+      isLegacy: true,
+    });
+
+    expect(searchProjects).toHaveBeenCalledWith({
+      f: 'analysisDate,leakPeriodDate',
+      facets: utils.LEGACY_LEAK_FACETS.join(),
+      filter: 'new_reliability_rating = 6 and query = "foo"',
+      p: 3,
+      ps: 50,
+    });
+  });
+
+  it('correctly converts the passed arguments to the desired query format for non legacy', async () => {
+    await utils.fetchProjects({ isFavorite: true, query: {}, isLegacy: false });
 
     expect(searchProjects).toHaveBeenCalledWith({
       f: 'analysisDate,leakPeriodDate',
@@ -103,12 +135,13 @@ describe('fetchProjects', () => {
         incorrect_property: 'should not appear in post data',
         search: 'foo',
       },
+      isLegacy: false,
     });
 
     expect(searchProjects).toHaveBeenCalledWith({
       f: 'analysisDate,leakPeriodDate',
       facets: utils.LEAK_FACETS.join(),
-      filter: 'new_reliability_rating = 6 and query = "foo"',
+      filter: 'new_software_quality_reliability_rating = 6 and query = "foo"',
       p: 3,
       ps: 50,
     });
@@ -132,7 +165,7 @@ describe('fetchProjects', () => {
       paging: { total: 2 },
     });
 
-    await utils.fetchProjects({ isFavorite: true, query: {} }).then((r) => {
+    await utils.fetchProjects({ isFavorite: true, query: {}, isLegacy: true }).then((r) => {
       expect(r).toEqual({
         facets: {
           new_coverage: { NO_DATA: 0 },
@@ -166,8 +199,22 @@ describe('defineMetrics', () => {
 
 describe('convertToSorting', () => {
   it('handles asc and desc sort', () => {
-    expect(utils.convertToSorting({ sort: '-size' })).toStrictEqual({ asc: false, s: 'ncloc' });
-    expect(utils.convertToSorting({})).toStrictEqual({ s: undefined });
-    expect(utils.convertToSorting({ sort: 'search' })).toStrictEqual({ s: 'query' });
+    expect(utils.convertToSorting({ sort: '-size' }, true)).toStrictEqual({
+      asc: false,
+      s: 'ncloc',
+    });
+    expect(utils.convertToSorting({}, true)).toStrictEqual({ s: undefined });
+    expect(utils.convertToSorting({ sort: 'search' }, true)).toStrictEqual({ s: 'query' });
+  });
+
+  it('handles sort for legacy and non legacy queries', () => {
+    expect(utils.convertToSorting({ sort: '-reliability' }, true)).toStrictEqual({
+      asc: false,
+      s: 'reliability_rating',
+    });
+    expect(utils.convertToSorting({ sort: '-reliability' }, false)).toStrictEqual({
+      asc: false,
+      s: 'software_quality_reliability_rating',
+    });
   });
 });
index f7fe5e311bd4e6088ec21515b12663a6da4f7b92..b366a9ca956ef84b67458a91b88936ddf80fef41 100644 (file)
@@ -44,6 +44,7 @@ import handleRequiredAuthentication from '../../../helpers/handleRequiredAuthent
 import { translate } from '../../../helpers/l10n';
 import { get, save } from '../../../helpers/storage';
 import { isDefined } from '../../../helpers/types';
+import { useIsLegacyCCTMode } from '../../../queries/settings';
 import { AppState } from '../../../types/appstate';
 import { CurrentUser, isLoggedIn } from '../../../types/users';
 import { Query, hasFilterParams, parseUrlQuery } from '../query';
@@ -58,6 +59,7 @@ interface Props {
   appState: AppState;
   currentUser: CurrentUser;
   isFavorite: boolean;
+  isLegacy: boolean;
   location: Location;
   router: Router;
 }
@@ -104,13 +106,13 @@ export class AllProjects extends React.PureComponent<Props, State> {
   }
 
   fetchMoreProjects = () => {
-    const { isFavorite } = this.props;
+    const { isFavorite, isLegacy } = this.props;
     const { pageIndex, projects, query } = this.state;
 
     if (pageIndex && projects && Object.keys(query).length !== 0) {
       this.setState({ loading: true });
 
-      fetchProjects({ isFavorite, query, pageIndex: pageIndex + 1 }).then((response) => {
+      fetchProjects({ isFavorite, query, pageIndex: pageIndex + 1, isLegacy }).then((response) => {
         if (this.mounted) {
           this.setState({
             loading: false,
@@ -173,14 +175,14 @@ export class AllProjects extends React.PureComponent<Props, State> {
   };
 
   handleQueryChange() {
-    const { isFavorite } = this.props;
+    const { isFavorite, isLegacy } = this.props;
 
     const queryRaw = this.props.location.query;
     const query = parseUrlQuery(queryRaw);
 
     this.setState({ loading: true, query });
 
-    fetchProjects({ isFavorite, query }).then((response) => {
+    fetchProjects({ isFavorite, query, isLegacy }).then((response) => {
       // We ignore the request if the query changed since the time it was initiated
       // If that happened, another query will be initiated anyway
       if (this.mounted && queryRaw === this.props.location.query) {
@@ -202,10 +204,10 @@ export class AllProjects extends React.PureComponent<Props, State> {
   };
 
   loadSearchResultCount = (property: string, values: string[]) => {
-    const { isFavorite } = this.props;
+    const { isFavorite, isLegacy } = this.props;
     const { query = {} } = this.state;
 
-    const data = convertToQueryData({ ...query, [property]: values }, isFavorite, {
+    const data = convertToQueryData({ ...query, [property]: values }, isFavorite, isLegacy, {
       ps: 1,
       facets: property,
     });
@@ -347,9 +349,10 @@ function getStorageOptions() {
   return options;
 }
 
-function SetSearchParamsWrapper(props: Readonly<Props>) {
+function AllProjectsWrapper(props: Readonly<Omit<Props, 'isLegacy'>>) {
   const [searchParams, setSearchParams] = useSearchParams();
   const savedOptions = getStorageOptions();
+  const { data: isLegacy, isLoading } = useIsLegacyCCTMode();
 
   React.useEffect(
     () => {
@@ -366,10 +369,14 @@ function SetSearchParamsWrapper(props: Readonly<Props>) {
     ],
   );
 
-  return <AllProjects {...props} />;
+  return (
+    <Spinner isLoading={isLoading}>
+      <AllProjects {...props} isLegacy={isLegacy ?? false} />
+    </Spinner>
+  );
 }
 
-export default withRouter(withCurrentUserContext(withAppStateContext(SetSearchParamsWrapper)));
+export default withRouter(withCurrentUserContext(withAppStateContext(AllProjectsWrapper)));
 
 const StyledWrapper = styled.div`
   background-color: ${themeColor('backgroundPrimary')};
index 2aed6d9bfaee15fb1faf9a6af3e3d83a59b99fbf..3398f06718f6da19e3ff6b41236beaf85ddf7f31 100644 (file)
@@ -27,18 +27,12 @@ import { Dict } from '../../../types/types';
 import CoverageFilter from '../filters/CoverageFilter';
 import DuplicationsFilter from '../filters/DuplicationsFilter';
 import LanguagesFilter from '../filters/LanguagesFilter';
-import MaintainabilityFilter from '../filters/MaintainabilityFilter';
 import NewCoverageFilter from '../filters/NewCoverageFilter';
 import NewDuplicationsFilter from '../filters/NewDuplicationsFilter';
 import NewLinesFilter from '../filters/NewLinesFilter';
-import NewMaintainabilityFilter from '../filters/NewMaintainabilityFilter';
-import NewReliabilityFilter from '../filters/NewReliabilityFilter';
-import NewSecurityFilter from '../filters/NewSecurityFilter';
 import QualifierFacet from '../filters/QualifierFilter';
 import QualityGateFacet from '../filters/QualityGateFilter';
-import ReliabilityFilter from '../filters/ReliabilityFilter';
-import SecurityFilter from '../filters/SecurityFilter';
-import SecurityReviewFilter from '../filters/SecurityReviewFilter';
+import RatingFilter from '../filters/RatingFilter';
 import SizeFilter from '../filters/SizeFilter';
 import TagsFacet from '../filters/TagsFilter';
 import { hasFilterParams } from '../query';
@@ -107,34 +101,38 @@ export default function PageSidebar(props: PageSidebarProps) {
 
       {!isLeakView && (
         <>
-          <ReliabilityFilter
+          <RatingFilter
             {...facetProps}
-            facet={getFacet(facets, 'reliability')}
-            value={query.reliability}
+            facets={facets}
+            property="security"
+            value={query.security}
           />
 
           <BasicSeparator className="sw-my-4" />
 
-          <SecurityFilter
+          <RatingFilter
             {...facetProps}
-            facet={getFacet(facets, 'security')}
-            value={query.security}
+            facets={facets}
+            property="reliability"
+            value={query.reliability}
           />
 
           <BasicSeparator className="sw-my-4" />
 
-          <SecurityReviewFilter
+          <RatingFilter
             {...facetProps}
-            facet={getFacet(facets, 'security_review')}
-            value={query.security_review_rating}
+            facets={facets}
+            property="maintainability"
+            value={query.maintainability}
           />
 
           <BasicSeparator className="sw-my-4" />
 
-          <MaintainabilityFilter
+          <RatingFilter
             {...facetProps}
-            facet={getFacet(facets, 'maintainability')}
-            value={query.maintainability}
+            facets={facets}
+            property="security_review"
+            value={query.security_review_rating}
           />
 
           <BasicSeparator className="sw-my-4" />
@@ -160,35 +158,38 @@ export default function PageSidebar(props: PageSidebarProps) {
       )}
       {isLeakView && (
         <>
-          <NewReliabilityFilter
+          <RatingFilter
             {...facetProps}
-            facet={getFacet(facets, 'new_reliability')}
-            value={query.new_reliability}
+            facets={facets}
+            property="new_security"
+            value={query.new_security}
           />
 
           <BasicSeparator className="sw-my-4" />
 
-          <NewSecurityFilter
+          <RatingFilter
             {...facetProps}
-            facet={getFacet(facets, 'new_security')}
-            value={query.new_security}
+            facets={facets}
+            property="new_reliability"
+            value={query.new_reliability}
           />
 
           <BasicSeparator className="sw-my-4" />
 
-          <SecurityReviewFilter
+          <RatingFilter
             {...facetProps}
-            facet={getFacet(facets, 'new_security_review')}
-            property="new_security_review"
-            value={query.new_security_review_rating}
+            facets={facets}
+            property="new_maintainability"
+            value={query.new_maintainability}
           />
 
           <BasicSeparator className="sw-my-4" />
 
-          <NewMaintainabilityFilter
+          <RatingFilter
             {...facetProps}
-            facet={getFacet(facets, 'new_maintainability')}
-            value={query.new_maintainability}
+            facets={facets}
+            property="security_review"
+            value={query.new_security_review_rating}
           />
 
           <BasicSeparator className="sw-my-4" />
index df733296b250f5e21ae52bcc343e7d0fa6439682..698bc1abb133490dbffcea07be210b09ccbbf151 100644 (file)
@@ -81,7 +81,7 @@ it('changes sort and perspective', async () => {
   const user = userEvent.setup();
   renderProjects();
 
-  await user.click(ui.sortSelect.get());
+  await user.click(await ui.sortSelect.find());
   await user.click(screen.getByText('projects.sorting.size'));
 
   const projects = ui.projects.getAll();
@@ -108,7 +108,7 @@ it('handles showing favorite projects on load', async () => {
   const user = userEvent.setup();
   renderProjects(`${BASE_PATH}/favorite`);
 
-  expect(ui.myFavoritesToggleOption.get()).toHaveAttribute('aria-current', 'true');
+  expect(await ui.myFavoritesToggleOption.find()).toHaveAttribute('aria-current', 'true');
   expect(await ui.projects.findAll()).toHaveLength(2);
 
   await user.click(ui.allToggleOption.get());
index 1ed4cde499ba37986a50930c5fe43eab5bf0c520..63fe3f135e97d1f740e0308740949163475b73ec 100644 (file)
 import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import * as React from 'react';
+import SettingsServiceMock from '../../../../api/mocks/SettingsServiceMock';
 import { CurrentUserContext } from '../../../../app/components/current-user/CurrentUserContext';
 import { mockCurrentUser } from '../../../../helpers/testMocks';
 import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { SettingsKey } from '../../../../types/settings';
 import { CurrentUser } from '../../../../types/users';
 import PageSidebar, { PageSidebarProps } from '../PageSidebar';
 
+const settingsHandler = new SettingsServiceMock();
+
+beforeEach(() => {
+  settingsHandler.reset();
+});
+
 it('should render the right facets for overview', () => {
   renderPageSidebar({
     query: { size: '3' },
@@ -76,6 +84,26 @@ it('should allow to clear all filters', async () => {
   expect(screen.getByRole('heading', { level: 2, name: 'filters' })).toHaveFocus();
 });
 
+it('should show legacy filters', async () => {
+  settingsHandler.set(SettingsKey.LegacyMode, 'true');
+  renderPageSidebar();
+
+  expect(await screen.findAllByText('E')).toHaveLength(4);
+  expect(screen.queryByText(/projects.facets.rating_option/)).not.toBeInTheDocument();
+  expect(screen.queryByText('projects.facets.maintainability.description')).not.toBeInTheDocument();
+  expect(screen.queryByText('projects.facets.security_review.description')).not.toBeInTheDocument();
+});
+
+it('should show non legacy filters', async () => {
+  settingsHandler.set(SettingsKey.LegacyMode, 'false');
+  renderPageSidebar();
+
+  expect(await screen.findAllByText(/projects.facets.rating_option/)).toHaveLength(16);
+  expect(screen.queryAllByText('E')).toHaveLength(0);
+  expect(screen.getByText('projects.facets.maintainability.description')).toBeInTheDocument();
+  expect(screen.getByText('projects.facets.security_review.description')).toBeInTheDocument();
+});
+
 function renderPageSidebar(overrides: Partial<PageSidebarProps> = {}, currentUser?: CurrentUser) {
   return renderComponent(
     <CurrentUserContext.Provider
index 26d8ae6558deababd1221aeb343e6c70dd3fd2e7..e062e3961027ab9b1c3c8ceefcc87e4b7820a1a5 100644 (file)
@@ -27,6 +27,7 @@ import SettingsServiceMock from '../../../../../api/mocks/SettingsServiceMock';
 import { mockComponent } from '../../../../../helpers/mocks/component';
 import { mockCurrentUser, mockLoggedInUser, mockMeasure } from '../../../../../helpers/testMocks';
 import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
+import { SettingsKey } from '../../../../../types/settings';
 import { CurrentUser } from '../../../../../types/users';
 import { Project } from '../../../types';
 import ProjectCard from '../ProjectCard';
@@ -282,7 +283,7 @@ describe('upgrade scenario (awaiting scan)', () => {
   });
 
   it('should not display awaiting analysis badge if legacy mode is enabled', async () => {
-    settingsHandler.set('sonar.legacy.ratings.mode.enabled', 'true');
+    settingsHandler.set(SettingsKey.LegacyMode, 'true');
     renderProjectCard({
       ...PROJECT,
       measures: {
@@ -300,7 +301,7 @@ describe('upgrade scenario (awaiting scan)', () => {
   });
 
   it('should not display new values if legacy mode is enabled', async () => {
-    settingsHandler.set('sonar.legacy.ratings.mode.enabled', 'true');
+    settingsHandler.set(SettingsKey.LegacyMode, 'true');
     measuresHandler.registerComponentMeasures({
       [PROJECT.key]: {
         ...newRatings,
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.tsx
deleted file mode 100644 (file)
index b383a36..0000000
+++ /dev/null
@@ -1,36 +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 { RawQuery } from '~sonar-aligned/types/router';
-import { Facet } from '../types';
-import RatingFacet from './RatingFacet';
-
-interface Props {
-  className?: string;
-  facet?: Facet;
-  headerDetail?: React.ReactNode;
-  maxFacetValue?: number;
-  onQueryChange: (change: RawQuery) => void;
-  value?: any;
-}
-
-export default function MaintainabilityFilter(props: Props) {
-  return <RatingFacet {...props} name="Maintainability" property="maintainability" />;
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/NewMaintainabilityFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/NewMaintainabilityFilter.tsx
deleted file mode 100644 (file)
index 7ecdbd1..0000000
+++ /dev/null
@@ -1,34 +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 { RawQuery } from '~sonar-aligned/types/router';
-import { Facet } from '../types';
-import RatingFacet from './RatingFacet';
-
-interface Props {
-  facet?: Facet;
-  maxFacetValue?: number;
-  onQueryChange: (change: RawQuery) => void;
-  value?: any;
-}
-
-export default function NewMaintainabilityFilter(props: Props) {
-  return <RatingFacet {...props} name="Maintainability" property="new_maintainability" />;
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/NewReliabilityFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/NewReliabilityFilter.tsx
deleted file mode 100644 (file)
index ab38d55..0000000
+++ /dev/null
@@ -1,34 +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 { RawQuery } from '~sonar-aligned/types/router';
-import { Facet } from '../types';
-import RatingFacet from './RatingFacet';
-
-interface Props {
-  facet?: Facet;
-  maxFacetValue?: number;
-  onQueryChange: (change: RawQuery) => void;
-  value?: any;
-}
-
-export default function NewReliabilityFilter(props: Props) {
-  return <RatingFacet {...props} name="Reliability" property="new_reliability" />;
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/NewSecurityFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/NewSecurityFilter.tsx
deleted file mode 100644 (file)
index 43bded5..0000000
+++ /dev/null
@@ -1,34 +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 { RawQuery } from '~sonar-aligned/types/router';
-import { Facet } from '../types';
-import RatingFacet from './RatingFacet';
-
-interface Props {
-  facet?: Facet;
-  maxFacetValue?: number;
-  onQueryChange: (change: RawQuery) => void;
-  value?: any;
-}
-
-export default function NewSecurityFilter(props: Props) {
-  return <RatingFacet {...props} name="Security" property="new_security" />;
-}
index b80678c43504b5305b5fe6feb21f761458dae40a..63ebbd130629d89b4cfe27fb2dae68a4cafe513e 100644 (file)
@@ -31,6 +31,7 @@ export type Option = string | number;
 
 interface Props {
   className?: string;
+  description?: string;
   facet?: Facet;
   getFacetValueForOption?: (facet: Facet, option: Option) => number;
   header: string;
@@ -153,10 +154,13 @@ export default class RangeFacetBase extends React.PureComponent<Props> {
   };
 
   render() {
-    const { className, header, property } = this.props;
+    const { className, header, property, description } = this.props;
 
     return (
       <FacetBox className={className} name={header} data-key={property} open>
+        {description && (
+          <LightLabel className="sw-mb-4 sw--mt-2 sw-block">{description}</LightLabel>
+        )}
         {this.renderOptions()}
       </FacetBox>
     );
index 9b8df67e781f4fcf22cbf3aea7e5f696c7050bad..5dbbfc52654b2c780027f5a5a54dff936eefd55c 100644 (file)
  */
 import { MetricsRatingBadge, RatingEnum } from 'design-system';
 import * as React from 'react';
+import { useIntl } from 'react-intl';
 import { formatMeasure } from '~sonar-aligned/helpers/measures';
 import { MetricType } from '~sonar-aligned/types/metrics';
 import { RawQuery } from '~sonar-aligned/types/router';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { useIsLegacyCCTMode } from '../../../queries/settings';
 import { Facet } from '../types';
 import RangeFacetBase from './RangeFacetBase';
 
@@ -37,6 +39,7 @@ interface Props {
 
 export default function RatingFacet(props: Props) {
   const { facet, maxFacetValue, name, property, value } = props;
+  const { data: isLegacy } = useIsLegacyCCTMode();
 
   const renderAccessibleLabel = React.useCallback(
     (option: number) => {
@@ -61,22 +64,54 @@ export default function RatingFacet(props: Props) {
     <RangeFacetBase
       facet={facet}
       header={translate('metric_domain', name)}
+      description={
+        !isLegacy && hasDescription(property)
+          ? translate(`projects.facets.${property.replace('new_', '')}.description`)
+          : undefined
+      }
       highlightUnder={1}
       maxFacetValue={maxFacetValue}
       onQueryChange={props.onQueryChange}
-      options={[1, 2, 3, 4, 5]}
+      options={isLegacy ? [1, 2, 3, 4, 5] : [1, 2, 3, 4]}
       property={property}
       renderAccessibleLabel={renderAccessibleLabel}
-      renderOption={renderOption}
+      renderOption={(option) => renderOption(option, property)}
       value={value}
     />
   );
 }
 
-function renderOption(option: number) {
-  const ratingFormatted = formatMeasure(option, MetricType.Rating);
+const hasDescription = (property: string) => {
+  return ['maintainability', 'new_maintainability', 'security_review'].includes(property);
+};
+
+function renderOption(option: string | number, property: string) {
+  return <RatingOption option={option} property={property} />;
+}
+
+function RatingOption({
+  option,
+  property,
+}: Readonly<{ option: string | number; property: string }>) {
+  const { data: isLegacy } = useIsLegacyCCTMode();
+  const intl = useIntl();
 
+  const ratingFormatted = formatMeasure(option, MetricType.Rating);
   return (
-    <MetricsRatingBadge label={ratingFormatted} rating={ratingFormatted as RatingEnum} size="xs" />
+    <>
+      <MetricsRatingBadge
+        label={ratingFormatted}
+        rating={ratingFormatted as RatingEnum}
+        isLegacy={isLegacy}
+        size="xs"
+      />
+      {!isLegacy && (
+        <span className="sw-ml-2">
+          {intl.formatMessage({
+            id: `projects.facets.rating_option.${property.replace('new_', '')}.${option}`,
+          })}
+        </span>
+      )}
+    </>
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/RatingFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/RatingFilter.tsx
new file mode 100644 (file)
index 0000000..15d2d5a
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * 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 { RawQuery } from '~sonar-aligned/types/router';
+import { Facets } from '../types';
+import RatingFacet from './RatingFacet';
+
+interface Props {
+  facets?: Facets;
+  headerDetail?: React.ReactNode;
+  maxFacetValue?: number;
+  onQueryChange: (change: RawQuery) => void;
+  property: string;
+  value?: any;
+}
+
+export default function RatingFilter({ facets, ...props }: Readonly<Props>) {
+  return (
+    <RatingFacet
+      {...props}
+      facet={getFacet(facets, props.property)}
+      name={getFacetName(props.property)}
+    />
+  );
+}
+
+function getFacetName(property: string) {
+  switch (property) {
+    case 'new_security':
+    case 'security':
+      return 'Security';
+    case 'new_maintainability':
+    case 'maintainability':
+      return 'Maintainability';
+    case 'new_reliability':
+    case 'reliability':
+      return 'Reliability';
+    case 'new_security_review':
+    case 'security_review':
+      return 'SecurityReview';
+    default:
+      return property;
+  }
+}
+
+function getFacet(facets: Facets | undefined, name: string) {
+  return facets && facets[name];
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/ReliabilityFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/ReliabilityFilter.tsx
deleted file mode 100644 (file)
index 82f29a5..0000000
+++ /dev/null
@@ -1,35 +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 { RawQuery } from '~sonar-aligned/types/router';
-import { Facet } from '../types';
-import RatingFacet from './RatingFacet';
-
-interface Props {
-  facet?: Facet;
-  headerDetail?: React.ReactNode;
-  maxFacetValue?: number;
-  onQueryChange: (change: RawQuery) => void;
-  value?: any;
-}
-
-export default function ReliabilityFilter(props: Props) {
-  return <RatingFacet {...props} name="Reliability" property="reliability" />;
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SecurityFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/SecurityFilter.tsx
deleted file mode 100644 (file)
index 1adb18e..0000000
+++ /dev/null
@@ -1,35 +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 { RawQuery } from '~sonar-aligned/types/router';
-import { Facet } from '../types';
-import RatingFacet from './RatingFacet';
-
-interface Props {
-  facet?: Facet;
-  headerDetail?: React.ReactNode;
-  maxFacetValue?: number;
-  onQueryChange: (change: RawQuery) => void;
-  value?: any;
-}
-
-export default function SecurityFilter(props: Props) {
-  return <RatingFacet {...props} name="Security" property="security" />;
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SecurityReviewFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/SecurityReviewFilter.tsx
deleted file mode 100644 (file)
index dde1c3b..0000000
+++ /dev/null
@@ -1,94 +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 { MetricsRatingBadge, RatingEnum } from 'design-system';
-import * as React from 'react';
-import { formatMeasure } from '~sonar-aligned/helpers/measures';
-import { MetricType } from '~sonar-aligned/types/metrics';
-import { RawQuery } from '~sonar-aligned/types/router';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { Dict } from '../../../types/types';
-import { Facet } from '../types';
-import RangeFacetBase from './RangeFacetBase';
-
-export interface Props {
-  facet?: Facet;
-  maxFacetValue?: number;
-  onQueryChange: (change: RawQuery) => void;
-  property?: string;
-  value?: any;
-}
-
-const labels: Dict<string> = {
-  1: '≥ 80%',
-  2: '70% - 80%',
-  3: '50% - 70%',
-  4: '30% - 50%',
-  5: '< 30%',
-};
-
-export default function SecurityReviewFilter(props: Props) {
-  const { facet, maxFacetValue, property = 'security_review', value } = props;
-
-  return (
-    <RangeFacetBase
-      facet={facet}
-      header={translate('metric_domain.SecurityReview')}
-      highlightUnder={1}
-      maxFacetValue={maxFacetValue}
-      onQueryChange={props.onQueryChange}
-      options={[1, 2, 3, 4, 5]}
-      property={property}
-      renderAccessibleLabel={renderAccessibleLabel}
-      renderOption={renderOption}
-      value={value}
-    />
-  );
-}
-
-function renderAccessibleLabel(option: number) {
-  if (option === 1) {
-    return translateWithParameters(
-      'projects.facets.rating_label_single_x',
-      translate('metric_domain.SecurityReview'),
-      formatMeasure(option, MetricType.Rating),
-    );
-  }
-
-  return translateWithParameters(
-    'projects.facets.rating_label_multi_x',
-    translate('metric_domain.SecurityReview'),
-    formatMeasure(option, MetricType.Rating),
-  );
-}
-
-function renderOption(option: number) {
-  const ratingFormatted = formatMeasure(option, MetricType.Rating);
-
-  return (
-    <div className="sw-flex sw-items-center">
-      <MetricsRatingBadge
-        label={ratingFormatted}
-        rating={ratingFormatted as RatingEnum}
-        size="xs"
-      />
-      <span className="sw-ml-2">{labels[option]}</span>
-    </div>
-  );
-}
index 3ccf4a0d176a2eaf5d68ba7cc42945001a766a31..5dd5b96420455486b83585ec8c09d4ab1b0a2bcf 100644 (file)
@@ -19,7 +19,7 @@
  */
 import { ComponentQualifier } from '~sonar-aligned/types/component';
 import { RawQuery } from '~sonar-aligned/types/router';
-import { Dict } from '../../types/types';
+import { propertyToMetricMap, propertyToMetricMapLegacy } from './utils';
 
 type Level = 'ERROR' | 'WARN' | 'OK';
 
@@ -74,7 +74,7 @@ export function parseUrlQuery(urlQuery: RawQuery): Query {
   };
 }
 
-export function convertToFilter(query: Query, isFavorite: boolean): string {
+export function convertToFilter(query: Query, isFavorite: boolean, isLegacy: boolean): string {
   const conditions: string[] = [];
 
   if (isFavorite) {
@@ -82,19 +82,19 @@ export function convertToFilter(query: Query, isFavorite: boolean): string {
   }
 
   if (query['gate'] != null) {
-    conditions.push(mapPropertyToMetric('gate') + ' = ' + query['gate']);
+    conditions.push(mapPropertyToMetric('gate', isLegacy) + ' = ' + query['gate']);
   }
 
   ['coverage', 'new_coverage'].forEach((property) =>
-    pushMetricToArray(query, property, conditions, convertCoverage),
+    pushMetricToArray(query, property, conditions, convertCoverage, isLegacy),
   );
 
   ['duplications', 'new_duplications'].forEach((property) =>
-    pushMetricToArray(query, property, conditions, convertDuplications),
+    pushMetricToArray(query, property, conditions, convertDuplications, isLegacy),
   );
 
   ['size', 'new_lines'].forEach((property) =>
-    pushMetricToArray(query, property, conditions, convertSize),
+    pushMetricToArray(query, property, conditions, convertSize, isLegacy),
   );
 
   [
@@ -106,14 +106,16 @@ export function convertToFilter(query: Query, isFavorite: boolean): string {
     'new_security',
     'new_security_review_rating',
     'new_maintainability',
-  ].forEach((property) => pushMetricToArray(query, property, conditions, convertIssuesRating));
+  ].forEach((property) =>
+    pushMetricToArray(query, property, conditions, convertIssuesRating, isLegacy),
+  );
 
   ['languages', 'tags', 'qualifier'].forEach((property) =>
-    pushMetricToArray(query, property, conditions, convertArrayMetric),
+    pushMetricToArray(query, property, conditions, convertArrayMetric, isLegacy),
   );
 
   if (query['search'] != null) {
-    conditions.push(`${mapPropertyToMetric('search')} = "${query['search']}"`);
+    conditions.push(`${mapPropertyToMetric('search', isLegacy)} = "${query['search']}"`);
   }
 
   return conditions.join(' and ');
@@ -233,30 +235,8 @@ function convertSize(metric: string, size: number): string {
   }
 }
 
-function mapPropertyToMetric(property?: string): string | undefined {
-  const map: Dict<string> = {
-    analysis_date: 'analysisDate',
-    reliability: 'reliability_rating',
-    new_reliability: 'new_reliability_rating',
-    security: 'security_rating',
-    new_security: 'new_security_rating',
-    security_review_rating: 'security_review_rating',
-    new_security_review_rating: 'new_security_review_rating',
-    maintainability: 'sqale_rating',
-    new_maintainability: 'new_maintainability_rating',
-    coverage: 'coverage',
-    new_coverage: 'new_coverage',
-    duplications: 'duplicated_lines_density',
-    new_duplications: 'new_duplicated_lines_density',
-    size: 'ncloc',
-    new_lines: 'new_lines',
-    gate: 'alert_status',
-    languages: 'languages',
-    tags: 'tags',
-    search: 'query',
-    qualifier: 'qualifier',
-  };
-  return property && map[property];
+function mapPropertyToMetric(property?: string, isLegacy: boolean = false): string | undefined {
+  return property && (isLegacy ? propertyToMetricMapLegacy : propertyToMetricMap)[property];
 }
 
 function pushMetricToArray(
@@ -264,8 +244,9 @@ function pushMetricToArray(
   property: string,
   conditionsArray: string[],
   convertFunction: (metric: string, value: Query[string]) => string,
+  isLegacy: boolean,
 ): void {
-  const metric = mapPropertyToMetric(property);
+  const metric = mapPropertyToMetric(property, isLegacy);
   if (query[property] !== undefined && metric !== undefined) {
     conditionsArray.push(convertFunction(metric, query[property]));
   }
index 426e9195529d386d48d9d767f43537d052e6c3f3..21ec392bfbea28938ce7cd2cc72a127516fddc35 100644 (file)
@@ -121,29 +121,57 @@ export const LEAK_METRICS = [
   MetricKey.projects,
 ];
 
+export const LEGACY_FACETS = [
+  MetricKey.reliability_rating,
+  MetricKey.security_rating,
+  MetricKey.security_review_rating,
+  MetricKey.sqale_rating,
+  MetricKey.coverage,
+  MetricKey.duplicated_lines_density,
+  MetricKey.ncloc,
+  MetricKey.alert_status,
+  'languages',
+  'tags',
+  'qualifier',
+];
+
 export const FACETS = [
-  'reliability_rating',
-  'security_rating',
-  'security_review_rating',
-  'sqale_rating',
-  'coverage',
-  'duplicated_lines_density',
-  'ncloc',
-  'alert_status',
+  MetricKey.software_quality_reliability_rating,
+  MetricKey.software_quality_security_rating,
+  MetricKey.software_quality_security_review_rating,
+  MetricKey.software_quality_maintainability_rating,
+  MetricKey.coverage,
+  MetricKey.duplicated_lines_density,
+  MetricKey.ncloc,
+  MetricKey.alert_status,
+  'languages',
+  'tags',
+  'qualifier',
+];
+
+export const LEGACY_LEAK_FACETS = [
+  MetricKey.new_reliability_rating,
+  MetricKey.new_security_rating,
+  MetricKey.new_security_review_rating,
+  MetricKey.new_maintainability_rating,
+  MetricKey.new_coverage,
+  MetricKey.new_duplicated_lines_density,
+  MetricKey.new_lines,
+  MetricKey.alert_status,
   'languages',
   'tags',
   'qualifier',
 ];
 
 export const LEAK_FACETS = [
-  'new_reliability_rating',
-  'new_security_rating',
-  'new_security_review_rating',
-  'new_maintainability_rating',
-  'new_coverage',
-  'new_duplicated_lines_density',
-  'new_lines',
-  'alert_status',
+  MetricKey.new_software_quality_reliability_rating,
+  MetricKey.new_software_quality_security_rating,
+  MetricKey.new_software_quality_security_review_rating,
+  MetricKey.new_software_quality_maintainability_rating,
+  MetricKey.new_coverage,
+  MetricKey.new_duplicated_lines_density,
+  MetricKey.new_lines,
+  MetricKey.alert_status,
   'languages',
   'tags',
   'qualifier',
@@ -179,17 +207,19 @@ export function fetchProjects({
   isFavorite,
   query,
   pageIndex = 1,
+  isLegacy,
 }: {
   isFavorite: boolean;
+  isLegacy: boolean;
   pageIndex?: number;
   query: Query;
 }) {
   const ps = PAGE_SIZE;
 
-  const data = convertToQueryData(query, isFavorite, {
+  const data = convertToQueryData(query, isFavorite, isLegacy, {
     p: pageIndex > 1 ? pageIndex : undefined,
     ps,
-    facets: defineFacets(query).join(),
+    facets: defineFacets(query, isLegacy).join(),
     f: 'analysisDate,leakPeriodDate',
   });
 
@@ -197,7 +227,7 @@ export function fetchProjects({
     .then((response) => Promise.all([Promise.resolve(response), fetchScannableProjects()]))
     .then(([{ components, facets, paging }, { scannableProjects }]) => {
       return {
-        facets: getFacetsMap(facets),
+        facets: getFacetsMap(facets, isLegacy),
         projects: components.map((component) => ({
           ...component,
           isScannable: scannableProjects.find((p) => p.key === component.key) !== undefined,
@@ -215,18 +245,23 @@ export function defineMetrics(query: Query): string[] {
   return METRICS;
 }
 
-function defineFacets(query: Query): string[] {
+function defineFacets(query: Query, isLegacy: boolean): string[] {
   if (query.view === 'leak') {
-    return LEAK_FACETS;
+    return isLegacy ? LEGACY_LEAK_FACETS : LEAK_FACETS;
   }
 
-  return FACETS;
+  return isLegacy ? LEGACY_FACETS : FACETS;
 }
 
-export function convertToQueryData(query: Query, isFavorite: boolean, defaultData = {}) {
+export function convertToQueryData(
+  query: Query,
+  isFavorite: boolean,
+  isLegacy: boolean,
+  defaultData = {},
+) {
   const data: RequestData = { ...defaultData };
-  const filter = convertToFilter(query, isFavorite);
-  const sort = convertToSorting(query);
+  const filter = convertToFilter(query, isFavorite, isLegacy);
+  const sort = convertToSorting(query, isLegacy);
 
   if (filter) {
     data.filter = filter;
@@ -253,7 +288,7 @@ function mapFacetValues(values: Array<{ count: number; val: string }>) {
   return map;
 }
 
-const propertyToMetricMap: Dict<string | undefined> = {
+export const propertyToMetricMapLegacy: Dict<string | undefined> = {
   analysis_date: 'analysisDate',
   reliability: 'reliability_rating',
   new_reliability: 'new_reliability_rating',
@@ -277,13 +312,25 @@ const propertyToMetricMap: Dict<string | undefined> = {
   creation_date: 'creationDate',
 };
 
-const metricToPropertyMap = invert(propertyToMetricMap);
+export const propertyToMetricMap: Dict<string | undefined> = {
+  ...propertyToMetricMapLegacy,
+  reliability: 'software_quality_reliability_rating',
+  new_reliability: 'new_software_quality_reliability_rating',
+  security: 'software_quality_security_rating',
+  new_security: 'new_software_quality_security_rating',
+  security_review: 'software_quality_security_review_rating',
+  new_security_review: 'new_software_quality_security_review_rating',
+  maintainability: 'software_quality_maintainability_rating',
+  new_maintainability: 'new_software_quality_maintainability_rating',
+};
 
-function getFacetsMap(facets: Facet[]) {
+function getFacetsMap(facets: Facet[], isLegacy: boolean) {
   const map: Dict<Dict<number>> = {};
 
   facets.forEach((facet) => {
-    const property = metricToPropertyMap[facet.property];
+    const property = invert(isLegacy ? propertyToMetricMapLegacy : propertyToMetricMap)[
+      facet.property
+    ];
     const { values } = facet;
 
     if (REVERSED_FACETS.includes(property)) {
@@ -296,12 +343,18 @@ function getFacetsMap(facets: Facet[]) {
   return map;
 }
 
-export function convertToSorting({ sort }: Query): { asc?: boolean; s?: string } {
+export function convertToSorting(
+  { sort }: Query,
+  isLegacy: boolean,
+): { asc?: boolean; s?: string } {
   if (sort?.startsWith('-')) {
-    return { s: propertyToMetricMap[sort.substring(1)], asc: false };
+    return {
+      s: (isLegacy ? propertyToMetricMapLegacy : propertyToMetricMap)[sort.substring(1)],
+      asc: false,
+    };
   }
 
-  return { s: propertyToMetricMap[sort ?? ''] };
+  return { s: (isLegacy ? propertyToMetricMapLegacy : propertyToMetricMap)[sort ?? ''] };
 }
 
 const ONE_MINUTE = 60000;
index f6534ee40b80e042f4f9a557264f286872f4196a..2648e452844d98bae5d05c77432ba8b954449833 100644 (file)
@@ -21,7 +21,7 @@ import { queryOptions, useMutation, useQuery, useQueryClient } from '@tanstack/r
 import { addGlobalSuccessMessage } from 'design-system';
 import { getValue, getValues, resetSettingValue, setSettingValue } from '../api/settings';
 import { translate } from '../helpers/l10n';
-import { ExtendedSettingDefinition } from '../types/settings';
+import { ExtendedSettingDefinition, SettingsKey } from '../types/settings';
 import { createQueryHook } from './common';
 
 type SettingValue = string | boolean | string[];
@@ -48,7 +48,7 @@ export const useGetValueQuery = createQueryHook(
 
 export const useIsLegacyCCTMode = () => {
   return useGetValueQuery(
-    { key: 'sonar.legacy.ratings.mode.enabled' },
+    { key: SettingsKey.LegacyMode },
     { staleTime: Infinity, select: (data) => data?.value === 'true' },
   );
 };
index 7722625a5eae043547419d8149926f2d41a5d2e7..16f5083bd5493b9fa46f2d12f24e5298b28682ff 100644 (file)
@@ -28,6 +28,7 @@ export const enum SettingsKey {
   LicenceRemainingLocNotificationThreshold = 'sonar.license.notifications.remainingLocThreshold',
   TokenMaxAllowedLifetime = 'sonar.auth.token.max.allowed.lifetime',
   QPAdminCanDisableInheritedRules = 'sonar.qualityProfiles.allowDisableInheritedRules',
+  LegacyMode = 'sonar.legacy.ratings.mode.enabled',
 }
 
 export enum GlobalSettingKeys {
index feb5feb18eb6d377c2fc85b4261996e5aa3d489a..31b034075ef96c27871dea255c49a4272796de43 100644 (file)
@@ -1305,6 +1305,24 @@ projects.limited_set_of_projects=Displayed project set limited to the top {0} pr
 projects.facets.quality_gate=Quality Gate
 projects.facets.quality_gate.warning_help=Warning status is deprecated. This filter will disappear when no Warning Quality Gate remains.
 projects.facets.rating_x={0} rating
+projects.facets.rating_option.reliability.1=0 issues
+projects.facets.rating_option.reliability.2=≥ 1 low issue
+projects.facets.rating_option.reliability.3=≥ 1 medium issue
+projects.facets.rating_option.reliability.4=≥ 1 high issue
+projects.facets.rating_option.security.1=0 issues
+projects.facets.rating_option.security.2=≥ 1 low issue
+projects.facets.rating_option.security.3=≥ 1 medium issue
+projects.facets.rating_option.security.4=≥ 1 high issue
+projects.facets.rating_option.maintainability.1=≤ 5% to 0%
+projects.facets.rating_option.maintainability.2=≥ 5% to <10%
+projects.facets.rating_option.maintainability.3=≥ 10% to <20%
+projects.facets.rating_option.maintainability.4=≥ 20%
+projects.facets.rating_option.security_review.1== 100%
+projects.facets.rating_option.security_review.2=≥ 70% to <100%
+projects.facets.rating_option.security_review.3=≥ 50% to <70%
+projects.facets.rating_option.security_review.4=< 50%
+projects.facets.security_review.description=The percentage of reviewed (fixed or safe) security hotspots
+projects.facets.maintainability.description=Ratio of the size of the project to the estimated time needed to fix all outstanding maintainability issues
 projects.facets.languages=Languages
 projects.facets.search.languages=Search for languages
 projects.facets.new_lines=New Lines