From e03b2bf40be7de4821cdf009c74dffc90b87757b Mon Sep 17 00:00:00 2001 From: Philippe Perrin Date: Wed, 20 Oct 2021 10:48:08 +0200 Subject: [PATCH] SONAR-15498 Manual selection of project's branches for portfolio Display portfolio's children branch information and group issues by project and branch --- .../components/extensions/exposeLibraries.ts | 4 +- .../main/js/apps/code/components/CodeApp.tsx | 2 +- .../js/apps/code/components/ComponentName.tsx | 23 +- .../js/apps/code/components/Components.tsx | 17 +- .../__tests__/ComponentName-test.tsx | 17 +- .../components/__tests__/Components-test.tsx | 10 +- .../__snapshots__/ComponentName-test.tsx.snap | 104 +++--- .../__snapshots__/Components-test.tsx.snap | 20 +- .../__tests__/utils-test.ts | 28 -- .../components/Breadcrumb.tsx | 4 +- .../components/Breadcrumbs.tsx | 4 +- .../components/MeasureContent.tsx | 87 +++-- .../components/MeasureOverview.tsx | 15 +- .../components/MeasureOverviewContainer.tsx | 11 +- .../drilldown/BubbleChart.tsx | 15 +- .../drilldown/ComponentCell.tsx | 59 ++-- .../drilldown/ComponentsList.tsx | 10 +- .../drilldown/FilesView.tsx | 27 +- .../drilldown/TreeMapView.tsx | 18 +- .../__tests__/ComponentCell-test.tsx | 186 ++++++---- .../__snapshots__/ComponentCell-test.tsx.snap | 231 +++--------- .../main/js/apps/component-measures/utils.ts | 14 - .../components/ComponentBreadcrumbs.tsx | 26 +- .../js/apps/issues/components/ListItem.tsx | 5 +- .../__tests__/ComponentBreadcrumbs-test.tsx | 3 +- .../components/__tests__/ListItem-test.tsx | 51 +++ .../ComponentBreadcrumbs-test.tsx.snap | 14 +- .../__snapshots__/ListItem-test.tsx.snap | 115 ++++++ .../js/apps/portfolio/components/Effort.tsx | 6 +- .../apps/portfolio/components/MetricBox.tsx | 8 +- .../portfolio/components/WorstProjects.tsx | 59 +++- .../__tests__/WorstProjects-test.tsx | 33 +- .../__snapshots__/Effort-test.tsx.snap | 4 +- .../__snapshots__/MetricBox-test.tsx.snap | 10 +- .../__snapshots__/WorstProjects-test.tsx.snap | 328 +++++++++++++++--- .../src/main/js/apps/portfolio/types.ts | 1 + .../src/main/js/components/charts/TreeMap.tsx | 11 +- .../charts/__tests__/TreeMap-test.tsx | 22 +- .../controls/SelectListListElement.tsx | 13 +- .../SelectListListElement-test.tsx.snap | 8 +- .../components/hoc/withKeyboardNavigation.tsx | 8 +- .../src/main/js/helpers/component.ts | 25 ++ .../__snapshots__/component-test.ts.snap | 71 ++++ .../main/js/types/__tests__/component-test.ts | 39 +++ .../sonar-web/src/main/js/types/component.ts | 14 + server/sonar-web/src/main/js/types/types.d.ts | 2 +- .../resources/org/sonar/l10n/core.properties | 13 +- .../org/sonar/api/measures/CoreMetrics.java | 4 +- 48 files changed, 1211 insertions(+), 588 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/ListItem-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/helpers/component.ts create mode 100644 server/sonar-web/src/main/js/types/__tests__/__snapshots__/component-test.ts.snap create mode 100644 server/sonar-web/src/main/js/types/__tests__/component-test.ts diff --git a/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts b/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts index cdbf12adf21..efa37b2a77a 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts +++ b/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts @@ -75,7 +75,8 @@ import { getBranchLikeQuery, isBranch, isMainBranch, - isPullRequest + isPullRequest, + sortBranches } from '../../../helpers/branch-like'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import * as measures from '../../../helpers/measures'; @@ -138,6 +139,7 @@ const exposeLibraries = () => { isBranch, isMainBranch, isPullRequest, + sortBranches, getStandards, renderCWECategory, renderOwaspTop10Category, 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 9bb59ded960..271be298ac2 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 @@ -217,7 +217,7 @@ export class CodeApp extends React.PureComponent { const { branchLike, component: rootComponent } = this.props; if (component.refKey) { - this.props.router.push(getProjectUrl(component.refKey)); + this.props.router.push(getProjectUrl(component.refKey, component.branch)); } else { this.props.router.push(getCodeUrl(rootComponent.key, branchLike, component.key)); } diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx index 495009f47c0..0d60199053d 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx @@ -26,14 +26,16 @@ import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { getProjectUrl } from '../../../helpers/urls'; import { BranchLike } from '../../../types/branch-like'; +import { ComponentQualifier } from '../../../types/component'; export function getTooltip(component: T.ComponentMeasure) { const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS'; + if (isFile && component.path) { return component.path + '\n\n' + component.key; - } else { - return component.name + '\n\n' + component.key; } + + return [component.name, component.key, component.branch].filter(s => !!s).join('\n\n'); } export function mostCommonPrefix(strings: string[]) { @@ -82,8 +84,12 @@ export default function ComponentName({ let inner = null; - if (component.refKey && component.qualifier !== 'SVW') { - const branch = rootComponent.qualifier === 'APP' ? component.branch : undefined; + if (component.refKey && component.qualifier !== ComponentQualifier.SubPortfolio) { + const branch = [ComponentQualifier.Application, ComponentQualifier.Portfolio].includes( + rootComponent.qualifier as ComponentQualifier + ) + ? component.branch + : undefined; inner = ( {name} @@ -107,7 +113,14 @@ export default function ComponentName({ ); } - if (rootComponent.qualifier === 'APP') { + if ( + [ComponentQualifier.Application, ComponentQualifier.Portfolio].includes( + rootComponent.qualifier as ComponentQualifier + ) && + [ComponentQualifier.Application, ComponentQualifier.Project].includes( + component.qualifier as ComponentQualifier + ) + ) { return ( 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 5191e9d484a..a4ede240bac 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,9 +17,10 @@ * 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 } from 'lodash'; +import { intersection, 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'; @@ -82,18 +83,26 @@ export class Components extends React.PureComponent { )} {components.length ? ( - components.map((component, index, list) => ( + sortBy( + components, + c => c.qualifier, + c => c.name.toLowerCase(), + c => c.branch?.toLowerCase() + ).map((component, index, list) => ( 0 ? list[index - 1] : undefined} rootComponent={rootComponent} - selected={selected && component.key === selected.key} + selected={ + selected && + getComponentMeasureUniqueKey(component) === getComponentMeasureUniqueKey(selected) + } /> )) ) : ( diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentName-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentName-test.tsx index 049c3ccc248..17d0976813f 100644 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentName-test.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentName-test.tsx @@ -21,6 +21,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { mockMainBranch } from '../../../../helpers/mocks/branch-like'; import { mockComponentMeasure } from '../../../../helpers/mocks/component'; +import { ComponentQualifier } from '../../../../types/component'; import ComponentName, { getTooltip, mostCommonPrefix, Props } from '../ComponentName'; describe('#getTooltip', () => { @@ -79,7 +80,7 @@ describe('#ComponentName', () => { component: mockComponentMeasure(false, { branch: 'foo', refKey: 'src/main/ts/app', - qualifier: 'TRK' + qualifier: ComponentQualifier.Project }) }) ).toMatchSnapshot(); @@ -88,9 +89,19 @@ describe('#ComponentName', () => { component: mockComponentMeasure(false, { branch: 'foo', refKey: 'src/main/ts/app', - qualifier: 'TRK' + qualifier: ComponentQualifier.Project }), - rootComponent: mockComponentMeasure(false, { qualifier: 'APP' }) + rootComponent: mockComponentMeasure(false, { qualifier: ComponentQualifier.Application }) + }) + ).toMatchSnapshot(); + + expect( + shallowRender({ + component: mockComponentMeasure(false, { + refKey: 'src/main/ts/app', + qualifier: ComponentQualifier.Project + }), + rootComponent: mockComponentMeasure(false, { qualifier: ComponentQualifier.Portfolio }) }) ).toMatchSnapshot(); }); 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 8d2565f925a..82dbe8e22b8 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 @@ -20,10 +20,16 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { mockBranch } from '../../../../helpers/mocks/branch-like'; +import { ComponentQualifier } from '../../../../types/component'; import { Components } from '../Components'; -const COMPONENT = { key: 'foo', name: 'Foo', qualifier: 'TRK' }; -const PORTFOLIO = { key: 'bar', name: 'Bar', qualifier: 'VW' }; +const COMPONENT = { + key: 'foo', + name: 'Foo', + qualifier: ComponentQualifier.Project, + branch: 'develop' +}; +const PORTFOLIO = { key: 'bar', name: 'Bar', qualifier: ComponentQualifier.Portfolio }; const METRICS = { coverage: { id: '1', key: 'coverage', type: 'PERCENT', name: 'Coverage' } }; const BRANCH = mockBranch({ name: 'feature' }); diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap index 8922328f440..fe5e63e44a1 100644 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap @@ -115,59 +115,34 @@ foo:src/index.tsx" exports[`#ComponentName should render correctly for files 4`] = ` - - - - - index.tsx - - - - branches.main_branch +> + + + + index.tsx `; exports[`#ComponentName should render correctly for files 5`] = ` - - - - - index.tsx - - - - + + - - foo - + + index.tsx `; @@ -177,6 +152,8 @@ exports[`#ComponentName should render correctly for refs 1`] = ` className="max-width-100 display-inline-block text-ellipsis" title="Foo +foo + foo" > `; +exports[`#ComponentName should render correctly for refs 3`] = ` + + + + + + + Foo + + + + + branches.main_branch + + +`; + exports[`#getTooltip should correctly format component information 1`] = ` "src/index.tsx diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Components-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Components-test.tsx.snap index 3a2adbdcace..5ed9ee9a737 100644 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Components-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Components-test.tsx.snap @@ -7,6 +7,7 @@ exports[`renders correctly 1`] = ` { }); }); }); - -describe('Component classification', () => { - const componentBuilder = (qual: ComponentQualifier): T.ComponentMeasure => { - return { - qualifier: qual, - key: '1', - name: 'TEST' - }; - }; - - it('should be file type', () => { - [ComponentQualifier.File, ComponentQualifier.TestFile].forEach(qual => { - const component = componentBuilder(qual); - expect(utils.isFileType(component)).toBe(true); - }); - }); - - it('should be view type', () => { - [ - ComponentQualifier.Portfolio, - ComponentQualifier.SubPortfolio, - ComponentQualifier.Application - ].forEach(qual => { - const component = componentBuilder(qual); - expect(utils.isViewType(component)).toBe(true); - }); - }); -}); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.tsx index 21e4aa10259..72e4d154497 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.tsx @@ -25,14 +25,14 @@ interface Props { canBrowse: boolean; component: T.ComponentMeasure; isLast: boolean; - handleSelect: (component: string) => void; + handleSelect: (component: T.ComponentMeasureIntern) => void; } export default class Breadcrumb extends React.PureComponent { handleClick = (event: React.MouseEvent) => { event.preventDefault(); event.currentTarget.blur(); - this.props.handleSelect(this.props.component.key); + this.props.handleSelect(this.props.component); }; render() { diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.tsx index bdb3214d5fb..aa2a001df31 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.tsx @@ -29,7 +29,7 @@ interface Props { branchLike?: BranchLike; className?: string; component: T.ComponentMeasure; - handleSelect: (component: string) => void; + handleSelect: (component: T.ComponentMeasureIntern) => void; rootComponent: T.ComponentMeasure; } @@ -66,7 +66,7 @@ export default class Breadcrumbs extends React.PureComponent { const { breadcrumbs } = this.state; if (breadcrumbs.length > 1) { const idx = this.props.backToFirst ? 0 : breadcrumbs.length - 2; - this.props.handleSelect(breadcrumbs[idx].key); + this.props.handleSelect(breadcrumbs[idx]); } return false; }); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx index c5c6b915305..268241477f9 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx @@ -25,18 +25,20 @@ import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget'; import SourceViewer from '../../../components/SourceViewer/SourceViewer'; import PageActions from '../../../components/ui/PageActions'; import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; +import { getComponentMeasureUniqueKey } from '../../../helpers/component'; import { translate } from '../../../helpers/l10n'; import { isDiffMetric } from '../../../helpers/measures'; import { RequestData } from '../../../helpers/request'; import { scrollToElement } from '../../../helpers/scrolling'; import { getProjectUrl } from '../../../helpers/urls'; import { BranchLike } from '../../../types/branch-like'; +import { isFile, isView } from '../../../types/component'; import { MeasurePageView } from '../../../types/measures'; import { MetricKey } from '../../../types/metrics'; import { complementary } from '../config/complementary'; import FilesView from '../drilldown/FilesView'; import TreeMapView from '../drilldown/TreeMapView'; -import { enhanceComponent, isFileType, isViewType, Query } from '../utils'; +import { enhanceComponent, Query } from '../utils'; import Breadcrumbs from './Breadcrumbs'; import MeasureContentHeader from './MeasureContentHeader'; import MeasureHeader from './MeasureHeader'; @@ -63,7 +65,7 @@ interface State { metric?: T.Metric; paging?: T.Paging; secondaryMeasure?: T.Measure; - selected?: string; + selectedComponent?: T.ComponentMeasureIntern; } export default class MeasureContent extends React.PureComponent { @@ -125,15 +127,21 @@ export default class MeasureContent extends React.PureComponent { measure => measure.metric !== this.props.requestedMetric.key ); - this.setState(({ selected }) => ({ + this.setState(({ selectedComponent }) => ({ baseComponent: tree.baseComponent, components, measure, metric, paging: tree.paging, secondaryMeasure, - selected: - components.length > 0 && components.find(c => c.key === selected) ? selected : undefined + selectedComponent: + components.length > 0 && + components.find( + c => + getComponentMeasureUniqueKey(c) === getComponentMeasureUniqueKey(selectedComponent) + ) + ? selectedComponent + : undefined })); } }); @@ -223,34 +231,39 @@ export default class MeasureContent extends React.PureComponent { this.props.updateQuery({ view }); }; - onOpenComponent = (componentKey: string) => { - if (isViewType(this.props.rootComponent)) { - const component = this.state.components.find( - component => component.refKey === componentKey || component.key === componentKey + onOpenComponent = (component: T.ComponentMeasureIntern) => { + if (isView(this.props.rootComponent.qualifier)) { + const comp = this.state.components.find( + c => + c.refKey === component.key || + getComponentMeasureUniqueKey(c) === getComponentMeasureUniqueKey(component) ); - if (component && component.refKey !== undefined) { - if (this.props.view === 'treemap') { - this.props.router.push(getProjectUrl(componentKey)); - } - return; + + if (comp) { + this.props.router.push(getProjectUrl(comp.refKey || comp.key, component.branch)); } + + return; } - this.setState(state => ({ selected: state.baseComponent!.key })); - this.updateSelected(componentKey); + + this.setState(state => ({ selectedComponent: state.baseComponent })); + this.updateSelected(component.key); if (this.container) { this.container.focus(); } }; - onSelectComponent = (componentKey: string) => { - this.setState({ selected: componentKey }); + onSelectComponent = (component: T.ComponentMeasureIntern) => { + this.setState({ selectedComponent: component }); }; getSelectedIndex = () => { - const componentKey = isFileType(this.state.baseComponent!) - ? this.state.baseComponent!.key - : this.state.selected; - const index = this.state.components.findIndex(component => component.key === componentKey); + const componentKey = isFile(this.state.baseComponent?.qualifier) + ? getComponentMeasureUniqueKey(this.state.baseComponent) + : getComponentMeasureUniqueKey(this.state.selectedComponent); + const index = this.state.components.findIndex( + component => getComponentMeasureUniqueKey(component) === componentKey + ); return index !== -1 ? index : undefined; }; @@ -281,20 +294,24 @@ export default class MeasureContent extends React.PureComponent { paging={this.state.paging} rootComponent={this.props.rootComponent} selectedIdx={selectedIdx} - selectedKey={selectedIdx !== undefined ? this.state.selected : undefined} + selectedComponent={ + selectedIdx !== undefined + ? (this.state.selectedComponent as T.ComponentMeasureEnhanced) + : undefined + } view={view} /> ); - } else { - return ( - - ); } + + return ( + + ); } render() { @@ -307,7 +324,7 @@ export default class MeasureContent extends React.PureComponent { const measureValue = measure && (isDiffMetric(measure.metric) ? measure.period?.value : measure.value); - const isFile = isFileType(baseComponent); + const isFileComponent = isFile(baseComponent.qualifier); const selectedIdx = this.getSelectedIndex(); return ( @@ -330,7 +347,7 @@ export default class MeasureContent extends React.PureComponent { } right={
- {!isFile && metric && ( + {!isFileComponent && metric && ( <>
{translate('component_measures.view_as')}
{ metric={metric} secondaryMeasure={secondaryMeasure} /> - {isFile ? ( + {isFileComponent ? (
void; rootComponent: T.ComponentMeasure; updateLoading: (param: T.Dict) => void; - updateSelected: (component: string) => void; + updateSelected: (component: T.ComponentMeasureIntern) => void; } interface State { @@ -82,7 +77,7 @@ export default class MeasureOverview extends React.PureComponent { fetchComponents = () => { const { branchLike, component, domain, metrics } = this.props; - if (isFileType(component)) { + if (isFile(component.qualifier)) { this.setState({ components: [], paging: undefined }); return; } @@ -120,7 +115,7 @@ export default class MeasureOverview extends React.PureComponent { const { branchLike, component, domain, metrics } = this.props; const { paging } = this.state; - if (isFileType(component)) { + if (isFile(component.qualifier)) { return (
{ - if (this.state.component && isViewType(this.state.component)) { - this.props.router.push(getProjectUrl(component)); + updateSelected = (component: T.ComponentMeasureIntern) => { + if (this.state.component && isView(this.state.component.qualifier)) { + this.props.router.push(getProjectUrl(component.refKey || component.key, component.branch)); } else { this.props.updateQuery({ - selected: component !== this.props.rootComponent.key ? component : undefined + selected: component.key !== this.props.rootComponent.key ? component.key : undefined }); } }; diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx index 2fb770d4722..bfeb34a62df 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx @@ -30,6 +30,7 @@ import { } from '../../../helpers/l10n'; import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; import { isDefined } from '../../../helpers/types'; +import { isProject } from '../../../types/component'; import { BUBBLES_FETCH_LIMIT, getBubbleMetrics, @@ -46,7 +47,7 @@ interface Props { domain: string; metrics: T.Dict; paging?: T.Paging; - updateSelected: (component: string) => void; + updateSelected: (component: T.ComponentMeasureIntern) => void; } interface State { @@ -67,16 +68,18 @@ export default class BubbleChart extends React.PureComponent { }; getTooltip( - componentName: string, + component: T.ComponentMeasureEnhanced, values: { x: number; y: number; size: number; colors?: Array }, metrics: { x: T.Metric; y: T.Metric; size: T.Metric; colors?: T.Metric[] } ) { const inner = [ - componentName, + [component.name, isProject(component.qualifier) ? component.branch : undefined] + .filter(s => !!s) + .join(' / '), `${metrics.x.name}: ${formatMeasure(values.x, metrics.x.type)}`, `${metrics.y.name}: ${formatMeasure(values.y, metrics.y.type)}`, `${metrics.size.name}: ${formatMeasure(values.size, metrics.size.type)}` - ]; + ].filter(s => !!s); const { colors: valuesColors } = values; const { colors: metricColors } = metrics; if (valuesColors && metricColors) { @@ -106,7 +109,7 @@ export default class BubbleChart extends React.PureComponent { }; handleBubbleClick = (component: T.ComponentMeasureEnhanced) => - this.props.updateSelected(component.refKey || component.key); + this.props.updateSelected(component); getDescription(domain: string) { const description = `component_measures.overview.${domain}.description`; @@ -144,7 +147,7 @@ export default class BubbleChart extends React.PureComponent { size, color: colorRating !== undefined ? RATING_COLORS[colorRating - 1] : undefined, data: component, - tooltip: this.getTooltip(component.name, { x, y, size, colors }, metrics) + tooltip: this.getTooltip(component, { x, y, size, colors }, metrics) }; }) .filter(isDefined); diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx index 9b81111a7db..7d0d3d5665b 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx @@ -23,13 +23,10 @@ import { Link } from 'react-router'; import BranchIcon from '../../../components/icons/BranchIcon'; import LinkIcon from '../../../components/icons/LinkIcon'; import QualifierIcon from '../../../components/icons/QualifierIcon'; +import { fillBranchLike } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { splitPath } from '../../../helpers/path'; -import { - getBranchLikeUrl, - getComponentDrilldownUrlWithSelection, - getProjectUrl -} from '../../../helpers/urls'; +import { getComponentDrilldownUrlWithSelection, getProjectUrl } from '../../../helpers/urls'; import { BranchLike } from '../../../types/branch-like'; import { ComponentQualifier, @@ -67,33 +64,29 @@ export default function ComponentCell(props: ComponentCellProps) { } let path: LocationDescriptor; - if (component.refKey) { - if ( - !isPortfolioLike(component.qualifier) && - ([MetricKey.releasability_rating, MetricKey.alert_status] as string[]).includes(metric.key) - ) { - path = isApplication(component.qualifier) - ? getProjectUrl(component.refKey, component.branch) - : getBranchLikeUrl(component.refKey, branchLike); - } else if (isProject(component.qualifier) && metric.key === MetricKey.projects) { - path = getBranchLikeUrl(component.refKey, branchLike); - } else { - path = getComponentDrilldownUrlWithSelection( - component.refKey, - '', - metric.key, - branchLike, - view - ); - } - } else { - path = getComponentDrilldownUrlWithSelection( - rootComponent.key, - component.key, - metric.key, - branchLike, - view - ); + const targetKey = component.refKey || rootComponent.key; + const selectionKey = component.refKey ? '' : component.key; + + // drilldown by default + path = getComponentDrilldownUrlWithSelection( + targetKey, + selectionKey, + metric.key, + component.branch ? fillBranchLike(component.branch) : branchLike, + view + ); + + // This metric doesn't exist for project + if (metric.key === MetricKey.projects && isProject(component.qualifier)) { + path = getProjectUrl(targetKey, component.branch); + } + + // Those metric doesn't exist for application and project + if ( + ([MetricKey.releasability_rating, MetricKey.alert_status] as string[]).includes(metric.key) && + (isApplication(component.qualifier) || isProject(component.qualifier)) + ) { + path = getProjectUrl(targetKey, component.branch); } return ( @@ -112,7 +105,7 @@ export default function ComponentCell(props: ComponentCellProps) { {head.length > 0 && {head}/} {tail} - {isApplication(rootComponent.qualifier) && + {(isApplication(rootComponent.qualifier) || isPortfolioLike(rootComponent.qualifier)) && (component.branch ? ( <> diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx index 907f671a76f..1209d04cb86 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { getComponentMeasureUniqueKey } from '../../../helpers/component'; import { getLocalizedMetricName } from '../../../helpers/l10n'; import { BranchLike } from '../../../types/branch-like'; import { MeasurePageView } from '../../../types/measures'; @@ -31,7 +32,7 @@ interface Props { metric: T.Metric; metrics: T.Dict; rootComponent: T.ComponentMeasure; - selectedComponent?: string; + selectedComponent?: T.ComponentMeasureEnhanced; view: MeasurePageView; } @@ -63,8 +64,11 @@ export default function ComponentsList({ components, metric, metrics, ...props } {components.map(component => ( void; - handleSelect: (component: string) => void; - handleOpen: (component: string) => void; + handleSelect: (component: T.ComponentMeasureEnhanced) => void; + handleOpen: (component: T.ComponentMeasureEnhanced) => void; loadingMore: boolean; metric: T.Metric; metrics: T.Dict; paging?: T.Paging; rootComponent: T.ComponentMeasure; - selectedKey?: string; + selectedComponent?: T.ComponentMeasureEnhanced; selectedIdx?: number; view: MeasurePageView; } @@ -65,13 +65,16 @@ export default class FilesView extends React.PureComponent { componentDidMount() { this.attachShortcuts(); - if (this.props.selectedKey !== undefined) { + if (this.props.selectedComponent !== undefined) { this.scrollToElement(); } } componentDidUpdate(prevProps: Props) { - if (this.props.selectedKey !== undefined && prevProps.selectedKey !== this.props.selectedKey) { + if ( + this.props.selectedComponent && + prevProps.selectedComponent !== this.props.selectedComponent + ) { this.scrollToElement(); } if (prevProps.metric.key !== this.props.metric.key || prevProps.view !== this.props.view) { @@ -128,8 +131,8 @@ export default class FilesView extends React.PureComponent { }; openSelected = () => { - if (this.props.selectedKey !== undefined) { - this.props.handleOpen(this.props.selectedKey); + if (this.props.selectedComponent !== undefined) { + this.props.handleOpen(this.props.selectedComponent); } }; @@ -137,9 +140,9 @@ export default class FilesView extends React.PureComponent { const { selectedIdx } = this.props; const visibleComponents = this.getVisibleComponents(); if (selectedIdx !== undefined && selectedIdx > 0) { - this.props.handleSelect(visibleComponents[selectedIdx - 1].key); + this.props.handleSelect(visibleComponents[selectedIdx - 1]); } else { - this.props.handleSelect(visibleComponents[visibleComponents.length - 1].key); + this.props.handleSelect(visibleComponents[visibleComponents.length - 1]); } }; @@ -147,9 +150,9 @@ export default class FilesView extends React.PureComponent { const { selectedIdx } = this.props; const visibleComponents = this.getVisibleComponents(); if (selectedIdx !== undefined && selectedIdx < visibleComponents.length - 1) { - this.props.handleSelect(visibleComponents[selectedIdx + 1].key); + this.props.handleSelect(visibleComponents[selectedIdx + 1]); } else { - this.props.handleSelect(visibleComponents[0].key); + this.props.handleSelect(visibleComponents[0]); } }; @@ -174,7 +177,7 @@ export default class FilesView extends React.PureComponent { metric={this.props.metric} metrics={this.props.metrics} rootComponent={this.props.rootComponent} - selectedComponent={this.props.selectedKey} + selectedComponent={this.props.selectedComponent} view={this.props.view} /> {hidingBestMeasures && this.props.paging && ( diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx index 13a4b4b30f5..052ddfb34a4 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx @@ -25,6 +25,7 @@ import ColorBoxLegend from '../../../components/charts/ColorBoxLegend'; import ColorGradientLegend from '../../../components/charts/ColorGradientLegend'; import TreeMap, { TreeMapItem } from '../../../components/charts/TreeMap'; import QualifierIcon from '../../../components/icons/QualifierIcon'; +import { getComponentMeasureUniqueKey } from '../../../helpers/component'; import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n'; import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; import { isDefined } from '../../../helpers/types'; @@ -35,7 +36,7 @@ import EmptyResult from './EmptyResult'; interface Props { branchLike?: BranchLike; components: T.ComponentMeasureEnhanced[]; - handleSelect: (component: string) => void; + handleSelect: (component: T.ComponentMeasureIntern) => void; metric: T.Metric; } @@ -88,18 +89,19 @@ export default class TreeMapView extends React.PureComponent { color: colorValue ? (colorScale as Function)(colorValue) : undefined, gradient: !colorValue ? NA_GRADIENT : undefined, icon: , - key: component.refKey || component.key, - label: component.name, + key: getComponentMeasureUniqueKey(component) ?? '', + label: [component.name, component.branch].filter(s => !!s).join(' / '), size: sizeValue, measureValue: colorValue, metric, tooltip: this.getTooltip({ colorMetric: metric, colorValue, - componentName: component.name, + component, sizeMetric: sizeMeasure.metric, sizeValue - }) + }), + component }; }) .filter(isDefined); @@ -134,13 +136,13 @@ export default class TreeMapView extends React.PureComponent { getTooltip = ({ colorMetric, colorValue, - componentName, + component, sizeMetric, sizeValue }: { colorMetric: T.Metric; colorValue?: string; - componentName: string; + component: T.ComponentMeasureEnhanced; sizeMetric: T.Metric; sizeValue: number; }) => { @@ -148,7 +150,7 @@ export default class TreeMapView extends React.PureComponent { colorMetric && colorValue !== undefined ? formatMeasure(colorValue, colorMetric.type) : '—'; return (
- {componentName} + {[component.name, component.branch].filter(s => !!s).join(' / ')}
{`${getLocalizedMetricName(sizeMetric)}: ${formatMeasure(sizeValue, sizeMetric.type)}`}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/ComponentCell-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/ComponentCell-test.tsx index ccb196e14b8..8d15167ee90 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/ComponentCell-test.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/ComponentCell-test.tsx @@ -19,7 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { mockBranch, mockPullRequest } from '../../../../helpers/mocks/branch-like'; +import { Link } from 'react-router'; import { mockComponentMeasure, mockComponentMeasureEnhanced @@ -32,89 +32,123 @@ import ComponentCell, { ComponentCellProps } from '../ComponentCell'; it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot('default'); +}); + +it.each([ + [ComponentQualifier.Project, undefined], + [ComponentQualifier.Project, 'develop'], + [ComponentQualifier.Application, undefined], + [ComponentQualifier.Application, 'develop'], + [ComponentQualifier.Portfolio, undefined], + [ComponentQualifier.Portfolio, 'develop'] +])( + 'should render correctly for a "%s" root component and a component with branch "%s"', + (rootComponentQualifier: ComponentQualifier, componentBranch: string | undefined) => { + expect( + shallowRender({ + rootComponent: mockComponentMeasure(false, { qualifier: rootComponentQualifier }), + component: mockComponentMeasureEnhanced({ branch: componentBranch }) + }) + ).toMatchSnapshot(); + } +); + +it('should properly deal with key and refKey', () => { expect( shallowRender({ - rootComponent: mockComponentMeasure(false, { qualifier: ComponentQualifier.Application }) + component: mockComponentMeasureEnhanced({ + qualifier: ComponentQualifier.SubPortfolio, + refKey: 'port-key' + }) }) - ).toMatchSnapshot('root component is application, component is on main branch'); + .find(Link) + .props().to + ).toEqual(expect.objectContaining({ query: expect.objectContaining({ id: 'port-key' }) })); + expect( - shallowRender({ - rootComponent: mockComponentMeasure(false, { qualifier: ComponentQualifier.Application }), - component: mockComponentMeasureEnhanced({ branch: 'develop' }) + shallowRender() + .find(Link) + .props().to + ).toEqual( + expect.objectContaining({ + query: expect.objectContaining({ id: 'foo', selected: 'foo:src/index.tsx' }) }) - ).toMatchSnapshot('root component is application, component has branch'); - expect( - shallowRender({ component: mockComponentMeasureEnhanced({ refKey: 'project-key' }) }) - ).toMatchSnapshot('ref project component'); - expect( - shallowRender( - { - component: mockComponentMeasureEnhanced({ - refKey: 'project-key', - qualifier: ComponentQualifier.Project - }), - branchLike: mockBranch() - }, - MetricKey.releasability_rating - ) - ).toMatchSnapshot('ref project component, releasability metric'); - expect( - shallowRender( - { - component: mockComponentMeasureEnhanced({ - refKey: 'app-key', - qualifier: ComponentQualifier.Application - }), - branchLike: mockBranch() - }, - MetricKey.projects - ) - ).toMatchSnapshot('ref application component, projects'); - expect( - shallowRender( - { - component: mockComponentMeasureEnhanced({ - refKey: 'project-key', - qualifier: ComponentQualifier.Project - }), - branchLike: mockBranch() - }, - MetricKey.projects - ) - ).toMatchSnapshot('ref project component, projects'); - expect( - shallowRender( - { - component: mockComponentMeasureEnhanced({ - refKey: 'app-key', - qualifier: ComponentQualifier.Application - }), - branchLike: mockPullRequest() - }, - MetricKey.alert_status - ) - ).toMatchSnapshot('ref application component, alert_status metric'); - expect( - shallowRender( + ); +}); + +it.each([ + [ + ComponentQualifier.File, + MetricKey.bugs, + expect.objectContaining({ + pathname: '/component_measures', + query: expect.objectContaining({ branch: 'develop' }) + }) + ], + [ + ComponentQualifier.Directory, + MetricKey.bugs, + expect.objectContaining({ + pathname: '/component_measures', + query: expect.objectContaining({ branch: 'develop' }) + }) + ], + [ + ComponentQualifier.Project, + MetricKey.projects, + expect.objectContaining({ + pathname: '/dashboard', + query: expect.objectContaining({ branch: 'develop' }) + }) + ], + [ + ComponentQualifier.Application, + MetricKey.releasability_rating, + expect.objectContaining({ + pathname: '/dashboard', + query: expect.objectContaining({ branch: 'develop' }) + }) + ], + [ + ComponentQualifier.Project, + MetricKey.releasability_rating, + expect.objectContaining({ + pathname: '/dashboard', + query: expect.objectContaining({ branch: 'develop' }) + }) + ], + [ + ComponentQualifier.Application, + MetricKey.alert_status, + expect.objectContaining({ + pathname: '/dashboard', + query: expect.objectContaining({ branch: 'develop' }) + }) + ], + [ + ComponentQualifier.Project, + MetricKey.alert_status, + expect.objectContaining({ + pathname: '/dashboard', + query: expect.objectContaining({ branch: 'develop' }) + }) + ] +])( + 'should display the proper link path for %s component qualifier and %s metric key', + (componentQualifier: ComponentQualifier, metricKey: MetricKey, expectedTo: any) => { + const wrapper = shallowRender( { component: mockComponentMeasureEnhanced({ - refKey: 'vw-key', - qualifier: ComponentQualifier.Portfolio - }), - branchLike: mockPullRequest() + qualifier: componentQualifier, + branch: 'develop' + }) }, - MetricKey.alert_status - ) - ).toMatchSnapshot('ref portfolio component, alert_status metric'); - expect( - shallowRender({ - component: mockComponentMeasureEnhanced({ - key: 'svw-bar', - qualifier: ComponentQualifier.SubPortfolio - }) - }) - ).toMatchSnapshot('sub-portfolio component'); -}); + metricKey + ); + + expect(wrapper.find(Link).props().to).toEqual(expectedTo); + } +); function shallowRender(overrides: Partial = {}, metricKey = MetricKey.bugs) { const metric = mockMetric({ key: metricKey }); diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentCell-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentCell-test.tsx.snap index 474ada97872..cc3d3f3ca07 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentCell-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentCell-test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render correctly: default 1`] = ` +exports[`should render correctly for a "APP" root component and a component with branch "develop" 1`] = ` @@ -9,87 +9,47 @@ exports[`should render correctly: default 1`] = ` > - - - - src - / - - - index.tsx - - - -
- -`; - -exports[`should render correctly: ref application component, alert_status metric 1`] = ` - -
- - - - Foo + + + develop +
`; -exports[`should render correctly: ref application component, projects 1`] = ` +exports[`should render correctly for a "APP" root component and a component with branch "undefined" 1`] = ` @@ -105,36 +65,36 @@ exports[`should render correctly: ref application component, projects 1`] = ` Object { "pathname": "/component_measures", "query": Object { - "branch": "branch-6.7", - "id": "app-key", - "metric": "projects", + "id": "foo", + "metric": "bugs", + "selected": "foo", "view": "list", }, } } > - - - Foo + + branches.main_branch +
`; -exports[`should render correctly: ref portfolio component, alert_status metric 1`] = ` +exports[`should render correctly for a "TRK" root component and a component with branch "develop" 1`] = ` @@ -150,25 +110,21 @@ exports[`should render correctly: ref portfolio component, alert_status metric 1 Object { "pathname": "/component_measures", "query": Object { - "id": "vw-key", - "metric": "alert_status", - "pullRequest": "1001", + "branch": "develop", + "id": "foo", + "metric": "bugs", + "selected": "foo", "view": "list", }, } } > - - - Foo @@ -179,7 +135,7 @@ exports[`should render correctly: ref portfolio component, alert_status metric 1 `; -exports[`should render correctly: ref project component 1`] = ` +exports[`should render correctly for a "TRK" root component and a component with branch "undefined" 1`] = ` @@ -195,18 +151,14 @@ exports[`should render correctly: ref project component 1`] = ` Object { "pathname": "/component_measures", "query": Object { - "id": "project-key", + "id": "foo", "metric": "bugs", + "selected": "foo", "view": "list", }, } } > - - - @@ -223,7 +175,7 @@ exports[`should render correctly: ref project component 1`] = ` `; -exports[`should render correctly: ref project component, projects 1`] = ` +exports[`should render correctly for a "VW" root component and a component with branch "develop" 1`] = ` @@ -237,19 +189,17 @@ exports[`should render correctly: ref project component, projects 1`] = ` style={Object {}} to={ Object { - "pathname": "/dashboard", + "pathname": "/component_measures", "query": Object { - "branch": "branch-6.7", - "id": "project-key", + "branch": "develop", + "id": "foo", + "metric": "bugs", + "selected": "foo", + "view": "list", }, } } > - - - @@ -260,48 +210,13 @@ exports[`should render correctly: ref project component, projects 1`] = ` Foo - - -
- -`; - -exports[`should render correctly: ref project component, releasability metric 1`] = ` - -
- - - - - - - - Foo + + develop @@ -309,7 +224,7 @@ exports[`should render correctly: ref project component, releasability metric 1` `; -exports[`should render correctly: root component is application, component has branch 1`] = ` +exports[`should render correctly for a "VW" root component and a component with branch "undefined" 1`] = ` @@ -343,13 +258,10 @@ exports[`should render correctly: root component is application, component has b Foo - - develop + branches.main_branch @@ -357,7 +269,7 @@ exports[`should render correctly: root component is application, component has b `; -exports[`should render correctly: root component is application, component is on main branch 1`] = ` +exports[`should render correctly: default 1`] = ` @@ -397,51 +309,6 @@ exports[`should render correctly: root component is application, component is on index.tsx - - branches.main_branch - - - -
- -`; - -exports[`should render correctly: sub-portfolio component 1`] = ` - -
- - - - - Foo -
diff --git a/server/sonar-web/src/main/js/apps/component-measures/utils.ts b/server/sonar-web/src/main/js/apps/component-measures/utils.ts index 5b95979165e..806602382b9 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/utils.ts +++ b/server/sonar-web/src/main/js/apps/component-measures/utils.ts @@ -105,20 +105,6 @@ export function enhanceComponent( return { ...component, value, leak, measures: enhancedMeasures }; } -export function isFileType(component: { qualifier: string | ComponentQualifier }): boolean { - return [ComponentQualifier.File, ComponentQualifier.TestFile].includes( - component.qualifier as ComponentQualifier - ); -} - -export function isViewType(component: T.ComponentMeasure): boolean { - return [ - ComponentQualifier.Portfolio, - ComponentQualifier.SubPortfolio, - ComponentQualifier.Application - ].includes(component.qualifier as ComponentQualifier); -} - export function isSecurityReviewMetric(metricKey: MetricKey | string): boolean { return [ MetricKey.security_hotspots, diff --git a/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx index 8328b94727d..6ee6c5e13d6 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx @@ -18,9 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import BranchIcon from '../../../components/icons/BranchIcon'; import QualifierIcon from '../../../components/icons/QualifierIcon'; import { translateWithParameters } from '../../../helpers/l10n'; import { collapsePath, limitComponentName } from '../../../helpers/path'; +import { ComponentQualifier } from '../../../types/component'; import { getSelectedLocation } from '../utils'; interface Props { @@ -36,11 +38,22 @@ export default function ComponentBreadcrumbs({ selectedFlowIndex, selectedLocationIndex }: Props) { - const displayProject = !component || !['TRK', 'BRC', 'DIR'].includes(component.qualifier); - const displaySubProject = !component || !['BRC', 'DIR'].includes(component.qualifier); + const displayProject = + !component || + ![ + ComponentQualifier.Project, + ComponentQualifier.SubProject, + ComponentQualifier.Directory + ].includes(component.qualifier as ComponentQualifier); + const displaySubProject = + !component || + ![ComponentQualifier.SubProject, ComponentQualifier.Directory].includes( + component.qualifier as ComponentQualifier + ); const selectedLocation = getSelectedLocation(issue, selectedFlowIndex, selectedLocationIndex); const componentName = selectedLocation ? selectedLocation.componentName : issue.componentLongName; + const projectName = [issue.projectName, issue.branch].filter(s => !!s).join(' - '); return (
{displayProject && ( - + {limitComponentName(issue.projectName)} + {issue.branch && ( + <> + {' - '} + + {issue.branch} + + )} )} diff --git a/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx b/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx index 1115f06c456..9900b6f07ea 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx @@ -87,7 +87,10 @@ export default class ListItem extends React.PureComponent { render() { const { branchLike, component, issue, previousIssue } = this.props; - const displayComponent = !previousIssue || previousIssue.component !== issue.component; + const displayComponent = + !previousIssue || + previousIssue.component !== issue.component || + previousIssue.branch !== issue.branch; return (
  • diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx index dbdc295e04a..d1b04259dad 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx @@ -28,7 +28,8 @@ const baseIssue = mockIssue(false, { componentLongName: 'comp-name', componentQualifier: ComponentQualifier.File, project: 'proj', - projectName: 'proj-name' + projectName: 'proj-name', + branch: 'test-branch' }); it('renders', () => { diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/ListItem-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/ListItem-test.tsx new file mode 100644 index 00000000000..02cc2634102 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/ListItem-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 { mockBranch } from '../../../../helpers/mocks/branch-like'; +import { mockComponent } from '../../../../helpers/mocks/component'; +import { mockIssue } from '../../../../helpers/testMocks'; +import ListItem from '../ListItem'; + +it('should render correctly', () => { + const wrapper = shallowRender(); + expect(wrapper).toMatchSnapshot(); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap index 3ecf7df7f33..783ef3479dc 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap @@ -10,9 +10,14 @@ exports[`renders 1`] = ` qualifier="FIL" /> proj-name + - + + + test-branch + @@ -35,9 +40,14 @@ exports[`renders with sub-project 1`] = ` qualifier="FIL" /> proj-name + - + + + test-branch + diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap new file mode 100644 index 00000000000..241cbbbf251 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap @@ -0,0 +1,115 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +
  • +
    + +
    + +
  • +`; diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx index 7a9097aac29..cc1a4579bc8 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx @@ -38,7 +38,7 @@ export default function Effort({ component, effort, metricKey }: Props) { defaultMessage={translate('portfolio.x_in_y')} id="portfolio.x_in_y" values={{ - projects: ( + project_branches: ( {effort.projects === 1 - ? translate('project_singular') - : translate('project_plural')} + ? translate('portfolio.project_branch') + : translate('portfolio.project_branches')}
    ), diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/MetricBox.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/MetricBox.tsx index e8942f336a9..0d6d46506ba 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/MetricBox.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/MetricBox.tsx @@ -73,7 +73,7 @@ export default function MetricBox({ component, measures, metricKey }: MetricBoxP {metricKey === 'releasability' ? Number(effort) > 0 && ( <> -

    {translate('portfolio.lowest_rated_projects')}

    +

    {translate('portfolio.lowest_rated_project_branches')}

    {Number(effort) === 1 - ? translate('project_singular') - : translate('project_plural')} + ? translate('portfolio.project_branch') + : translate('portfolio.project_branches')} -

    {translate('portfolio.lowest_rated_projects')}

    +

    {translate('portfolio.lowest_rated_project_branches')}

    )} diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx index ebadc03e3cc..921a0066e6c 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx @@ -17,10 +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 { max } from 'lodash'; +import { max, sortBy } from 'lodash'; import * as React from 'react'; import { Link } from 'react-router'; import { colors } from '../../../app/theme'; +import BranchIcon from '../../../components/icons/BranchIcon'; import QualifierIcon from '../../../components/icons/QualifierIcon'; import Measure from '../../../components/measure/Measure'; import { translate, translateWithParameters } from '../../../helpers/l10n'; @@ -48,6 +49,13 @@ export default function WorstProjects({ component, subComponents, total }: Props const projectsPageUrl = { pathname: '/code', query: { id: component } }; + const subCompList = sortBy( + subComponents, + c => c.qualifier, + c => c.name.toLowerCase(), + c => c.branch?.toLowerCase() + ); + return (
    @@ -75,26 +83,39 @@ export default function WorstProjects({ component, subComponents, total }: Props - {subComponents.map(component => ( - + {subCompList.map(comp => ( + !!s).join('/')}> - {component.qualifier === ComponentQualifier.Project - ? renderCell(component.measures, 'alert_status', 'LEVEL') - : renderCell(component.measures, 'releasability_rating', 'RATING')} - {renderCell(component.measures, 'reliability_rating', 'RATING')} - {renderCell(component.measures, 'security_rating', 'RATING')} - {renderCell(component.measures, 'security_review_rating', 'RATING')} - {renderCell(component.measures, 'sqale_rating', 'RATING')} - {renderNcloc(component.measures, maxLoc)} + {comp.qualifier === ComponentQualifier.Project + ? renderCell(comp.measures, 'alert_status', 'LEVEL') + : renderCell(comp.measures, 'releasability_rating', 'RATING')} + {renderCell(comp.measures, 'reliability_rating', 'RATING')} + {renderCell(comp.measures, 'security_rating', 'RATING')} + {renderCell(comp.measures, 'security_review_rating', 'RATING')} + {renderCell(comp.measures, 'sqale_rating', 'RATING')} + {renderNcloc(comp.measures, maxLoc)} ))} diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/WorstProjects-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/WorstProjects-test.tsx index aa70aa4cfca..799c07bfbdf 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/WorstProjects-test.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/WorstProjects-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { ComponentQualifier } from '../../../../types/component'; import WorstProjects from '../WorstProjects'; it('renders', () => { @@ -33,7 +34,33 @@ it('renders', () => { ncloc: '200' }, name: 'Foo', - qualifier: 'SVW' + qualifier: ComponentQualifier.SubPortfolio + }, + { + key: 'foo_app', + measures: { + releasability_rating: '3', + reliability_rating: '2', + security_rating: '1', + sqale_rating: '4', + ncloc: '200' + }, + name: 'Foo', + qualifier: ComponentQualifier.Application + }, + { + key: 'bar', + measures: { + alert_status: 'ERROR', + reliability_rating: '2', + security_rating: '1', + sqale_rating: '4', + ncloc: '100' + }, + name: 'Bar', + qualifier: ComponentQualifier.Project, + refKey: 'barbar', + branch: 'branch-1' }, { key: 'bar', @@ -45,7 +72,7 @@ it('renders', () => { ncloc: '100' }, name: 'Bar', - qualifier: 'TRK', + qualifier: ComponentQualifier.Project, refKey: 'barbar' }, { @@ -58,7 +85,7 @@ it('renders', () => { ncloc: '150' }, name: 'Baz', - qualifier: 'TRK', + qualifier: ComponentQualifier.Project, refKey: 'bazbaz' } ]; diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap index 8ee2d0d5355..9b07a418062 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap @@ -9,7 +9,7 @@ exports[`renders 1`] = ` id="portfolio.x_in_y" values={ Object { - "projects": - project_plural + portfolio.project_branches , "rating":

    - portfolio.lowest_rated_projects + portfolio.lowest_rated_project_branches

    - portfolio.lowest_rated_projects + portfolio.lowest_rated_project_branches

    - project_plural + portfolio.project_branches

    - portfolio.lowest_rated_projects + portfolio.lowest_rated_project_branches

    - project_singular + portfolio.project_branch
    + + + + + + + + + + + + + + + + + +
    - - {component.name} - + + + + {comp.name} + + + {[ComponentQualifier.Application, ComponentQualifier.Project].includes( + comp.qualifier as ComponentQualifier + ) && + (comp.branch ? ( + + + {comp.branch} + + ) : ( + {translate('branches.main_branch')} + ))} +
    - + + + Foo + + + branches.main_branch + + + + + + + + + + + + + + + + + + + +
    + - - Foo - + + Foo + +
    - + + + Bar + + + + + branch-1 + + + + + + + + + + + + + + + + + + + + +
    + - - Bar - + + Bar + + + branches.main_branch + + - - - Baz - + + Baz + + + branches.main_branch + + void; + onRectangleClick?: (item: T.ComponentMeasureEnhanced) => void; width: number; } @@ -64,6 +65,12 @@ export default class TreeMap extends React.PureComponent { return prefix.substr(0, prefix.length - lastPrefixPart.length); }; + handleClick = (component: T.ComponentMeasureEnhanced) => { + if (this.props.onRectangleClick) { + this.props.onRectangleClick(component); + } + }; + render() { const { items, height, width } = this.props; const hierarchy = d3Hierarchy({ children: items } as HierarchicalTreemapItem) @@ -90,7 +97,7 @@ export default class TreeMap extends React.PureComponent { key={node.data.key} label={node.data.label} link={node.data.link} - onClick={this.props.onRectangleClick} + onClick={() => this.handleClick(node.data.component)} placement={node.x0 === 0 || node.x1 < halfWidth ? 'right' : 'left'} prefix={prefix} value={ diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/TreeMap-test.tsx b/server/sonar-web/src/main/js/components/charts/__tests__/TreeMap-test.tsx index d4879956f2b..d9a97e54f7e 100644 --- a/server/sonar-web/src/main/js/components/charts/__tests__/TreeMap-test.tsx +++ b/server/sonar-web/src/main/js/components/charts/__tests__/TreeMap-test.tsx @@ -19,19 +19,33 @@ */ import { mount } from 'enzyme'; import * as React from 'react'; +import { mockComponentMeasureEnhanced } from '../../../helpers/mocks/component'; import TreeMap from '../TreeMap'; import TreeMapRect from '../TreeMapRect'; it('should render correctly', () => { const items = [ - { key: '1', size: 10, color: '#777', label: 'SonarQube :: Server' }, - { key: '2', size: 30, color: '#777', label: 'SonarQube :: Web' }, + { + key: '1', + size: 10, + color: '#777', + label: 'SonarQube :: Server', + component: mockComponentMeasureEnhanced() + }, + { + key: '2', + size: 30, + color: '#777', + label: 'SonarQube :: Web', + component: mockComponentMeasureEnhanced() + }, { key: '3', size: 20, gradient: '#777', label: 'SonarQube :: Search', - metric: { key: 'coverage', type: 'PERCENT' } + metric: { key: 'coverage', type: 'PERCENT' }, + component: mockComponentMeasureEnhanced() } ]; const onRectClick = jest.fn(); @@ -49,5 +63,5 @@ it('should render correctly', () => { expect(event.stopPropagation).toHaveBeenCalled(); (rects.first().instance() as TreeMapRect).handleRectClick(); - expect(onRectClick).toHaveBeenCalledWith('2'); + expect(onRectClick).toHaveBeenCalledWith(expect.objectContaining({ key: 'foo' })); }); diff --git a/server/sonar-web/src/main/js/components/controls/SelectListListElement.tsx b/server/sonar-web/src/main/js/components/controls/SelectListListElement.tsx index d3a0912c1c2..a3d3aac73f2 100644 --- a/server/sonar-web/src/main/js/components/controls/SelectListListElement.tsx +++ b/server/sonar-web/src/main/js/components/controls/SelectListListElement.tsx @@ -61,14 +61,21 @@ export default class SelectListListElement extends React.PureComponent +
  • - {this.props.renderElement(this.props.element)} + + {this.props.renderElement(this.props.element)} +
  • ); diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SelectListListElement-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SelectListListElement-test.tsx.snap index e5d4ba3601f..0b05a479c5c 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SelectListListElement-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SelectListListElement-test.tsx.snap @@ -6,13 +6,13 @@ exports[`should display a loader when checking 1`] = ` > foo @@ -26,13 +26,13 @@ exports[`should display a loader when checking 2`] = ` > foo diff --git a/server/sonar-web/src/main/js/components/hoc/withKeyboardNavigation.tsx b/server/sonar-web/src/main/js/components/hoc/withKeyboardNavigation.tsx index 9288e336bbb..cebddd9aa14 100644 --- a/server/sonar-web/src/main/js/components/hoc/withKeyboardNavigation.tsx +++ b/server/sonar-web/src/main/js/components/hoc/withKeyboardNavigation.tsx @@ -20,6 +20,7 @@ import key from 'keymaster'; import * as React from 'react'; import PageActions from '../../components/ui/PageActions'; +import { getComponentMeasureUniqueKey } from '../../helpers/component'; import { getWrappedDisplayName } from './utils'; export interface WithKeyboardNavigationProps { @@ -72,7 +73,12 @@ export default function withKeyboardNavigation

    ( getCurrentIndex = () => { const { selected, components = [] } = this.props; - return selected ? components.findIndex(component => component.key === selected.key) : -1; + return selected + ? components.findIndex( + component => + getComponentMeasureUniqueKey(component) === getComponentMeasureUniqueKey(selected) + ) + : -1; }; skipIfFile = (handler: () => void) => { diff --git a/server/sonar-web/src/main/js/helpers/component.ts b/server/sonar-web/src/main/js/helpers/component.ts new file mode 100644 index 00000000000..c3820cb2a2d --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/component.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +export function getComponentMeasureUniqueKey( + component?: T.ComponentMeasure | T.ComponentMeasureEnhanced +) { + return component ? [component.key, component.branch].filter(s => !!s).join('/') : undefined; +} diff --git a/server/sonar-web/src/main/js/types/__tests__/__snapshots__/component-test.ts.snap b/server/sonar-web/src/main/js/types/__tests__/__snapshots__/component-test.ts.snap new file mode 100644 index 00000000000..6711bdb59fe --- /dev/null +++ b/server/sonar-web/src/main/js/types/__tests__/__snapshots__/component-test.ts.snap @@ -0,0 +1,71 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`[Function isApplication] should work properly 1`] = ` +Object { + "APP": true, + "BRC": false, + "DEV": false, + "DIR": false, + "FIL": false, + "SVW": false, + "TRK": false, + "UTS": false, + "VW": false, +} +`; + +exports[`[Function isFile] should work properly 1`] = ` +Object { + "APP": false, + "BRC": false, + "DEV": false, + "DIR": false, + "FIL": true, + "SVW": false, + "TRK": false, + "UTS": true, + "VW": false, +} +`; + +exports[`[Function isPortfolioLike] should work properly 1`] = ` +Object { + "APP": false, + "BRC": false, + "DEV": false, + "DIR": false, + "FIL": false, + "SVW": true, + "TRK": false, + "UTS": false, + "VW": true, +} +`; + +exports[`[Function isProject] should work properly 1`] = ` +Object { + "APP": false, + "BRC": false, + "DEV": false, + "DIR": false, + "FIL": false, + "SVW": false, + "TRK": true, + "UTS": false, + "VW": false, +} +`; + +exports[`[Function isView] should work properly 1`] = ` +Object { + "APP": true, + "BRC": false, + "DEV": false, + "DIR": false, + "FIL": false, + "SVW": true, + "TRK": false, + "UTS": false, + "VW": true, +} +`; diff --git a/server/sonar-web/src/main/js/types/__tests__/component-test.ts b/server/sonar-web/src/main/js/types/__tests__/component-test.ts new file mode 100644 index 00000000000..d8e702a77d7 --- /dev/null +++ b/server/sonar-web/src/main/js/types/__tests__/component-test.ts @@ -0,0 +1,39 @@ +/* + * 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 { + ComponentQualifier, + isApplication, + isFile, + isPortfolioLike, + isProject, + isView +} from '../component'; + +it.each([[isFile], [isView], [isProject], [isApplication], [isPortfolioLike]])( + '%p should work properly', + (utilityMethod: (componentQualifier: ComponentQualifier) => void) => { + const results = Object.values(ComponentQualifier).reduce( + (prev, qualifier) => ({ ...prev, [qualifier]: utilityMethod(qualifier) }), + {} + ); + expect(results).toMatchSnapshot(); + } +); diff --git a/server/sonar-web/src/main/js/types/component.ts b/server/sonar-web/src/main/js/types/component.ts index ac13fb4effe..1e843536ee7 100644 --- a/server/sonar-web/src/main/js/types/component.ts +++ b/server/sonar-web/src/main/js/types/component.ts @@ -79,3 +79,17 @@ export function isProject( ): componentQualifier is ComponentQualifier.Project { return componentQualifier === ComponentQualifier.Project; } + +export function isFile(componentQualifier?: string | ComponentQualifier): boolean { + return [ComponentQualifier.File, ComponentQualifier.TestFile].includes( + componentQualifier as ComponentQualifier + ); +} + +export function isView(componentQualifier?: string | ComponentQualifier): boolean { + return [ + ComponentQualifier.Portfolio, + ComponentQualifier.SubPortfolio, + ComponentQualifier.Application + ].includes(componentQualifier as ComponentQualifier); +} diff --git a/server/sonar-web/src/main/js/types/types.d.ts b/server/sonar-web/src/main/js/types/types.d.ts index d350f26b38f..904fc039246 100644 --- a/server/sonar-web/src/main/js/types/types.d.ts +++ b/server/sonar-web/src/main/js/types/types.d.ts @@ -167,7 +167,7 @@ declare namespace T { name: string; } - interface ComponentMeasureIntern { + export interface ComponentMeasureIntern { branch?: string; description?: string; isFavorite?: boolean; 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 b8be20251a8..145b388204f 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -150,7 +150,6 @@ project_x=Project: {0} projects=Projects projects_=project(s) x_projects_={0} project(s) -project_singular=project project_plural=projects projects_management=Projects Management quality_profile=Quality Profile @@ -2441,8 +2440,8 @@ metric.profile.description=Selected Quality Profile metric.profile.name=Profile metric.profile_version.description=Selected Quality Profile version metric.profile_version.name=Profile Version -metric.projects.description=Number of projects -metric.projects.name=Projects +metric.projects.description=Number of project branches +metric.projects.name=Project branches metric.public_api.description=Public API metric.public_api.name=Public API metric.public_documented_api_density.description=Public documented classes and functions balanced by ncloc @@ -4021,7 +4020,9 @@ branch_like_navigation.tutorial_for_ci=Show me how to set up my CI #------------------------------------------------------------------------------ portfolio.has_always_been_x=has always been {rating} portfolio.was_x_y=was {rating} {date} -portfolio.x_in_y={projects} in {rating} +portfolio.x_in_y={project_branches} in {rating} +portfolio.project_branch=project branch +portfolio.project_branches=project branches portfolio.has_qg_status=Has Quality Gate Status portfolio.have_qg_status=Have Quality Gate Status portfolio.empty=This portfolio is empty. @@ -4030,13 +4031,13 @@ portfolio.not_computed=This portfolio is not yet computed. portfolio.app.empty=This application is empty. portfolio.app.no_lines_of_code=All projects in this application are empty portfolio.metric_trend=Metric trend -portfolio.lowest_rated_projects=Lowest rated projects +portfolio.lowest_rated_project_branches=Lowest rated project branches portfolio.health_factors=Portfolio health factors portfolio.activity_link=Activity portfolio.measures_link=Measures portfolio.language_breakdown_link=Language breakdown portfolio.breakdown=Portfolio breakdown -portfolio.number_of_projects=Number of projects +portfolio.number_of_projects=Number of project branches portfolio.number_of_lines=Number of lines of code portfolio.metric_domain.vulnerabilities=Security Vulnerabilities diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java b/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java index 58958699636..2fa656eb49d 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java @@ -229,8 +229,8 @@ public final class CoreMetrics { /** * @since 3.0 */ - public static final Metric PROJECTS = new Metric.Builder(PROJECTS_KEY, "Projects", Metric.ValueType.INT) - .setDescription("Number of projects") + public static final Metric PROJECTS = new Metric.Builder(PROJECTS_KEY, "Project branches", Metric.ValueType.INT) + .setDescription("Number of project branches") .setDirection(Metric.DIRECTION_WORST) .setQualitative(false) .setDomain(DOMAIN_SIZE) -- 2.39.5