aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMathieu Suen <mathieu.suen@sonarsource.com>2021-12-21 09:46:41 +0100
committersonartech <sonartech@sonarsource.com>2021-12-24 20:02:59 +0000
commit2da4d0ec8c8f96a8309dce79eaf368ce187c0027 (patch)
tree9335cbd0dc0af31939cfeaecdc662df4936cc93f
parenteb02c848b5abb29ef43ed4438205af46243faf3d (diff)
downloadsonarqube-2da4d0ec8c8f96a8309dce79eaf368ce187c0027.tar.gz
sonarqube-2da4d0ec8c8f96a8309dce79eaf368ce187c0027.zip
SONAR-15790 Add new/overall code filter on portfolio's projects page
-rw-r--r--server/sonar-web/src/main/js/apps/code/__tests__/__snapshots__/utils-test.tsx.snap36
-rw-r--r--server/sonar-web/src/main/js/apps/code/__tests__/utils-test.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/code/code.css1
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx32
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Components.tsx19
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/PortfolioNewCodeToggle.tsx47
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Search.tsx16
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/CodeApp-test.tsx64
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/Components-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/PortfolioNewCodeToggle-test.tsx51
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/Search-test.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/CodeApp-test.tsx.snap254
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/PortfolioNewCodeToggle-test.tsx.snap23
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Search-test.tsx.snap23
-rw-r--r--server/sonar-web/src/main/js/apps/code/utils.ts31
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties1
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