From 2da4d0ec8c8f96a8309dce79eaf368ce187c0027 Mon Sep 17 00:00:00 2001 From: Mathieu Suen Date: Tue, 21 Dec 2021 09:46:41 +0100 Subject: SONAR-15790 Add new/overall code filter on portfolio's projects page --- .../__tests__/__snapshots__/utils-test.tsx.snap | 36 +++ .../src/main/js/apps/code/__tests__/utils-test.tsx | 6 + server/sonar-web/src/main/js/apps/code/code.css | 1 + .../src/main/js/apps/code/components/CodeApp.tsx | 32 ++- .../main/js/apps/code/components/Components.tsx | 19 +- .../code/components/PortfolioNewCodeToggle.tsx | 47 ++++ .../src/main/js/apps/code/components/Search.tsx | 16 +- .../code/components/__tests__/CodeApp-test.tsx | 64 +++++- .../code/components/__tests__/Components-test.tsx | 4 +- .../__tests__/PortfolioNewCodeToggle-test.tsx | 51 +++++ .../apps/code/components/__tests__/Search-test.tsx | 6 + .../__tests__/__snapshots__/CodeApp-test.tsx.snap | 254 ++++++++++++++++----- .../PortfolioNewCodeToggle-test.tsx.snap | 23 ++ .../__tests__/__snapshots__/Search-test.tsx.snap | 23 ++ server/sonar-web/src/main/js/apps/code/utils.ts | 31 ++- .../main/resources/org/sonar/l10n/core.properties | 1 + 16 files changed, 516 insertions(+), 98 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/code/components/PortfolioNewCodeToggle.tsx create mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/PortfolioNewCodeToggle-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/PortfolioNewCodeToggle-test.tsx.snap diff --git a/server/sonar-web/src/main/js/apps/code/__tests__/__snapshots__/utils-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/__tests__/__snapshots__/utils-test.tsx.snap index 208dae9fd4a..fe6727e859a 100644 --- a/server/sonar-web/src/main/js/apps/code/__tests__/__snapshots__/utils-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/code/__tests__/__snapshots__/utils-test.tsx.snap @@ -15,6 +15,12 @@ Array [ exports[`getCodeMetrics should return the right metrics for portfolios 1`] = ` Array [ + "releasability_rating", + "new_reliability_rating", + "new_security_rating", + "new_security_review_rating", + "new_maintainability_rating", + "new_lines", "releasability_rating", "reliability_rating", "security_rating", @@ -25,6 +31,36 @@ Array [ `; exports[`getCodeMetrics should return the right metrics for portfolios 2`] = ` +Array [ + "releasability_rating", + "new_reliability_rating", + "new_security_rating", + "new_security_review_rating", + "new_maintainability_rating", + "new_lines", + "releasability_rating", + "reliability_rating", + "security_rating", + "security_review_rating", + "sqale_rating", + "ncloc", + "alert_status", +] +`; + +exports[`getCodeMetrics should return the right metrics for portfolios 3`] = ` +Array [ + "releasability_rating", + "new_reliability_rating", + "new_security_rating", + "new_security_review_rating", + "new_maintainability_rating", + "new_lines", + "alert_status", +] +`; + +exports[`getCodeMetrics should return the right metrics for portfolios 4`] = ` Array [ "releasability_rating", "reliability_rating", diff --git a/server/sonar-web/src/main/js/apps/code/__tests__/utils-test.tsx b/server/sonar-web/src/main/js/apps/code/__tests__/utils-test.tsx index e86f41b17c5..1244b91850f 100644 --- a/server/sonar-web/src/main/js/apps/code/__tests__/utils-test.tsx +++ b/server/sonar-web/src/main/js/apps/code/__tests__/utils-test.tsx @@ -55,6 +55,12 @@ describe('getCodeMetrics', () => { it('should return the right metrics for portfolios', () => { expect(getCodeMetrics('VW')).toMatchSnapshot(); expect(getCodeMetrics('VW', undefined, { includeQGStatus: true })).toMatchSnapshot(); + expect( + getCodeMetrics('VW', undefined, { includeQGStatus: true, newCode: true }) + ).toMatchSnapshot(); + expect( + getCodeMetrics('VW', undefined, { includeQGStatus: true, newCode: false }) + ).toMatchSnapshot(); }); it('should return the right metrics for apps', () => { diff --git a/server/sonar-web/src/main/js/apps/code/code.css b/server/sonar-web/src/main/js/apps/code/code.css index 39d4ff9255b..38a25ba3031 100644 --- a/server/sonar-web/src/main/js/apps/code/code.css +++ b/server/sonar-web/src/main/js/apps/code/code.css @@ -76,6 +76,7 @@ .code-search { margin-bottom: 10px; + display: flex; } .code-components-header { diff --git a/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx b/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx index 271be298ac2..50869e78662 100644 --- a/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx @@ -19,7 +19,7 @@ */ import classNames from 'classnames'; import { Location } from 'history'; -import { debounce } from 'lodash'; +import { debounce, intersection } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import { connect } from 'react-redux'; @@ -35,7 +35,12 @@ import { getMetrics } from '../../../store/rootReducer'; import { BranchLike } from '../../../types/branch-like'; import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket'; import '../code.css'; -import { loadMoreChildren, retrieveComponent, retrieveComponentChildren } from '../utils'; +import { + getCodeMetrics, + loadMoreChildren, + retrieveComponent, + retrieveComponentChildren +} from '../utils'; import Breadcrumbs from './Breadcrumbs'; import Components from './Components'; import Search from './Search'; @@ -69,9 +74,10 @@ interface State { searchResults?: T.ComponentMeasure[]; sourceViewer?: T.ComponentMeasure; total: number; + newCodeSelected: boolean; } -export class CodeApp extends React.PureComponent { +export class CodeApp extends React.Component { mounted = false; state: State; @@ -81,7 +87,8 @@ export class CodeApp extends React.PureComponent { breadcrumbs: [], loading: true, page: 0, - total: 0 + total: 0, + newCodeSelected: true }; this.refreshBranchStatus = debounce(this.refreshBranchStatus, 1000); } @@ -225,6 +232,10 @@ export class CodeApp extends React.PureComponent { this.setState({ highlighted: undefined }); }; + handleSelectNewCode = (newCodeSelected: boolean) => { + this.setState({ newCodeSelected }); + }; + handleUpdate = () => { const { component, location } = this.props; const { selected } = location.query; @@ -248,6 +259,7 @@ export class CodeApp extends React.PureComponent { components = [], highlighted, loading, + newCodeSelected, total, searchResults, sourceViewer @@ -266,6 +278,12 @@ export class CodeApp extends React.PureComponent { 'search-results': showSearch }); + const metricKeys = intersection( + getCodeMetrics(component.qualifier, branchLike, { newCode: newCodeSelected }), + Object.keys(this.props.metrics) + ); + const metrics = metricKeys.map(metric => this.props.metrics[metric]); + const defaultTitle = baseComponent && ['APP', 'VW', 'SVW'].includes(baseComponent.qualifier) ? translate('projects.page') @@ -284,6 +302,8 @@ export class CodeApp extends React.PureComponent { @@ -316,7 +336,7 @@ export class CodeApp extends React.PureComponent { branchLike={branchLike} components={components} cycle={true} - metrics={this.props.metrics} + metrics={metrics} onEndOfList={this.handleLoadMore} onGoToParent={this.handleGoToParent} onHighlight={this.handleHighlight} @@ -334,7 +354,7 @@ export class CodeApp extends React.PureComponent { ; + metrics: T.Metric[]; rootComponent: T.ComponentMeasure; selected?: T.ComponentMeasure; } @@ -41,12 +40,8 @@ const BASE_COLUMN_COUNT = 4; export class Components extends React.PureComponent { render() { const { baseComponent, branchLike, components, rootComponent, selected } = this.props; - const metricKeys = intersection( - getCodeMetrics(rootComponent.qualifier, branchLike), - Object.keys(this.props.metrics) - ); - const metrics = metricKeys.map(metric => this.props.metrics[metric]); - const colSpan = metrics.length + BASE_COLUMN_COUNT; + + const colSpan = this.props.metrics.length + BASE_COLUMN_COUNT; const canBePinned = baseComponent && !['APP', 'VW', 'SVW'].includes(baseComponent.qualifier); return ( @@ -55,7 +50,7 @@ export class Components extends React.PureComponent { metric.key)} rootComponent={rootComponent} /> )} @@ -68,7 +63,7 @@ export class Components extends React.PureComponent { component={baseComponent} hasBaseComponent={false} key={baseComponent.key} - metrics={metrics} + metrics={this.props.metrics} rootComponent={rootComponent} /> @@ -96,7 +91,7 @@ export class Components extends React.PureComponent { component={component} hasBaseComponent={baseComponent !== undefined} key={getComponentMeasureUniqueKey(component)} - metrics={metrics} + metrics={this.props.metrics} previous={index > 0 ? list[index - 1] : undefined} rootComponent={rootComponent} selected={ diff --git a/server/sonar-web/src/main/js/apps/code/components/PortfolioNewCodeToggle.tsx b/server/sonar-web/src/main/js/apps/code/components/PortfolioNewCodeToggle.tsx new file mode 100644 index 00000000000..b69acfffd56 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/PortfolioNewCodeToggle.tsx @@ -0,0 +1,47 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { Button } from '../../../components/controls/buttons'; +import { translate } from '../../../helpers/l10n'; + +export interface PortfolioNewCodeToggleProps { + showNewCode: boolean; + onNewCodeToggle: (newSelected: boolean) => void; +} + +export default function PortfolioNewCodeToggle(props: PortfolioNewCodeToggleProps) { + const { showNewCode } = props; + return ( +
+
+ + +
+
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/code/components/Search.tsx b/server/sonar-web/src/main/js/apps/code/components/Search.tsx index a680e771c18..90d9fbb8c83 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Search.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Search.tsx @@ -26,12 +26,15 @@ import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { BranchLike } from '../../../types/branch-like'; +import PortfolioNewCodeToggle from './PortfolioNewCodeToggle'; interface Props { branchLike?: BranchLike; component: T.ComponentMeasure; location: Location; + newCodeSelected: boolean; onSearchClear: () => void; + onNewCodeToggle: (newCode: boolean) => void; onSearchResults: (results?: T.ComponentMeasure[]) => void; router: Router; } @@ -85,7 +88,10 @@ export class Search extends React.PureComponent { if (this.mounted) { const { branchLike, component, router, location } = this.props; this.setState({ loading: true }); - router.replace({ pathname: location.pathname, query: { ...location.query, search: query } }); + router.replace({ + pathname: location.pathname, + query: { ...location.query, search: query } + }); const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier); const qualifiers = isPortfolio ? 'SVW,TRK' : 'BRC,UTS,FIL'; @@ -125,12 +131,18 @@ export class Search extends React.PureComponent { }; render() { - const { component } = this.props; + const { component, newCodeSelected } = this.props; const { loading } = this.state; const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier); return (