Browse Source

SONAR-15498 Manual selection of project's branches for portfolio

Display portfolio's children branch information and group issues by project and branch
tags/9.2.0.49834
Philippe Perrin 2 years ago
parent
commit
e03b2bf40b
48 changed files with 1211 additions and 588 deletions
  1. 3
    1
      server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts
  2. 1
    1
      server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx
  3. 18
    5
      server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx
  4. 13
    4
      server/sonar-web/src/main/js/apps/code/components/Components.tsx
  5. 14
    3
      server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentName-test.tsx
  6. 8
    2
      server/sonar-web/src/main/js/apps/code/components/__tests__/Components-test.tsx
  7. 62
    42
      server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap
  8. 17
    3
      server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Components-test.tsx.snap
  9. 0
    28
      server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts
  10. 2
    2
      server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.tsx
  11. 2
    2
      server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.tsx
  12. 52
    35
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
  13. 5
    10
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx
  14. 6
    5
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx
  15. 9
    6
      server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx
  16. 26
    33
      server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx
  17. 7
    3
      server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx
  18. 15
    12
      server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx
  19. 10
    8
      server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx
  20. 110
    76
      server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/ComponentCell-test.tsx
  21. 49
    182
      server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentCell-test.tsx.snap
  22. 0
    14
      server/sonar-web/src/main/js/apps/component-measures/utils.ts
  23. 23
    3
      server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx
  24. 4
    1
      server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx
  25. 2
    1
      server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx
  26. 51
    0
      server/sonar-web/src/main/js/apps/issues/components/__tests__/ListItem-test.tsx
  27. 12
    2
      server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap
  28. 115
    0
      server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap
  29. 3
    3
      server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx
  30. 4
    4
      server/sonar-web/src/main/js/apps/portfolio/components/MetricBox.tsx
  31. 40
    19
      server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx
  32. 30
    3
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/WorstProjects-test.tsx
  33. 2
    2
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap
  34. 5
    5
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MetricBox-test.tsx.snap
  35. 283
    45
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap
  36. 1
    0
      server/sonar-web/src/main/js/apps/portfolio/types.ts
  37. 9
    2
      server/sonar-web/src/main/js/components/charts/TreeMap.tsx
  38. 18
    4
      server/sonar-web/src/main/js/components/charts/__tests__/TreeMap-test.tsx
  39. 10
    3
      server/sonar-web/src/main/js/components/controls/SelectListListElement.tsx
  40. 4
    4
      server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SelectListListElement-test.tsx.snap
  41. 7
    1
      server/sonar-web/src/main/js/components/hoc/withKeyboardNavigation.tsx
  42. 25
    0
      server/sonar-web/src/main/js/helpers/component.ts
  43. 71
    0
      server/sonar-web/src/main/js/types/__tests__/__snapshots__/component-test.ts.snap
  44. 39
    0
      server/sonar-web/src/main/js/types/__tests__/component-test.ts
  45. 14
    0
      server/sonar-web/src/main/js/types/component.ts
  46. 1
    1
      server/sonar-web/src/main/js/types/types.d.ts
  47. 7
    6
      sonar-core/src/main/resources/org/sonar/l10n/core.properties
  48. 2
    2
      sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java

+ 3
- 1
server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts View File

@@ -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,

+ 1
- 1
server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx View File

@@ -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));
}

+ 18
- 5
server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx View File

@@ -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)}>

+ 13
- 4
server/sonar-web/src/main/js/apps/code/components/Components.tsx View File

@@ -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)
}
/>
))
) : (

+ 14
- 3
server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentName-test.tsx View File

@@ -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();
});

+ 8
- 2
server/sonar-web/src/main/js/apps/code/components/__tests__/Components-test.tsx View File

@@ -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' });


+ 62
- 42
server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap View File

@@ -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


+ 17
- 3
server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Components-test.tsx.snap View File

@@ -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",

+ 0
- 28
server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts View File

@@ -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);
});
});
});

+ 2
- 2
server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.tsx View File

@@ -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() {

+ 2
- 2
server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.tsx View File

@@ -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;
});

+ 52
- 35
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx View File

@@ -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}

+ 5
- 10
server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx View File

@@ -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

+ 6
- 5
server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx View File

@@ -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
});
}
};

+ 9
- 6
server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx View File

@@ -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);

+ 26
- 33
server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx View File

@@ -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" />

+ 7
- 3
server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx View File

@@ -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}

+ 15
- 12
server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx View File

@@ -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 && (

+ 10
- 8
server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx View File

@@ -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 />

+ 110
- 76
server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/ComponentCell-test.tsx View File

@@ -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 });

+ 49
- 182
server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentCell-test.tsx.snap View File

@@ -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,18 +151,14 @@ exports[`should render correctly: ref project component 1`] = `
Object {
"pathname": "/component_measures",
"query": Object {
"id": "project-key",
"id": "foo",
"metric": "bugs",
"selected": "foo",
"view": "list",
},
}
}
>
<span
className="big-spacer-right"
>
<LinkIcon />
</span>
<span
title="foo"
>
@@ -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,19 +189,17 @@ exports[`should render correctly: ref project component, projects 1`] = `
style={Object {}}
to={
Object {
"pathname": "/dashboard",
"pathname": "/component_measures",
"query": Object {
"branch": "branch-6.7",
"id": "project-key",
"branch": "develop",
"id": "foo",
"metric": "bugs",
"selected": "foo",
"view": "list",
},
}
}
>
<span
className="big-spacer-right"
>
<LinkIcon />
</span>
<span
title="foo"
>
@@ -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>

+ 0
- 14
server/sonar-web/src/main/js/apps/component-measures/utils.ts View File

@@ -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,

+ 23
- 3
server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx View File

@@ -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>
)}

+ 4
- 1
server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx View File

@@ -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">

+ 2
- 1
server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx View File

@@ -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', () => {

+ 51
- 0
server/sonar-web/src/main/js/apps/issues/components/__tests__/ListItem-test.tsx View File

@@ -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}
/>
);
}

+ 12
- 2
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap View File

@@ -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"
/>

+ 115
- 0
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap View File

@@ -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>
`;

+ 3
- 3
server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx View File

@@ -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>
),

+ 4
- 4
server/sonar-web/src/main/js/apps/portfolio/components/MetricBox.tsx View File

@@ -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} />
</>
)}

+ 40
- 19
server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx View File

@@ -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>

+ 30
- 3
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/WorstProjects-test.tsx View File

@@ -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'
}
];

+ 2
- 2
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap View File

@@ -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

+ 5
- 5
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MetricBox-test.tsx.snap View File

@@ -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

+ 283
- 45
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap View File

@@ -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"

+ 1
- 0
server/sonar-web/src/main/js/apps/portfolio/types.ts View File

@@ -23,4 +23,5 @@ export interface SubComponent {
name: string;
refKey?: string;
qualifier: string;
branch?: string;
}

+ 9
- 2
server/sonar-web/src/main/js/components/charts/TreeMap.tsx View File

@@ -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={

+ 18
- 4
server/sonar-web/src/main/js/components/charts/__tests__/TreeMap-test.tsx View File

@@ -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' }));
});

+ 10
- 3
server/sonar-web/src/main/js/components/controls/SelectListListElement.tsx View File

@@ -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>
);

+ 4
- 4
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SelectListListElement-test.tsx.snap View File

@@ -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>

+ 7
- 1
server/sonar-web/src/main/js/components/hoc/withKeyboardNavigation.tsx View File

@@ -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) => {

+ 25
- 0
server/sonar-web/src/main/js/helpers/component.ts View File

@@ -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;
}

+ 71
- 0
server/sonar-web/src/main/js/types/__tests__/__snapshots__/component-test.ts.snap View File

@@ -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,
}
`;

+ 39
- 0
server/sonar-web/src/main/js/types/__tests__/component-test.ts View File

@@ -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();
}
);

+ 14
- 0
server/sonar-web/src/main/js/types/component.ts View File

@@ -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);
}

+ 1
- 1
server/sonar-web/src/main/js/types/types.d.ts View File

@@ -167,7 +167,7 @@ declare namespace T {
name: string;
}

interface ComponentMeasureIntern {
export interface ComponentMeasureIntern {
branch?: string;
description?: string;
isFavorite?: boolean;

+ 7
- 6
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -150,7 +150,6 @@ project_x=Project: {0}
projects=Projects
projects_=project(s)
x_projects_={0} project(s)
project_singular=project
project_plural=projects
projects_management=Projects Management
quality_profile=Quality Profile
@@ -2441,8 +2440,8 @@ metric.profile.description=Selected Quality Profile
metric.profile.name=Profile
metric.profile_version.description=Selected Quality Profile version
metric.profile_version.name=Profile Version
metric.projects.description=Number of projects
metric.projects.name=Projects
metric.projects.description=Number of project branches
metric.projects.name=Project branches
metric.public_api.description=Public API
metric.public_api.name=Public API
metric.public_documented_api_density.description=Public documented classes and functions balanced by ncloc
@@ -4021,7 +4020,9 @@ branch_like_navigation.tutorial_for_ci=Show me how to set up my CI
#------------------------------------------------------------------------------
portfolio.has_always_been_x=has always been {rating}
portfolio.was_x_y=was {rating} {date}
portfolio.x_in_y={projects} in {rating}
portfolio.x_in_y={project_branches} in {rating}
portfolio.project_branch=project branch
portfolio.project_branches=project branches
portfolio.has_qg_status=Has Quality Gate Status
portfolio.have_qg_status=Have Quality Gate Status
portfolio.empty=This portfolio is empty.
@@ -4030,13 +4031,13 @@ portfolio.not_computed=This portfolio is not yet computed.
portfolio.app.empty=This application is empty.
portfolio.app.no_lines_of_code=All projects in this application are empty
portfolio.metric_trend=Metric trend
portfolio.lowest_rated_projects=Lowest rated projects
portfolio.lowest_rated_project_branches=Lowest rated project branches
portfolio.health_factors=Portfolio health factors
portfolio.activity_link=Activity
portfolio.measures_link=Measures
portfolio.language_breakdown_link=Language breakdown
portfolio.breakdown=Portfolio breakdown
portfolio.number_of_projects=Number of projects
portfolio.number_of_projects=Number of project branches
portfolio.number_of_lines=Number of lines of code

portfolio.metric_domain.vulnerabilities=Security Vulnerabilities

+ 2
- 2
sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java View File

@@ -229,8 +229,8 @@ public final class CoreMetrics {
/**
* @since 3.0
*/
public static final Metric<Integer> PROJECTS = new Metric.Builder(PROJECTS_KEY, "Projects", Metric.ValueType.INT)
.setDescription("Number of projects")
public static final Metric<Integer> PROJECTS = new Metric.Builder(PROJECTS_KEY, "Project branches", Metric.ValueType.INT)
.setDescription("Number of project branches")
.setDirection(Metric.DIRECTION_WORST)
.setQualitative(false)
.setDomain(DOMAIN_SIZE)

Loading…
Cancel
Save