aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js
diff options
context:
space:
mode:
authorPhilippe Perrin <philippe.perrin@sonarsource.com>2021-10-20 10:48:08 +0200
committersonartech <sonartech@sonarsource.com>2021-11-09 20:03:16 +0000
commite03b2bf40be7de4821cdf009c74dffc90b87757b (patch)
tree0348599747bae1b6e74b52d9efbc8f05013250aa /server/sonar-web/src/main/js
parente8905886d0e77cc080022dff7e0df79e4823e5de (diff)
downloadsonarqube-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')
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts4
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx23
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Components.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentName-test.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/Components-test.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap104
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Components-test.tsx.snap20
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts28
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx87
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx59
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx27
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx18
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/ComponentCell-test.tsx186
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentCell-test.tsx.snap231
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/utils.ts14
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/ListItem-test.tsx51
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap14
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap115
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/MetricBox.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx59
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/WorstProjects-test.tsx33
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MetricBox-test.tsx.snap10
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap328
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/types.ts1
-rw-r--r--server/sonar-web/src/main/js/components/charts/TreeMap.tsx11
-rw-r--r--server/sonar-web/src/main/js/components/charts/__tests__/TreeMap-test.tsx22
-rw-r--r--server/sonar-web/src/main/js/components/controls/SelectListListElement.tsx13
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SelectListListElement-test.tsx.snap8
-rw-r--r--server/sonar-web/src/main/js/components/hoc/withKeyboardNavigation.tsx8
-rw-r--r--server/sonar-web/src/main/js/helpers/component.ts25
-rw-r--r--server/sonar-web/src/main/js/types/__tests__/__snapshots__/component-test.ts.snap71
-rw-r--r--server/sonar-web/src/main/js/types/__tests__/component-test.ts39
-rw-r--r--server/sonar-web/src/main/js/types/component.ts14
-rw-r--r--server/sonar-web/src/main/js/types/types.d.ts2
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;