From ac8fc4c3d9ff6d8b366f13c159e8fad51d58843c Mon Sep 17 00:00:00 2001 From: Viktor Vorona Date: Wed, 14 Aug 2024 14:03:11 +0200 Subject: [PATCH] SONAR-22710 Projects facet --- .../__snapshots__/HotspotRating-test.tsx.snap | 2 +- .../components/MetricsRatingBadge.tsx | 16 ++- .../design-system/src/theme/light.ts | 9 +- .../components/metrics/RatingComponent.tsx | 5 +- .../js/apps/projects/__tests__/utils-test.ts | 59 ++++++++- .../apps/projects/components/AllProjects.tsx | 25 ++-- .../apps/projects/components/PageSidebar.tsx | 65 +++++----- .../components/__tests__/AllProjects-test.tsx | 4 +- .../components/__tests__/PageSidebar-test.tsx | 28 +++++ .../__tests__/ProjectCard-test.tsx | 5 +- .../filters/NewMaintainabilityFilter.tsx | 34 ----- .../projects/filters/NewReliabilityFilter.tsx | 34 ----- .../projects/filters/NewSecurityFilter.tsx | 34 ----- .../apps/projects/filters/RangeFacetBase.tsx | 6 +- .../js/apps/projects/filters/RatingFacet.tsx | 45 ++++++- ...tainabilityFilter.tsx => RatingFilter.tsx} | 39 +++++- .../projects/filters/ReliabilityFilter.tsx | 35 ------ .../apps/projects/filters/SecurityFilter.tsx | 35 ------ .../projects/filters/SecurityReviewFilter.tsx | 94 -------------- .../src/main/js/apps/projects/query.ts | 49 +++----- .../src/main/js/apps/projects/utils.ts | 117 +++++++++++++----- .../sonar-web/src/main/js/queries/settings.ts | 4 +- .../sonar-web/src/main/js/types/settings.ts | 1 + .../resources/org/sonar/l10n/core.properties | 18 +++ 24 files changed, 361 insertions(+), 402 deletions(-) delete mode 100644 server/sonar-web/src/main/js/apps/projects/filters/NewMaintainabilityFilter.tsx delete mode 100644 server/sonar-web/src/main/js/apps/projects/filters/NewReliabilityFilter.tsx delete mode 100644 server/sonar-web/src/main/js/apps/projects/filters/NewSecurityFilter.tsx rename server/sonar-web/src/main/js/apps/projects/filters/{MaintainabilityFilter.tsx => RatingFilter.tsx} (57%) delete mode 100644 server/sonar-web/src/main/js/apps/projects/filters/ReliabilityFilter.tsx delete mode 100644 server/sonar-web/src/main/js/apps/projects/filters/SecurityFilter.tsx delete mode 100644 server/sonar-web/src/main/js/apps/projects/filters/SecurityReviewFilter.tsx diff --git a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/HotspotRating-test.tsx.snap b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/HotspotRating-test.tsx.snap index 741beaababd..900f1f226d9 100644 --- a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/HotspotRating-test.tsx.snap +++ b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/HotspotRating-test.tsx.snap @@ -74,7 +74,7 @@ exports[`should render HotspotRating with MEDIUM rating 1`] = ` ( - ({ className, size = 'sm', label, rating, ...ariaAttrs }: Readonly, ref) => { + ( + { className, size = 'sm', isLegacy = true, label, rating, ...ariaAttrs }: Readonly, + ref, + ) => { if (!rating) { return ( ( { } }; -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; diff --git a/server/sonar-web/design-system/src/theme/light.ts b/server/sonar-web/design-system/src/theme/light.ts index 098ca5ce763..dce350b51b6 100644 --- a/server/sonar-web/design-system/src/theme/light.ts +++ b/server/sonar-web/design-system/src/theme/light.ts @@ -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 diff --git a/server/sonar-web/src/main/js/app/components/metrics/RatingComponent.tsx b/server/sonar-web/src/main/js/app/components/metrics/RatingComponent.tsx index d5e2a61644a..03de2c241b6 100644 --- a/server/sonar-web/src/main/js/app/components/metrics/RatingComponent.tsx +++ b/server/sonar-web/src/main/js/app/components/metrics/RatingComponent.tsx @@ -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) { const badge = ( { 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', + }); }); }); diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx index f7fe5e311bd..b366a9ca956 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx @@ -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 { } 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 { }; 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 { }; 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) { +function AllProjectsWrapper(props: Readonly>) { const [searchParams, setSearchParams] = useSearchParams(); const savedOptions = getStorageOptions(); + const { data: isLegacy, isLoading } = useIsLegacyCCTMode(); React.useEffect( () => { @@ -366,10 +369,14 @@ function SetSearchParamsWrapper(props: Readonly) { ], ); - return ; + return ( + + + + ); } -export default withRouter(withCurrentUserContext(withAppStateContext(SetSearchParamsWrapper))); +export default withRouter(withCurrentUserContext(withAppStateContext(AllProjectsWrapper))); const StyledWrapper = styled.div` background-color: ${themeColor('backgroundPrimary')}; diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx index 2aed6d9bfae..3398f06718f 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx @@ -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 && ( <> - - - - @@ -160,35 +158,38 @@ export default function PageSidebar(props: PageSidebarProps) { )} {isLeakView && ( <> - - - - diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx index df733296b25..698bc1abb13 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx @@ -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()); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.tsx index 1ed4cde499b..63fe3f135e9 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.tsx @@ -20,12 +20,20 @@ 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 = {}, currentUser?: CurrentUser) { return renderComponent( { }); 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/NewMaintainabilityFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/NewMaintainabilityFilter.tsx deleted file mode 100644 index 7ecdbd101ab..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/filters/NewMaintainabilityFilter.tsx +++ /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 ; -} 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 index ab38d55005c..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/filters/NewReliabilityFilter.tsx +++ /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 ; -} 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 index 43bded56cec..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/filters/NewSecurityFilter.tsx +++ /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 ; -} diff --git a/server/sonar-web/src/main/js/apps/projects/filters/RangeFacetBase.tsx b/server/sonar-web/src/main/js/apps/projects/filters/RangeFacetBase.tsx index b80678c4350..63ebbd13062 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/RangeFacetBase.tsx +++ b/server/sonar-web/src/main/js/apps/projects/filters/RangeFacetBase.tsx @@ -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 { }; render() { - const { className, header, property } = this.props; + const { className, header, property, description } = this.props; return ( + {description && ( + {description} + )} {this.renderOptions()} ); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/RatingFacet.tsx b/server/sonar-web/src/main/js/apps/projects/filters/RatingFacet.tsx index 9b8df67e781..5dbbfc52654 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/RatingFacet.tsx +++ b/server/sonar-web/src/main/js/apps/projects/filters/RatingFacet.tsx @@ -19,10 +19,12 @@ */ 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) { 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 ; +} + +function RatingOption({ + option, + property, +}: Readonly<{ option: string | number; property: string }>) { + const { data: isLegacy } = useIsLegacyCCTMode(); + const intl = useIntl(); + const ratingFormatted = formatMeasure(option, MetricType.Rating); return ( - + <> + + {!isLegacy && ( + + {intl.formatMessage({ + id: `projects.facets.rating_option.${property.replace('new_', '')}.${option}`, + })} + + )} + ); } diff --git a/server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/RatingFilter.tsx similarity index 57% rename from server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.tsx rename to server/sonar-web/src/main/js/apps/projects/filters/RatingFilter.tsx index b383a368fd3..15d2d5aa270 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.tsx +++ b/server/sonar-web/src/main/js/apps/projects/filters/RatingFilter.tsx @@ -19,18 +19,47 @@ */ import * as React from 'react'; import { RawQuery } from '~sonar-aligned/types/router'; -import { Facet } from '../types'; +import { Facets } from '../types'; import RatingFacet from './RatingFacet'; interface Props { - className?: string; - facet?: Facet; + facets?: Facets; headerDetail?: React.ReactNode; maxFacetValue?: number; onQueryChange: (change: RawQuery) => void; + property: string; value?: any; } -export default function MaintainabilityFilter(props: Props) { - return ; +export default function RatingFilter({ facets, ...props }: Readonly) { + return ( + + ); +} + +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 index 82f29a5f837..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/filters/ReliabilityFilter.tsx +++ /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 ; -} 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 index 1adb18ec05d..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/filters/SecurityFilter.tsx +++ /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 ; -} 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 index dde1c3b2707..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/filters/SecurityReviewFilter.tsx +++ /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 = { - 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 ( - - ); -} - -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 ( -
- - {labels[option]} -
- ); -} diff --git a/server/sonar-web/src/main/js/apps/projects/query.ts b/server/sonar-web/src/main/js/apps/projects/query.ts index 3ccf4a0d176..5dd5b964204 100644 --- a/server/sonar-web/src/main/js/apps/projects/query.ts +++ b/server/sonar-web/src/main/js/apps/projects/query.ts @@ -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 = { - 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])); } diff --git a/server/sonar-web/src/main/js/apps/projects/utils.ts b/server/sonar-web/src/main/js/apps/projects/utils.ts index 426e9195529..21ec392bfbe 100644 --- a/server/sonar-web/src/main/js/apps/projects/utils.ts +++ b/server/sonar-web/src/main/js/apps/projects/utils.ts @@ -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 = { +export const propertyToMetricMapLegacy: Dict = { analysis_date: 'analysisDate', reliability: 'reliability_rating', new_reliability: 'new_reliability_rating', @@ -277,13 +312,25 @@ const propertyToMetricMap: Dict = { creation_date: 'creationDate', }; -const metricToPropertyMap = invert(propertyToMetricMap); +export const propertyToMetricMap: Dict = { + ...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> = {}; 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; diff --git a/server/sonar-web/src/main/js/queries/settings.ts b/server/sonar-web/src/main/js/queries/settings.ts index f6534ee40b8..2648e452844 100644 --- a/server/sonar-web/src/main/js/queries/settings.ts +++ b/server/sonar-web/src/main/js/queries/settings.ts @@ -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' }, ); }; diff --git a/server/sonar-web/src/main/js/types/settings.ts b/server/sonar-web/src/main/js/types/settings.ts index 7722625a5ea..16f5083bd54 100644 --- a/server/sonar-web/src/main/js/types/settings.ts +++ b/server/sonar-web/src/main/js/types/settings.ts @@ -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 { diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index feb5feb18eb..31b034075ef 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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 -- 2.39.5