diff options
author | Philippe Perrin <philippe.perrin@sonarsource.com> | 2021-10-20 10:48:08 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-11-09 20:03:16 +0000 |
commit | e03b2bf40be7de4821cdf009c74dffc90b87757b (patch) | |
tree | 0348599747bae1b6e74b52d9efbc8f05013250aa /server/sonar-web/src/main/js | |
parent | e8905886d0e77cc080022dff7e0df79e4823e5de (diff) | |
download | sonarqube-e03b2bf40be7de4821cdf009c74dffc90b87757b.tar.gz sonarqube-e03b2bf40be7de4821cdf009c74dffc90b87757b.zip |
SONAR-15498 Manual selection of project's branches for portfolio
Display portfolio's children branch information and group issues by project and branch
Diffstat (limited to 'server/sonar-web/src/main/js')
46 files changed, 1202 insertions, 580 deletions
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<Props, State> { 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 = ( <Link className="link-with-icon" to={getProjectUrl(component.refKey, branch)}> <QualifierIcon qualifier={component.qualifier} /> <span>{name}</span> @@ -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 ( <span className="max-width-100 display-inline-flex-center"> <span className="text-ellipsis" title={getTooltip(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 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<Props> { )} {components.length ? ( - components.map((component, index, list) => ( + sortBy( + components, + c => c.qualifier, + c => c.name.toLowerCase(), + c => c.branch?.toLowerCase() + ).map((component, index, list) => ( <Component branchLike={branchLike} canBePinned={canBePinned} canBrowse={true} component={component} hasBaseComponent={baseComponent !== undefined} - key={component.key} + key={getComponentMeasureUniqueKey(component)} metrics={metrics} previous={index > 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`] = ` <span - className="max-width-100 display-inline-flex-center" -> - <span - className="text-ellipsis" - title="src/index.tsx + className="max-width-100 display-inline-block text-ellipsis" + title="src/index.tsx foo:src/index.tsx" - > - <span> - <QualifierIcon - qualifier="FIL" - /> - - index.tsx - </span> - </span> - <span - className="spacer-left badge flex-1" - > - branches.main_branch +> + <span> + <QualifierIcon + qualifier="FIL" + /> + + index.tsx </span> </span> `; exports[`#ComponentName should render correctly for files 5`] = ` <span - className="max-width-100 display-inline-flex-center" -> - <span - className="text-ellipsis" - title="src/index.tsx + className="max-width-100 display-inline-block text-ellipsis" + title="src/index.tsx foo:src/index.tsx" - > - <span> - <QualifierIcon - qualifier="FIL" - /> - - index.tsx - </span> - </span> - <span - className="text-ellipsis spacer-left" - > - <BranchIcon - className="little-spacer-right" +> + <span> + <QualifierIcon + qualifier="FIL" /> - <span - className="note" - > - foo - </span> + + index.tsx </span> </span> `; @@ -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" > <Link @@ -212,6 +189,8 @@ exports[`#ComponentName should render correctly for refs 2`] = ` className="text-ellipsis" title="Foo +foo + foo" > <Link @@ -252,6 +231,47 @@ foo" </span> `; +exports[`#ComponentName should render correctly for refs 3`] = ` +<span + className="max-width-100 display-inline-flex-center" +> + <span + className="text-ellipsis" + title="Foo + +foo" + > + <Link + className="link-with-icon" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": undefined, + "id": "src/main/ts/app", + }, + } + } + > + <QualifierIcon + qualifier="TRK" + /> + + <span> + Foo + </span> + </Link> + </span> + <span + className="spacer-left badge flex-1" + > + branches.main_branch + </span> +</span> +`; + 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`] = ` <ComponentsHeader baseComponent={ Object { + "branch": "develop", "key": "foo", "name": "Foo", "qualifier": "TRK", @@ -20,6 +21,7 @@ exports[`renders correctly 1`] = ` } rootComponent={ Object { + "branch": "develop", "key": "foo", "name": "Foo", "qualifier": "TRK", @@ -31,6 +33,7 @@ exports[`renders correctly 1`] = ` canBePinned={true} component={ Object { + "branch": "develop", "key": "foo", "name": "Foo", "qualifier": "TRK", @@ -50,6 +53,7 @@ exports[`renders correctly 1`] = ` } rootComponent={ Object { + "branch": "develop", "key": "foo", "name": "Foo", "qualifier": "TRK", @@ -79,13 +83,14 @@ exports[`renders correctly 1`] = ` canBrowse={true} component={ Object { + "branch": "develop", "key": "foo", "name": "Foo", "qualifier": "TRK", } } hasBaseComponent={true} - key="foo" + key="foo/develop" metrics={ Array [ Object { @@ -98,6 +103,7 @@ exports[`renders correctly 1`] = ` } rootComponent={ Object { + "branch": "develop", "key": "foo", "name": "Foo", "qualifier": "TRK", @@ -127,16 +133,18 @@ exports[`renders correctly for a search 1`] = ` canBrowse={true} component={ Object { + "branch": "develop", "key": "foo", "name": "Foo", "qualifier": "TRK", } } hasBaseComponent={false} - key="foo" + key="foo/develop" metrics={Array []} rootComponent={ Object { + "branch": "develop", "key": "foo", "name": "Foo", "qualifier": "TRK", @@ -164,6 +172,7 @@ exports[`renders correctly for leak 1`] = ` <ComponentsHeader baseComponent={ Object { + "branch": "develop", "key": "foo", "name": "Foo", "qualifier": "TRK", @@ -177,6 +186,7 @@ exports[`renders correctly for leak 1`] = ` } rootComponent={ Object { + "branch": "develop", "key": "foo", "name": "Foo", "qualifier": "TRK", @@ -196,6 +206,7 @@ exports[`renders correctly for leak 1`] = ` canBePinned={true} component={ Object { + "branch": "develop", "key": "foo", "name": "Foo", "qualifier": "TRK", @@ -215,6 +226,7 @@ exports[`renders correctly for leak 1`] = ` } rootComponent={ Object { + "branch": "develop", "key": "foo", "name": "Foo", "qualifier": "TRK", @@ -252,13 +264,14 @@ exports[`renders correctly for leak 1`] = ` canBrowse={true} component={ Object { + "branch": "develop", "key": "foo", "name": "Foo", "qualifier": "TRK", } } hasBaseComponent={true} - key="foo" + key="foo/develop" metrics={ Array [ Object { @@ -271,6 +284,7 @@ exports[`renders correctly for leak 1`] = ` } rootComponent={ Object { + "branch": "develop", "key": "foo", "name": "Foo", "qualifier": "TRK", diff --git a/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts index 22594996a15..8b8e75c7701 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts @@ -196,31 +196,3 @@ describe('extract measure', () => { }); }); }); - -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<Props> { handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => { 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<Props, State> { 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<Props, State> { @@ -125,15 +127,21 @@ export default class MeasureContent extends React.PureComponent<Props, State> { 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<Props, State> { 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<Props, State> { 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 ( - <TreeMapView - branchLike={this.props.branchLike} - components={this.state.components} - handleSelect={this.onOpenComponent} - metric={metric} - /> - ); } + + return ( + <TreeMapView + branchLike={this.props.branchLike} + components={this.state.components} + handleSelect={this.onOpenComponent} + metric={metric} + /> + ); } render() { @@ -307,7 +324,7 @@ export default class MeasureContent extends React.PureComponent<Props, State> { 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<Props, State> { } right={ <div className="display-flex-center"> - {!isFile && metric && ( + {!isFileComponent && metric && ( <> <div>{translate('component_measures.view_as')}</div> <MeasureViewSelect @@ -368,7 +385,7 @@ export default class MeasureContent extends React.PureComponent<Props, State> { metric={metric} secondaryMeasure={secondaryMeasure} /> - {isFile ? ( + {isFileComponent ? ( <div className="measure-details-viewer"> <SourceViewer branchLike={branchLike} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx index abc99393977..95842447139 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx @@ -25,14 +25,9 @@ import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import PageActions from '../../../components/ui/PageActions'; import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; import { BranchLike } from '../../../types/branch-like'; +import { isFile } from '../../../types/component'; import BubbleChart from '../drilldown/BubbleChart'; -import { - BUBBLES_FETCH_LIMIT, - enhanceComponent, - getBubbleMetrics, - hasFullMeasures, - isFileType -} from '../utils'; +import { BUBBLES_FETCH_LIMIT, enhanceComponent, getBubbleMetrics, hasFullMeasures } from '../utils'; import Breadcrumbs from './Breadcrumbs'; import LeakPeriodLegend from './LeakPeriodLegend'; import MeasureContentHeader from './MeasureContentHeader'; @@ -48,7 +43,7 @@ interface Props { onIssueChange?: (issue: T.Issue) => void; rootComponent: T.ComponentMeasure; updateLoading: (param: T.Dict<boolean>) => void; - updateSelected: (component: string) => void; + updateSelected: (component: T.ComponentMeasureIntern) => void; } interface State { @@ -82,7 +77,7 @@ export default class MeasureOverview extends React.PureComponent<Props, State> { 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<Props, State> { const { branchLike, component, domain, metrics } = this.props; const { paging } = this.state; - if (isFileType(component)) { + if (isFile(component.qualifier)) { return ( <div className="measure-details-viewer"> <SourceViewer diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx index d3572d07bbb..0fb21b2703e 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx @@ -23,7 +23,8 @@ import { getComponentShow } from '../../../api/components'; import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; import { getProjectUrl } from '../../../helpers/urls'; import { BranchLike } from '../../../types/branch-like'; -import { isViewType, Query } from '../utils'; +import { isView } from '../../../types/component'; +import { Query } from '../utils'; import MeasureOverview from './MeasureOverview'; interface Props { @@ -102,12 +103,12 @@ export default class MeasureOverviewContainer extends React.PureComponent<Props, } }; - updateSelected = (component: string) => { - 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<T.Metric>; 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<Props, State> { }; getTooltip( - componentName: string, + component: T.ComponentMeasureEnhanced, values: { x: number; y: number; size: number; colors?: Array<number | undefined> }, 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<Props, State> { }; 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<Props, State> { 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) { <QualifierIcon className="little-spacer-right" qualifier={component.qualifier} /> {head.length > 0 && <span className="note">{head}/</span>} <span>{tail}</span> - {isApplication(rootComponent.qualifier) && + {(isApplication(rootComponent.qualifier) || isPortfolioLike(rootComponent.qualifier)) && (component.branch ? ( <> <BranchIcon className="spacer-left little-spacer-right" /> 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<T.Metric>; 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 => ( <ComponentsListRow component={component} - isSelected={component.key === props.selectedComponent} - key={component.key} + isSelected={ + getComponentMeasureUniqueKey(component) === + getComponentMeasureUniqueKey(props.selectedComponent) + } + key={getComponentMeasureUniqueKey(component)} metric={metric} otherMetrics={otherMetrics} {...props} diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx index fb3819160e5..8eb9b7403a8 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx @@ -35,14 +35,14 @@ interface Props { components: T.ComponentMeasureEnhanced[]; defaultShowBestMeasures: boolean; fetchMore: () => 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<T.Metric>; 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<Props, State> { 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<Props, State> { }; 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<Props, State> { 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<Props, State> { 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<Props, State> { 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<Props, State> { color: colorValue ? (colorScale as Function)(colorValue) : undefined, gradient: !colorValue ? NA_GRADIENT : undefined, icon: <QualifierIcon fill={colors.baseFontColor} qualifier={component.qualifier} />, - 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<Props, State> { 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<Props, State> { colorMetric && colorValue !== undefined ? formatMeasure(colorValue, colorMetric.type) : '—'; return ( <div className="text-left"> - {componentName} + {[component.name, component.branch].filter(s => !!s).join(' / ')} <br /> {`${getLocalizedMetricName(sizeMetric)}: ${formatMeasure(sizeValue, sizeMetric.type)}`} <br /> 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<ComponentCellProps> = {}, 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`] = ` <td className="measure-details-component-cell" > @@ -9,87 +9,47 @@ exports[`should render correctly: default 1`] = ` > <Link className="link-no-underline" - id="component-measures-component-link-foo:src/index.tsx" + id="component-measures-component-link-foo" onlyActiveOnIndex={false} style={Object {}} to={ Object { "pathname": "/component_measures", "query": Object { + "branch": "develop", "id": "foo", "metric": "bugs", - "selected": "foo:src/index.tsx", + "selected": "foo", "view": "list", }, } } > <span - title="foo:src/index.tsx" - > - <QualifierIcon - className="little-spacer-right" - qualifier="FIL" - /> - <span - className="note" - > - src - / - </span> - <span> - index.tsx - </span> - </span> - </Link> - </div> -</td> -`; - -exports[`should render correctly: ref application component, alert_status metric 1`] = ` -<td - className="measure-details-component-cell" -> - <div - className="text-ellipsis" - > - <Link - className="link-no-underline" - id="component-measures-component-link-foo" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "app-key", - }, - } - } - > - <span - className="big-spacer-right" - > - <LinkIcon /> - </span> - <span title="foo" > <QualifierIcon className="little-spacer-right" - qualifier="APP" + qualifier="TRK" /> <span> Foo </span> + <BranchIcon + className="spacer-left little-spacer-right" + /> + <span + className="note" + > + develop + </span> </span> </Link> </div> </td> `; -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`] = ` <td className="measure-details-component-cell" > @@ -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", }, } } > <span - className="big-spacer-right" - > - <LinkIcon /> - </span> - <span title="foo" > <QualifierIcon className="little-spacer-right" - qualifier="APP" + qualifier="TRK" /> <span> Foo </span> + <span + className="spacer-left badge" + > + branches.main_branch + </span> </span> </Link> </div> </td> `; -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`] = ` <td className="measure-details-component-cell" > @@ -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", }, } } > <span - className="big-spacer-right" - > - <LinkIcon /> - </span> - <span title="foo" > <QualifierIcon className="little-spacer-right" - qualifier="VW" + qualifier="TRK" /> <span> Foo @@ -179,7 +135,7 @@ exports[`should render correctly: ref portfolio component, alert_status metric 1 </td> `; -exports[`should render correctly: ref project component 1`] = ` +exports[`should render correctly for a "TRK" root component and a component with branch "undefined" 1`] = ` <td className="measure-details-component-cell" > @@ -195,19 +151,15 @@ 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", }, } } > <span - className="big-spacer-right" - > - <LinkIcon /> - </span> - <span title="foo" > <QualifierIcon @@ -223,7 +175,7 @@ exports[`should render correctly: ref project component 1`] = ` </td> `; -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`] = ` <td className="measure-details-component-cell" > @@ -237,20 +189,18 @@ 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", }, } } > <span - className="big-spacer-right" - > - <LinkIcon /> - </span> - <span title="foo" > <QualifierIcon @@ -260,48 +210,13 @@ exports[`should render correctly: ref project component, projects 1`] = ` <span> Foo </span> - </span> - </Link> - </div> -</td> -`; - -exports[`should render correctly: ref project component, releasability metric 1`] = ` -<td - className="measure-details-component-cell" -> - <div - className="text-ellipsis" - > - <Link - className="link-no-underline" - id="component-measures-component-link-foo" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/dashboard", - "query": Object { - "branch": "branch-6.7", - "id": "project-key", - }, - } - } - > - <span - className="big-spacer-right" - > - <LinkIcon /> - </span> - <span - title="foo" - > - <QualifierIcon - className="little-spacer-right" - qualifier="TRK" + <BranchIcon + className="spacer-left little-spacer-right" /> - <span> - Foo + <span + className="note" + > + develop </span> </span> </Link> @@ -309,7 +224,7 @@ exports[`should render correctly: ref project component, releasability metric 1` </td> `; -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`] = ` <td className="measure-details-component-cell" > @@ -343,13 +258,10 @@ exports[`should render correctly: root component is application, component has b <span> Foo </span> - <BranchIcon - className="spacer-left little-spacer-right" - /> <span - className="note" + className="spacer-left badge" > - develop + branches.main_branch </span> </span> </Link> @@ -357,7 +269,7 @@ exports[`should render correctly: root component is application, component has b </td> `; -exports[`should render correctly: root component is application, component is on main branch 1`] = ` +exports[`should render correctly: default 1`] = ` <td className="measure-details-component-cell" > @@ -397,51 +309,6 @@ exports[`should render correctly: root component is application, component is on <span> index.tsx </span> - <span - className="spacer-left badge" - > - branches.main_branch - </span> - </span> - </Link> - </div> -</td> -`; - -exports[`should render correctly: sub-portfolio component 1`] = ` -<td - className="measure-details-component-cell" -> - <div - className="text-ellipsis" - > - <Link - className="link-no-underline" - id="component-measures-component-link-svw-bar" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/component_measures", - "query": Object { - "id": "foo", - "metric": "bugs", - "selected": "svw-bar", - "view": "list", - }, - } - } - > - <span - title="svw-bar" - > - <QualifierIcon - className="little-spacer-right" - qualifier="SVW" - /> - <span> - Foo - </span> </span> </Link> </div> 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 ( <div @@ -52,8 +65,15 @@ export default function ComponentBreadcrumbs({ <QualifierIcon className="spacer-right" qualifier={issue.componentQualifier} /> {displayProject && ( - <span title={issue.projectName}> + <span title={projectName}> {limitComponentName(issue.projectName)} + {issue.branch && ( + <> + {' - '} + <BranchIcon /> + <span>{issue.branch}</span> + </> + )} <span className="slash-separator" /> </span> )} 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<Props> { 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 ( <li className="issues-workspace-list-item"> 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<ListItem['props']> = {}) { + return shallow<ListItem>( + <ListItem + branchLike={mockBranch()} + checked={false} + component={mockComponent()} + issue={mockIssue()} + onChange={jest.fn()} + onCheck={jest.fn()} + onClick={jest.fn()} + onFilterChange={jest.fn()} + onPopupToggle={jest.fn()} + openPopup={undefined} + previousIssue={mockIssue(false, { branch: 'branch-8.7' })} + selected={false} + {...props} + /> + ); +} 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" /> <span - title="proj-name" + title="proj-name - test-branch" > proj-name + - + <BranchIcon /> + <span> + test-branch + </span> <span className="slash-separator" /> @@ -35,9 +40,14 @@ exports[`renders with sub-project 1`] = ` qualifier="FIL" /> <span - title="proj-name" + title="proj-name - test-branch" > proj-name + - + <BranchIcon /> + <span> + test-branch + </span> <span className="slash-separator" /> 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`] = ` +<li + className="issues-workspace-list-item" +> + <div + className="issues-workspace-list-component note" + > + <ComponentBreadcrumbs + component={ + 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 [], + } + } + issue={ + Object { + "actions": Array [], + "component": "main.js", + "componentLongName": "main.js", + "componentQualifier": "FIL", + "componentUuid": "foo1234", + "creationDate": "2017-03-01T09:36:01+0100", + "flows": Array [], + "fromHotspot": false, + "key": "AVsae-CQS-9G3txfbFN2", + "line": 25, + "message": "Reduce the number of conditional operators (4) used in the expression", + "project": "myproject", + "projectKey": "foo", + "projectName": "Foo", + "rule": "javascript:S1067", + "ruleName": "foo", + "secondaryLocations": Array [], + "severity": "MAJOR", + "status": "OPEN", + "textRange": Object { + "endLine": 26, + "endOffset": 15, + "startLine": 25, + "startOffset": 0, + }, + "transitions": Array [], + "type": "BUG", + } + } + /> + </div> + <Issue + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": false, + "name": "branch-6.7", + } + } + checked={false} + issue={ + Object { + "actions": Array [], + "component": "main.js", + "componentLongName": "main.js", + "componentQualifier": "FIL", + "componentUuid": "foo1234", + "creationDate": "2017-03-01T09:36:01+0100", + "flows": Array [], + "fromHotspot": false, + "key": "AVsae-CQS-9G3txfbFN2", + "line": 25, + "message": "Reduce the number of conditional operators (4) used in the expression", + "project": "myproject", + "projectKey": "foo", + "projectName": "Foo", + "rule": "javascript:S1067", + "ruleName": "foo", + "secondaryLocations": Array [], + "severity": "MAJOR", + "status": "OPEN", + "textRange": Object { + "endLine": 26, + "endOffset": 15, + "startLine": 25, + "startOffset": 0, + }, + "transitions": Array [], + "type": "BUG", + } + } + onChange={[MockFunction]} + onCheck={[MockFunction]} + onClick={[MockFunction]} + onFilter={[Function]} + onPopupToggle={[MockFunction]} + selected={false} + /> +</li> +`; 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: ( <Link to={getComponentDrilldownUrl({ componentKey: component, @@ -53,8 +53,8 @@ export default function Effort({ component, effort, metricKey }: Props) { value={String(effort.projects)} /> {effort.projects === 1 - ? translate('project_singular') - : translate('project_plural')} + ? translate('portfolio.project_branch') + : translate('portfolio.project_branches')} </span> </Link> ), 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 && ( <> - <h3>{translate('portfolio.lowest_rated_projects')}</h3> + <h3>{translate('portfolio.lowest_rated_project_branches')}</h3> <div className="portfolio-effort"> <Link to={getComponentDrilldownUrl({ @@ -88,8 +88,8 @@ export default function MetricBox({ component, measures, metricKey }: MetricBoxP value={effort} /> {Number(effort) === 1 - ? translate('project_singular') - : translate('project_plural')} + ? translate('portfolio.project_branch') + : translate('portfolio.project_branches')} </span> </Link> <Level @@ -107,7 +107,7 @@ export default function MetricBox({ component, measures, metricKey }: MetricBoxP ) : effort && ( <> - <h3>{translate('portfolio.lowest_rated_projects')}</h3> + <h3>{translate('portfolio.lowest_rated_project_branches')}</h3> <Effort component={component} effort={effort} metricKey={keys.rating} /> </> )} 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 ( <div className="panel panel-white portfolio-sub-components" id="portfolio-sub-components"> <table className="data zebra"> @@ -75,26 +83,39 @@ export default function WorstProjects({ component, subComponents, total }: Props </tr> </thead> <tbody> - {subComponents.map(component => ( - <tr key={component.key}> + {subCompList.map(comp => ( + <tr key={[comp.key, comp.branch].filter(s => !!s).join('/')}> <td> - <Link - className="link-with-icon" - to={getComponentOverviewUrl( - component.refKey || component.key, - component.qualifier - )}> - <QualifierIcon qualifier={component.qualifier} /> {component.name} - </Link> + <span className="display-flex-center"> + <QualifierIcon className="spacer-right" qualifier={comp.qualifier} /> + <Link + to={getComponentOverviewUrl(comp.refKey || comp.key, comp.qualifier, { + branch: comp.branch + })}> + {comp.name} + </Link> + + {[ComponentQualifier.Application, ComponentQualifier.Project].includes( + comp.qualifier as ComponentQualifier + ) && + (comp.branch ? ( + <span className="spacer-left"> + <BranchIcon className="little-spacer-right" /> + <span className="note">{comp.branch}</span> + </span> + ) : ( + <span className="spacer-left badge">{translate('branches.main_branch')}</span> + ))} + </span> </td> - {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)} </tr> ))} </tbody> 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": <Link + "project_branches": <Link onlyActiveOnIndex={false} style={Object {}} to={ @@ -30,7 +30,7 @@ exports[`renders 1`] = ` metricType="SHORT_INT" value="3" /> - project_plural + portfolio.project_branches </span> </Link>, "rating": <Rating diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MetricBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MetricBox-test.tsx.snap index 9abbbb19522..72ceee3b74b 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MetricBox-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MetricBox-test.tsx.snap @@ -26,7 +26,7 @@ exports[`should render correctly 1`] = ` rating="3" /> <h3> - portfolio.lowest_rated_projects + portfolio.lowest_rated_project_branches </h3> <Effort component="foo" @@ -84,7 +84,7 @@ exports[`should render correctly for releasability 1`] = ` rating="2" /> <h3> - portfolio.lowest_rated_projects + portfolio.lowest_rated_project_branches </h3> <div className="portfolio-effort" @@ -109,7 +109,7 @@ exports[`should render correctly for releasability 1`] = ` metricType="SHORT_INT" value={5} /> - project_plural + portfolio.project_branches </span> </Link> <Level @@ -165,7 +165,7 @@ exports[`should render correctly for releasability 2`] = ` rating="2" /> <h3> - portfolio.lowest_rated_projects + portfolio.lowest_rated_project_branches </h3> <div className="portfolio-effort" @@ -190,7 +190,7 @@ exports[`should render correctly for releasability 2`] = ` metricType="SHORT_INT" value={1} /> - project_singular + portfolio.project_branch </span> </Link> <Level diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap index 9ed6966558b..1fcc3521891 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap @@ -47,28 +47,136 @@ exports[`renders 1`] = ` </thead> <tbody> <tr - key="foo" + key="foo_app" > <td> - <Link - className="link-with-icon" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/portfolio", - "query": Object { - "id": "foo", - }, + <span + className="display-flex-center" + > + <QualifierIcon + className="spacer-right" + qualifier="APP" + /> + <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": undefined, + "id": "foo_app", + }, + } } - } + > + Foo + </Link> + <span + className="spacer-left badge" + > + branches.main_branch + </span> + </span> + </td> + <td + className="text-center" + > + <Measure + metricKey="releasability_rating" + metricType="RATING" + value="3" + /> + </td> + <td + className="text-center" + > + <Measure + metricKey="reliability_rating" + metricType="RATING" + value="2" + /> + </td> + <td + className="text-center" + > + <Measure + metricKey="security_rating" + metricType="RATING" + value="1" + /> + </td> + <td + className="text-center" + > + <Measure + metricKey="security_review_rating" + metricType="RATING" + /> + </td> + <td + className="text-center" + > + <Measure + metricKey="sqale_rating" + metricType="RATING" + value="4" + /> + </td> + <td + className="text-right" + > + <span + className="note" + > + <Measure + metricKey="ncloc" + metricType="SHORT_INT" + value="200" + /> + </span> + <svg + className="spacer-left" + height="16" + width="50" + > + <rect + className="bar-chart-bar" + fill="#4b9fd5" + height="10" + width={50} + x="0" + y="3" + /> + </svg> + </td> + </tr> + <tr + key="foo" + > + <td> + <span + className="display-flex-center" > <QualifierIcon + className="spacer-right" qualifier="SVW" /> - - Foo - </Link> + <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/portfolio", + "query": Object { + "id": "foo", + }, + } + } + > + Foo + </Link> + </span> </td> <td className="text-center" @@ -143,28 +251,149 @@ exports[`renders 1`] = ` </td> </tr> <tr - key="bar" + key="bar/branch-1" > <td> - <Link - className="link-with-icon" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/dashboard", - "query": Object { - "id": "barbar", - }, + <span + className="display-flex-center" + > + <QualifierIcon + className="spacer-right" + qualifier="TRK" + /> + <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": "branch-1", + "id": "barbar", + }, + } } - } + > + Bar + </Link> + <span + className="spacer-left" + > + <BranchIcon + className="little-spacer-right" + /> + <span + className="note" + > + branch-1 + </span> + </span> + </span> + </td> + <td + className="text-center" + > + <Measure + metricKey="alert_status" + metricType="LEVEL" + value="ERROR" + /> + </td> + <td + className="text-center" + > + <Measure + metricKey="reliability_rating" + metricType="RATING" + value="2" + /> + </td> + <td + className="text-center" + > + <Measure + metricKey="security_rating" + metricType="RATING" + value="1" + /> + </td> + <td + className="text-center" + > + <Measure + metricKey="security_review_rating" + metricType="RATING" + /> + </td> + <td + className="text-center" + > + <Measure + metricKey="sqale_rating" + metricType="RATING" + value="4" + /> + </td> + <td + className="text-right" + > + <span + className="note" + > + <Measure + metricKey="ncloc" + metricType="SHORT_INT" + value="100" + /> + </span> + <svg + className="spacer-left" + height="16" + width="50" + > + <rect + className="bar-chart-bar" + fill="#4b9fd5" + height="10" + width={25} + x="0" + y="3" + /> + </svg> + </td> + </tr> + <tr + key="bar" + > + <td> + <span + className="display-flex-center" > <QualifierIcon + className="spacer-right" qualifier="TRK" /> - - Bar - </Link> + <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": undefined, + "id": "barbar", + }, + } + } + > + Bar + </Link> + <span + className="spacer-left badge" + > + branches.main_branch + </span> + </span> </td> <td className="text-center" @@ -242,25 +471,34 @@ exports[`renders 1`] = ` key="baz" > <td> - <Link - className="link-with-icon" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/dashboard", - "query": Object { - "id": "bazbaz", - }, - } - } + <span + className="display-flex-center" > <QualifierIcon + className="spacer-right" qualifier="TRK" /> - - Baz - </Link> + <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": undefined, + "id": "bazbaz", + }, + } + } + > + Baz + </Link> + <span + className="spacer-left badge" + > + branches.main_branch + </span> + </span> </td> <td className="text-center" diff --git a/server/sonar-web/src/main/js/apps/portfolio/types.ts b/server/sonar-web/src/main/js/apps/portfolio/types.ts index 83f014815d5..c3e3a64dca7 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/types.ts +++ b/server/sonar-web/src/main/js/apps/portfolio/types.ts @@ -23,4 +23,5 @@ export interface SubComponent { name: string; refKey?: string; qualifier: string; + branch?: string; } diff --git a/server/sonar-web/src/main/js/components/charts/TreeMap.tsx b/server/sonar-web/src/main/js/components/charts/TreeMap.tsx index 3a6e25112a7..30eef58f938 100644 --- a/server/sonar-web/src/main/js/components/charts/TreeMap.tsx +++ b/server/sonar-web/src/main/js/components/charts/TreeMap.tsx @@ -35,6 +35,7 @@ export interface TreeMapItem { metric?: { key: string; type: string }; size: number; tooltip?: React.ReactNode; + component: T.ComponentMeasureEnhanced; } interface HierarchicalTreemapItem extends TreeMapItem { @@ -44,7 +45,7 @@ interface HierarchicalTreemapItem extends TreeMapItem { interface Props { height: number; items: TreeMapItem[]; - onRectangleClick?: (item: string) => void; + onRectangleClick?: (item: T.ComponentMeasureEnhanced) => void; width: number; } @@ -64,6 +65,12 @@ export default class TreeMap extends React.PureComponent<Props> { 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<Props> { 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<Props, St render() { return ( - <li className={classNames({ 'select-list-list-disabled': this.props.disabled })}> + <li + className={classNames({ + 'select-list-list-disabled': this.props.disabled + })}> <Checkbox checked={this.props.selected} - className={classNames('select-list-list-checkbox', { active: this.props.active })} + className={classNames('select-list-list-checkbox display-flex-center', { + active: this.props.active + })} disabled={this.props.disabled} loading={this.state.loading} onCheck={this.handleCheck}> - <span className="little-spacer-left">{this.props.renderElement(this.props.element)}</span> + <span className="little-spacer-left flex-1"> + {this.props.renderElement(this.props.element)} + </span> </Checkbox> </li> ); 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`] = ` > <Checkbox checked={false} - className="select-list-list-checkbox" + className="select-list-list-checkbox display-flex-center" loading={false} onCheck={[Function]} thirdState={false} > <span - className="little-spacer-left" + className="little-spacer-left flex-1" > foo </span> @@ -26,13 +26,13 @@ exports[`should display a loader when checking 2`] = ` > <Checkbox checked={false} - className="select-list-list-checkbox" + className="select-list-list-checkbox display-flex-center" loading={true} onCheck={[Function]} thirdState={false} > <span - className="little-spacer-left" + className="little-spacer-left flex-1" > foo </span> 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<P>( 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; |