diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2021-12-21 09:46:41 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-12-24 20:02:59 +0000 |
commit | 2da4d0ec8c8f96a8309dce79eaf368ce187c0027 (patch) | |
tree | 9335cbd0dc0af31939cfeaecdc662df4936cc93f | |
parent | eb02c848b5abb29ef43ed4438205af46243faf3d (diff) | |
download | sonarqube-2da4d0ec8c8f96a8309dce79eaf368ce187c0027.tar.gz sonarqube-2da4d0ec8c8f96a8309dce79eaf368ce187c0027.zip |
SONAR-15790 Add new/overall code filter on portfolio's projects page
16 files changed, 516 insertions, 98 deletions
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 @@ -16,6 +16,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", "security_review_rating", @@ -27,6 +33,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", "security_rating", "security_review_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<Props, State> { +export class CodeApp extends React.Component<Props, State> { mounted = false; state: State; @@ -81,7 +87,8 @@ export class CodeApp extends React.PureComponent<Props, State> { 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<Props, State> { 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<Props, State> { components = [], highlighted, loading, + newCodeSelected, total, searchResults, sourceViewer @@ -266,6 +278,12 @@ export class CodeApp extends React.PureComponent<Props, State> { '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<Props, State> { <Search branchLike={branchLike} component={component} + newCodeSelected={newCodeSelected} + onNewCodeToggle={this.handleSelectNewCode} onSearchClear={this.handleSearchClear} onSearchResults={this.handleSearchResults} /> @@ -316,7 +336,7 @@ export class CodeApp extends React.PureComponent<Props, State> { 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<Props, State> { <Components branchLike={this.props.branchLike} components={searchResults} - metrics={{}} + metrics={[]} onHighlight={this.handleHighlight} onSelect={this.handleSelect} rootComponent={component} diff --git a/server/sonar-web/src/main/js/apps/code/components/Components.tsx b/server/sonar-web/src/main/js/apps/code/components/Components.tsx index a4ede240bac..7568a1ef14b 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Components.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Components.tsx @@ -17,12 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { intersection, sortBy } from 'lodash'; +import { sortBy } from 'lodash'; import * as React from 'react'; import withKeyboardNavigation from '../../../components/hoc/withKeyboardNavigation'; import { getComponentMeasureUniqueKey } from '../../../helpers/component'; import { BranchLike } from '../../../types/branch-like'; -import { getCodeMetrics } from '../utils'; import Component from './Component'; import ComponentsEmpty from './ComponentsEmpty'; import ComponentsHeader from './ComponentsHeader'; @@ -31,7 +30,7 @@ interface Props { baseComponent?: T.ComponentMeasure; branchLike?: BranchLike; components: T.ComponentMeasure[]; - metrics: T.Dict<T.Metric>; + metrics: T.Metric[]; rootComponent: T.ComponentMeasure; selected?: T.ComponentMeasure; } @@ -41,12 +40,8 @@ const BASE_COLUMN_COUNT = 4; export class Components extends React.PureComponent<Props> { 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<Props> { <ComponentsHeader baseComponent={baseComponent} canBePinned={canBePinned} - metrics={metricKeys} + metrics={this.props.metrics.map(metric => metric.key)} rootComponent={rootComponent} /> )} @@ -68,7 +63,7 @@ export class Components extends React.PureComponent<Props> { component={baseComponent} hasBaseComponent={false} key={baseComponent.key} - metrics={metrics} + metrics={this.props.metrics} rootComponent={rootComponent} /> <tr className="blank"> @@ -96,7 +91,7 @@ export class Components extends React.PureComponent<Props> { 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 ( + <div className="big-spacer-right"> + <div className="button-group"> + <Button + className={showNewCode ? 'button-active' : undefined} + onClick={() => props.onNewCodeToggle(true)}> + {translate('projects.view.new_code')} + </Button> + <Button + className={showNewCode ? undefined : 'button-active'} + onClick={() => props.onNewCodeToggle(false)}> + {translate('projects.view.overall_code')} + </Button> + </div> + </div> + ); +} 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<Props, State> { 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<Props, State> { }; render() { - const { component } = this.props; + const { component, newCodeSelected } = this.props; const { loading } = this.state; const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier); return ( <div className="code-search" id="code-search"> + {isPortfolio && ( + <PortfolioNewCodeToggle + onNewCodeToggle={this.props.onNewCodeToggle} + showNewCode={newCodeSelected} + /> + )} <SearchBox minLength={3} onChange={this.handleQueryChange} diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/CodeApp-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/CodeApp-test.tsx index 20645e522f2..0aab5241e01 100644 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/CodeApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/CodeApp-test.tsx @@ -27,17 +27,21 @@ import { ComponentQualifier } from '../../../../types/component'; import { loadMoreChildren, retrieveComponent } from '../../utils'; import { CodeApp } from '../CodeApp'; -jest.mock('../../utils', () => ({ - loadMoreChildren: jest.fn().mockResolvedValue({}), - retrieveComponent: jest.fn().mockResolvedValue({ - breadcrumbs: [], - component: { qualifier: 'APP' }, - components: [], - page: 0, - total: 1 - }), - retrieveComponentChildren: () => Promise.resolve() -})); +jest.mock('../../utils', () => { + const { getCodeMetrics } = jest.requireActual('../../utils'); + return { + getCodeMetrics, + loadMoreChildren: jest.fn().mockResolvedValue({}), + retrieveComponent: jest.fn().mockResolvedValue({ + breadcrumbs: [], + component: { qualifier: 'APP' }, + components: [], + page: 0, + total: 1 + }), + retrieveComponentChildren: () => Promise.resolve() + }; +}); const METRICS = { coverage: { id: '2', key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' }, @@ -155,6 +159,44 @@ it('should handle go to parent correctly', async () => { }); }); +it('should correcly display new/overall measure for portfolio', async () => { + const component1 = mockComponent({ qualifier: ComponentQualifier.Project }); + const metrics = { + reliability_rating: { + id: '2', + key: 'reliability_rating', + type: 'RATING', + name: 'reliability_rating', + domain: 'reliability_rating' + }, + new_reliability_rating: { + id: '4', + key: 'new_reliability_rating', + type: 'RATING', + name: 'new_reliability_rating', + domain: 'new_reliability_rating' + } + }; + (retrieveComponent as jest.Mock<any>).mockResolvedValueOnce({ + breadcrumbs: [], + component: mockComponent(), + components: [component1], + page: 0, + total: 1 + }); + + const wrapper = shallowRender({ + component: mockComponent({ qualifier: ComponentQualifier.Portfolio }), + metrics + }); + await waitAndUpdate(wrapper); + expect(wrapper.find('withKeyboardNavigation(Components)').props()).toMatchSnapshot('new metrics'); + wrapper.setState({ newCodeSelected: false }); + expect(wrapper.find('withKeyboardNavigation(Components)').props()).toMatchSnapshot( + 'overall metrics' + ); +}); + it('should handle select correctly', () => { const router = mockRouter(); const wrapper = shallowRender({ router }); diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/Components-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/Components-test.tsx index 82dbe8e22b8..8696f503464 100644 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/Components-test.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/Components-test.tsx @@ -30,7 +30,7 @@ const COMPONENT = { branch: 'develop' }; const PORTFOLIO = { key: 'bar', name: 'Bar', qualifier: ComponentQualifier.Portfolio }; -const METRICS = { coverage: { id: '1', key: 'coverage', type: 'PERCENT', name: 'Coverage' } }; +const METRICS = [{ id: '1', key: 'coverage', type: 'PERCENT', name: 'Coverage' }]; const BRANCH = mockBranch({ name: 'feature' }); it('renders correctly', () => { @@ -48,7 +48,7 @@ it('renders correctly', () => { it('renders correctly for a search', () => { expect( - shallow(<Components components={[COMPONENT]} metrics={{}} rootComponent={COMPONENT} />) + shallow(<Components components={[COMPONENT]} metrics={[]} rootComponent={COMPONENT} />) ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/PortfolioNewCodeToggle-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/PortfolioNewCodeToggle-test.tsx new file mode 100644 index 00000000000..7ac8ef235ad --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/PortfolioNewCodeToggle-test.tsx @@ -0,0 +1,51 @@ +/* + * 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 { shallow } from 'enzyme'; +import * as React from 'react'; +import { Button } from '../../../../components/controls/buttons'; +import PortfolioNewCodeToggle, { PortfolioNewCodeToggleProps } from '../PortfolioNewCodeToggle'; + +it('renders correctly', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it('should toggle correctly', () => { + const onNewCodeToggle = jest.fn(); + const wrapper = shallowRender({ onNewCodeToggle }); + wrapper + .find(Button) + .at(1) + .simulate('click'); + + expect(onNewCodeToggle).toBeCalledWith(false); + + wrapper + .find(Button) + .at(0) + .simulate('click'); + + expect(onNewCodeToggle).toBeCalledWith(true); +}); + +function shallowRender(props?: Partial<PortfolioNewCodeToggleProps>) { + return shallow( + <PortfolioNewCodeToggle showNewCode={true} onNewCodeToggle={jest.fn()} {...props} /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/Search-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/Search-test.tsx index 829fb65f7a4..e050a1d9092 100644 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/Search-test.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/Search-test.tsx @@ -23,6 +23,7 @@ import { getTree } from '../../../../api/components'; import { mockComponent } from '../../../../helpers/mocks/component'; import { mockLocation, mockRouter } from '../../../../helpers/testMocks'; import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { ComponentQualifier } from '../../../../types/component'; import { Search } from '../Search'; jest.mock('../../../../api/components', () => { @@ -41,6 +42,9 @@ jest.mock('../../../../api/components', () => { it('should render correcly', () => { expect(shallowRender()).toMatchSnapshot(); + expect( + shallowRender({ component: mockComponent({ qualifier: ComponentQualifier.Portfolio }) }) + ).toMatchSnapshot('node code toggle for portfolio'); }); it('should search correct query on mount', async () => { @@ -100,10 +104,12 @@ it('should handle search correctly', async () => { function shallowRender(props?: Partial<Search['props']>) { return shallow<Search>( <Search + newCodeSelected={false} component={mockComponent()} location={mockLocation()} onSearchClear={jest.fn()} onSearchResults={jest.fn()} + onNewCodeToggle={jest.fn()} router={mockRouter()} {...props} /> diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/CodeApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/CodeApp-test.tsx.snap index 233b60a1e9f..b45c7534519 100644 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/CodeApp-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/CodeApp-test.tsx.snap @@ -1,5 +1,171 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`should correcly display new/overall measure for portfolio: new metrics 1`] = ` +Object { + "baseComponent": Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + }, + "branchLike": undefined, + "components": Array [ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + }, + ], + "cycle": true, + "metrics": Array [ + Object { + "domain": "new_reliability_rating", + "id": "4", + "key": "new_reliability_rating", + "name": "new_reliability_rating", + "type": "RATING", + }, + ], + "onEndOfList": [Function], + "onGoToParent": [Function], + "onHighlight": [Function], + "onSelect": [Function], + "rootComponent": Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "VW", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + }, + "selected": undefined, +} +`; + +exports[`should correcly display new/overall measure for portfolio: overall metrics 1`] = ` +Object { + "baseComponent": Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + }, + "branchLike": undefined, + "components": Array [ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + }, + ], + "cycle": true, + "metrics": Array [ + Object { + "domain": "reliability_rating", + "id": "2", + "key": "reliability_rating", + "name": "reliability_rating", + "type": "RATING", + }, + ], + "onEndOfList": [Function], + "onGoToParent": [Function], + "onHighlight": [Function], + "onSelect": [Function], + "rootComponent": Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "VW", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + }, + "selected": undefined, +} +`; + exports[`should render correclty when no sub component for APP 1`] = ` <div className="page page-limited" @@ -55,6 +221,8 @@ exports[`should render correclty when no sub component for APP: no search 1`] = "qualifier": "APP", } } + newCodeSelected={true} + onNewCodeToggle={[Function]} onSearchClear={[Function]} onSearchResults={[Function]} /> @@ -66,7 +234,7 @@ exports[`should render correclty when no sub component for APP: no search 1`] = > <withKeyboardNavigation(Components) components={Array []} - metrics={Object {}} + metrics={Array []} onHighlight={[Function]} onSelect={[Function]} rootComponent={ @@ -107,6 +275,8 @@ exports[`should render correclty when no sub component for APP: with sub compone "qualifier": "APP", } } + newCodeSelected={true} + onNewCodeToggle={[Function]} onSearchClear={[Function]} onSearchResults={[Function]} /> @@ -151,22 +321,15 @@ exports[`should render correclty when no sub component for APP: with sub compone } cycle={true} metrics={ - Object { - "coverage": Object { + Array [ + Object { "domain": "Coverage", "id": "2", "key": "coverage", "name": "Coverage", "type": "PERCENT", }, - "new_bugs": Object { - "domain": "Reliability", - "id": "4", - "key": "new_bugs", - "name": "New Bugs", - "type": "INT", - }, - } + ] } onEndOfList={[Function]} onGoToParent={[Function]} @@ -246,6 +409,8 @@ exports[`should render correclty when no sub component for SVW: no search 1`] = "qualifier": "SVW", } } + newCodeSelected={true} + onNewCodeToggle={[Function]} onSearchClear={[Function]} onSearchResults={[Function]} /> @@ -257,7 +422,7 @@ exports[`should render correclty when no sub component for SVW: no search 1`] = > <withKeyboardNavigation(Components) components={Array []} - metrics={Object {}} + metrics={Array []} onHighlight={[Function]} onSelect={[Function]} rootComponent={ @@ -298,6 +463,8 @@ exports[`should render correclty when no sub component for SVW: with sub compone "qualifier": "SVW", } } + newCodeSelected={true} + onNewCodeToggle={[Function]} onSearchClear={[Function]} onSearchResults={[Function]} /> @@ -341,24 +508,7 @@ exports[`should render correclty when no sub component for SVW: with sub compone ] } cycle={true} - metrics={ - Object { - "coverage": Object { - "domain": "Coverage", - "id": "2", - "key": "coverage", - "name": "Coverage", - "type": "PERCENT", - }, - "new_bugs": Object { - "domain": "Reliability", - "id": "4", - "key": "new_bugs", - "name": "New Bugs", - "type": "INT", - }, - } - } + metrics={Array []} onEndOfList={[Function]} onGoToParent={[Function]} onHighlight={[Function]} @@ -437,6 +587,8 @@ exports[`should render correclty when no sub component for TRK: no search 1`] = "qualifier": "TRK", } } + newCodeSelected={true} + onNewCodeToggle={[Function]} onSearchClear={[Function]} onSearchResults={[Function]} /> @@ -448,7 +600,7 @@ exports[`should render correclty when no sub component for TRK: no search 1`] = > <withKeyboardNavigation(Components) components={Array []} - metrics={Object {}} + metrics={Array []} onHighlight={[Function]} onSelect={[Function]} rootComponent={ @@ -489,6 +641,8 @@ exports[`should render correclty when no sub component for TRK: with sub compone "qualifier": "TRK", } } + newCodeSelected={true} + onNewCodeToggle={[Function]} onSearchClear={[Function]} onSearchResults={[Function]} /> @@ -533,22 +687,15 @@ exports[`should render correclty when no sub component for TRK: with sub compone } cycle={true} metrics={ - Object { - "coverage": Object { + Array [ + Object { "domain": "Coverage", "id": "2", "key": "coverage", "name": "Coverage", "type": "PERCENT", }, - "new_bugs": Object { - "domain": "Reliability", - "id": "4", - "key": "new_bugs", - "name": "New Bugs", - "type": "INT", - }, - } + ] } onEndOfList={[Function]} onGoToParent={[Function]} @@ -628,6 +775,8 @@ exports[`should render correclty when no sub component for VW: no search 1`] = ` "qualifier": "VW", } } + newCodeSelected={true} + onNewCodeToggle={[Function]} onSearchClear={[Function]} onSearchResults={[Function]} /> @@ -639,7 +788,7 @@ exports[`should render correclty when no sub component for VW: no search 1`] = ` > <withKeyboardNavigation(Components) components={Array []} - metrics={Object {}} + metrics={Array []} onHighlight={[Function]} onSelect={[Function]} rootComponent={ @@ -680,6 +829,8 @@ exports[`should render correclty when no sub component for VW: with sub componen "qualifier": "VW", } } + newCodeSelected={true} + onNewCodeToggle={[Function]} onSearchClear={[Function]} onSearchResults={[Function]} /> @@ -723,24 +874,7 @@ exports[`should render correclty when no sub component for VW: with sub componen ] } cycle={true} - metrics={ - Object { - "coverage": Object { - "domain": "Coverage", - "id": "2", - "key": "coverage", - "name": "Coverage", - "type": "PERCENT", - }, - "new_bugs": Object { - "domain": "Reliability", - "id": "4", - "key": "new_bugs", - "name": "New Bugs", - "type": "INT", - }, - } - } + metrics={Array []} onEndOfList={[Function]} onGoToParent={[Function]} onHighlight={[Function]} diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/PortfolioNewCodeToggle-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/PortfolioNewCodeToggle-test.tsx.snap new file mode 100644 index 00000000000..0a91ab15adf --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/PortfolioNewCodeToggle-test.tsx.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<div + className="big-spacer-right" +> + <div + className="button-group" + > + <Button + className="button-active" + onClick={[Function]} + > + projects.view.new_code + </Button> + <Button + onClick={[Function]} + > + projects.view.overall_code + </Button> + </div> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Search-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Search-test.tsx.snap index a1d47cde013..5ca52b0c451 100644 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Search-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Search-test.tsx.snap @@ -18,3 +18,26 @@ exports[`should render correcly 1`] = ` /> </div> `; + +exports[`should render correcly: node code toggle for portfolio 1`] = ` +<div + className="code-search" + id="code-search" +> + <PortfolioNewCodeToggle + onNewCodeToggle={[MockFunction]} + showNewCode={false} + /> + <SearchBox + minLength={3} + onChange={[Function]} + onKeyDown={[Function]} + placeholder="code.search_placeholder.portfolio" + value="" + /> + <DeferredSpinner + className="spacer-left" + loading={false} + /> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/code/utils.ts b/server/sonar-web/src/main/js/apps/code/utils.ts index f9c2a59eda5..3f957269065 100644 --- a/server/sonar-web/src/main/js/apps/code/utils.ts +++ b/server/sonar-web/src/main/js/apps/code/utils.ts @@ -20,6 +20,7 @@ import { getBreadcrumbs, getChildren, getComponent } from '../../api/components'; import { getBranchLikeQuery, isPullRequest } from '../../helpers/branch-like'; import { BranchLike } from '../../types/branch-like'; +import { isPortfolioLike } from '../../types/component'; import { MetricKey } from '../../types/metrics'; import { addComponent, @@ -51,6 +52,15 @@ const PORTFOLIO_METRICS = [ MetricKey.ncloc ]; +const NEW_PORTFOLIO_METRICS = [ + MetricKey.releasability_rating, + MetricKey.new_reliability_rating, + MetricKey.new_security_rating, + MetricKey.new_security_review_rating, + MetricKey.new_maintainability_rating, + MetricKey.new_lines +]; + const LEAK_METRICS = [ MetricKey.new_lines, MetricKey.bugs, @@ -104,10 +114,17 @@ function storeChildrenBreadcrumbs(parentComponentKey: string, children: T.Breadc export function getCodeMetrics( qualifier: string, branchLike?: BranchLike, - options: { includeQGStatus?: boolean } = {} + options: { includeQGStatus?: boolean; newCode?: boolean } = {} ) { - if (['VW', 'SVW'].includes(qualifier)) { - const metrics = [...PORTFOLIO_METRICS]; + if (isPortfolioLike(qualifier)) { + let metrics: MetricKey[] = []; + if (options?.newCode === undefined) { + metrics = [...NEW_PORTFOLIO_METRICS, ...PORTFOLIO_METRICS]; + } else if (options?.newCode) { + metrics = [...NEW_PORTFOLIO_METRICS]; + } else { + metrics = [...PORTFOLIO_METRICS]; + } return options.includeQGStatus ? metrics.concat(MetricKey.alert_status) : metrics; } if (qualifier === 'APP') { @@ -159,7 +176,9 @@ export function retrieveComponentChildren( }); } - const metrics = getCodeMetrics(qualifier, branchLike, { includeQGStatus: true }); + const metrics = getCodeMetrics(qualifier, branchLike, { + includeQGStatus: true + }); return getChildren(componentKey, metrics, { ps: PAGE_SIZE, @@ -231,7 +250,9 @@ export function loadMoreChildren( instance: { mounted: boolean }, branchLike?: BranchLike ): Promise<Children> { - const metrics = getCodeMetrics(qualifier, branchLike, { includeQGStatus: true }); + const metrics = getCodeMetrics(qualifier, branchLike, { + includeQGStatus: true + }); return getChildren(componentKey, metrics, { ps: PAGE_SIZE, 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 28a3afe520e..24e273ea316 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1046,6 +1046,7 @@ projects.sorting.new_coverage=Coverage projects.sorting.new_duplications=Duplications projects.sorting.new_lines=New Lines projects.view.overall=Overall Status +projects.view.overall_code=Overall Code projects.view.new_code=New Code projects.worse_of_reliablity_and_security=Worse of Reliability and Security projects.visualization.risk=Risk |