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

getBranchLikeQuery, getBranchLikeQuery,
isBranch, isBranch,
isMainBranch, isMainBranch,
isPullRequest
isPullRequest,
sortBranches
} from '../../../helpers/branch-like'; } from '../../../helpers/branch-like';
import { translate, translateWithParameters } from '../../../helpers/l10n'; import { translate, translateWithParameters } from '../../../helpers/l10n';
import * as measures from '../../../helpers/measures'; import * as measures from '../../../helpers/measures';
isBranch, isBranch,
isMainBranch, isMainBranch,
isPullRequest, isPullRequest,
sortBranches,
getStandards, getStandards,
renderCWECategory, renderCWECategory,
renderOwaspTop10Category, renderOwaspTop10Category,

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

const { branchLike, component: rootComponent } = this.props; const { branchLike, component: rootComponent } = this.props;


if (component.refKey) { if (component.refKey) {
this.props.router.push(getProjectUrl(component.refKey));
this.props.router.push(getProjectUrl(component.refKey, component.branch));
} else { } else {
this.props.router.push(getCodeUrl(rootComponent.key, branchLike, component.key)); 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

import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';
import { getProjectUrl } from '../../../helpers/urls'; import { getProjectUrl } from '../../../helpers/urls';
import { BranchLike } from '../../../types/branch-like'; import { BranchLike } from '../../../types/branch-like';
import { ComponentQualifier } from '../../../types/component';


export function getTooltip(component: T.ComponentMeasure) { export function getTooltip(component: T.ComponentMeasure) {
const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS'; const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS';

if (isFile && component.path) { if (isFile && component.path) {
return component.path + '\n\n' + component.key; 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[]) { export function mostCommonPrefix(strings: string[]) {


let inner = null; 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 = ( inner = (
<Link className="link-with-icon" to={getProjectUrl(component.refKey, branch)}> <Link className="link-with-icon" to={getProjectUrl(component.refKey, branch)}>
<QualifierIcon qualifier={component.qualifier} /> <span>{name}</span> <QualifierIcon qualifier={component.qualifier} /> <span>{name}</span>
); );
} }


if (rootComponent.qualifier === 'APP') {
if (
[ComponentQualifier.Application, ComponentQualifier.Portfolio].includes(
rootComponent.qualifier as ComponentQualifier
) &&
[ComponentQualifier.Application, ComponentQualifier.Project].includes(
component.qualifier as ComponentQualifier
)
) {
return ( return (
<span className="max-width-100 display-inline-flex-center"> <span className="max-width-100 display-inline-flex-center">
<span className="text-ellipsis" title={getTooltip(component)}> <span className="text-ellipsis" title={getTooltip(component)}>

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

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * 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 * as React from 'react';
import withKeyboardNavigation from '../../../components/hoc/withKeyboardNavigation'; import withKeyboardNavigation from '../../../components/hoc/withKeyboardNavigation';
import { getComponentMeasureUniqueKey } from '../../../helpers/component';
import { BranchLike } from '../../../types/branch-like'; import { BranchLike } from '../../../types/branch-like';
import { getCodeMetrics } from '../utils'; import { getCodeMetrics } from '../utils';
import Component from './Component'; import Component from './Component';
)} )}


{components.length ? ( {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 <Component
branchLike={branchLike} branchLike={branchLike}
canBePinned={canBePinned} canBePinned={canBePinned}
canBrowse={true} canBrowse={true}
component={component} component={component}
hasBaseComponent={baseComponent !== undefined} hasBaseComponent={baseComponent !== undefined}
key={component.key}
key={getComponentMeasureUniqueKey(component)}
metrics={metrics} metrics={metrics}
previous={index > 0 ? list[index - 1] : undefined} previous={index > 0 ? list[index - 1] : undefined}
rootComponent={rootComponent} 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

import * as React from 'react'; import * as React from 'react';
import { mockMainBranch } from '../../../../helpers/mocks/branch-like'; import { mockMainBranch } from '../../../../helpers/mocks/branch-like';
import { mockComponentMeasure } from '../../../../helpers/mocks/component'; import { mockComponentMeasure } from '../../../../helpers/mocks/component';
import { ComponentQualifier } from '../../../../types/component';
import ComponentName, { getTooltip, mostCommonPrefix, Props } from '../ComponentName'; import ComponentName, { getTooltip, mostCommonPrefix, Props } from '../ComponentName';


describe('#getTooltip', () => { describe('#getTooltip', () => {
component: mockComponentMeasure(false, { component: mockComponentMeasure(false, {
branch: 'foo', branch: 'foo',
refKey: 'src/main/ts/app', refKey: 'src/main/ts/app',
qualifier: 'TRK'
qualifier: ComponentQualifier.Project
}) })
}) })
).toMatchSnapshot(); ).toMatchSnapshot();
component: mockComponentMeasure(false, { component: mockComponentMeasure(false, {
branch: 'foo', branch: 'foo',
refKey: 'src/main/ts/app', 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(); ).toMatchSnapshot();
}); });

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

import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import * as React from 'react'; import * as React from 'react';
import { mockBranch } from '../../../../helpers/mocks/branch-like'; import { mockBranch } from '../../../../helpers/mocks/branch-like';
import { ComponentQualifier } from '../../../../types/component';
import { Components } from '../Components'; 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 METRICS = { coverage: { id: '1', key: 'coverage', type: 'PERCENT', name: 'Coverage' } };
const BRANCH = mockBranch({ name: 'feature' }); const BRANCH = mockBranch({ name: 'feature' });



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



exports[`#ComponentName should render correctly for files 4`] = ` exports[`#ComponentName should render correctly for files 4`] = `
<span <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" 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>
</span> </span>
`; `;


exports[`#ComponentName should render correctly for files 5`] = ` exports[`#ComponentName should render correctly for files 5`] = `
<span <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" 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>
</span> </span>
`; `;
className="max-width-100 display-inline-block text-ellipsis" className="max-width-100 display-inline-block text-ellipsis"
title="Foo title="Foo


foo

foo" foo"
> >
<Link <Link
className="text-ellipsis" className="text-ellipsis"
title="Foo title="Foo


foo

foo" foo"
> >
<Link <Link
</span> </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`] = ` exports[`#getTooltip should correctly format component information 1`] = `
"src/index.tsx "src/index.tsx



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

<ComponentsHeader <ComponentsHeader
baseComponent={ baseComponent={
Object { Object {
"branch": "develop",
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"qualifier": "TRK", "qualifier": "TRK",
} }
rootComponent={ rootComponent={
Object { Object {
"branch": "develop",
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"qualifier": "TRK", "qualifier": "TRK",
canBePinned={true} canBePinned={true}
component={ component={
Object { Object {
"branch": "develop",
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"qualifier": "TRK", "qualifier": "TRK",
} }
rootComponent={ rootComponent={
Object { Object {
"branch": "develop",
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"qualifier": "TRK", "qualifier": "TRK",
canBrowse={true} canBrowse={true}
component={ component={
Object { Object {
"branch": "develop",
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"qualifier": "TRK", "qualifier": "TRK",
} }
} }
hasBaseComponent={true} hasBaseComponent={true}
key="foo"
key="foo/develop"
metrics={ metrics={
Array [ Array [
Object { Object {
} }
rootComponent={ rootComponent={
Object { Object {
"branch": "develop",
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"qualifier": "TRK", "qualifier": "TRK",
canBrowse={true} canBrowse={true}
component={ component={
Object { Object {
"branch": "develop",
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"qualifier": "TRK", "qualifier": "TRK",
} }
} }
hasBaseComponent={false} hasBaseComponent={false}
key="foo"
key="foo/develop"
metrics={Array []} metrics={Array []}
rootComponent={ rootComponent={
Object { Object {
"branch": "develop",
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"qualifier": "TRK", "qualifier": "TRK",
<ComponentsHeader <ComponentsHeader
baseComponent={ baseComponent={
Object { Object {
"branch": "develop",
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"qualifier": "TRK", "qualifier": "TRK",
} }
rootComponent={ rootComponent={
Object { Object {
"branch": "develop",
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"qualifier": "TRK", "qualifier": "TRK",
canBePinned={true} canBePinned={true}
component={ component={
Object { Object {
"branch": "develop",
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"qualifier": "TRK", "qualifier": "TRK",
} }
rootComponent={ rootComponent={
Object { Object {
"branch": "develop",
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"qualifier": "TRK", "qualifier": "TRK",
canBrowse={true} canBrowse={true}
component={ component={
Object { Object {
"branch": "develop",
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"qualifier": "TRK", "qualifier": "TRK",
} }
} }
hasBaseComponent={true} hasBaseComponent={true}
key="foo"
key="foo/develop"
metrics={ metrics={
Array [ Array [
Object { Object {
} }
rootComponent={ rootComponent={
Object { Object {
"branch": "develop",
"key": "foo", "key": "foo",
"name": "Foo", "name": "Foo",
"qualifier": "TRK", "qualifier": "TRK",

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

}); });
}); });
}); });

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

canBrowse: boolean; canBrowse: boolean;
component: T.ComponentMeasure; component: T.ComponentMeasure;
isLast: boolean; isLast: boolean;
handleSelect: (component: string) => void;
handleSelect: (component: T.ComponentMeasureIntern) => void;
} }


export default class Breadcrumb extends React.PureComponent<Props> { export default class Breadcrumb extends React.PureComponent<Props> {
handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => { handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault(); event.preventDefault();
event.currentTarget.blur(); event.currentTarget.blur();
this.props.handleSelect(this.props.component.key);
this.props.handleSelect(this.props.component);
}; };


render() { render() {

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

branchLike?: BranchLike; branchLike?: BranchLike;
className?: string; className?: string;
component: T.ComponentMeasure; component: T.ComponentMeasure;
handleSelect: (component: string) => void;
handleSelect: (component: T.ComponentMeasureIntern) => void;
rootComponent: T.ComponentMeasure; rootComponent: T.ComponentMeasure;
} }


const { breadcrumbs } = this.state; const { breadcrumbs } = this.state;
if (breadcrumbs.length > 1) { if (breadcrumbs.length > 1) {
const idx = this.props.backToFirst ? 0 : breadcrumbs.length - 2; const idx = this.props.backToFirst ? 0 : breadcrumbs.length - 2;
this.props.handleSelect(breadcrumbs[idx].key);
this.props.handleSelect(breadcrumbs[idx]);
} }
return false; return false;
}); });

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

import SourceViewer from '../../../components/SourceViewer/SourceViewer'; import SourceViewer from '../../../components/SourceViewer/SourceViewer';
import PageActions from '../../../components/ui/PageActions'; import PageActions from '../../../components/ui/PageActions';
import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like';
import { getComponentMeasureUniqueKey } from '../../../helpers/component';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';
import { isDiffMetric } from '../../../helpers/measures'; import { isDiffMetric } from '../../../helpers/measures';
import { RequestData } from '../../../helpers/request'; import { RequestData } from '../../../helpers/request';
import { scrollToElement } from '../../../helpers/scrolling'; import { scrollToElement } from '../../../helpers/scrolling';
import { getProjectUrl } from '../../../helpers/urls'; import { getProjectUrl } from '../../../helpers/urls';
import { BranchLike } from '../../../types/branch-like'; import { BranchLike } from '../../../types/branch-like';
import { isFile, isView } from '../../../types/component';
import { MeasurePageView } from '../../../types/measures'; import { MeasurePageView } from '../../../types/measures';
import { MetricKey } from '../../../types/metrics'; import { MetricKey } from '../../../types/metrics';
import { complementary } from '../config/complementary'; import { complementary } from '../config/complementary';
import FilesView from '../drilldown/FilesView'; import FilesView from '../drilldown/FilesView';
import TreeMapView from '../drilldown/TreeMapView'; import TreeMapView from '../drilldown/TreeMapView';
import { enhanceComponent, isFileType, isViewType, Query } from '../utils';
import { enhanceComponent, Query } from '../utils';
import Breadcrumbs from './Breadcrumbs'; import Breadcrumbs from './Breadcrumbs';
import MeasureContentHeader from './MeasureContentHeader'; import MeasureContentHeader from './MeasureContentHeader';
import MeasureHeader from './MeasureHeader'; import MeasureHeader from './MeasureHeader';
metric?: T.Metric; metric?: T.Metric;
paging?: T.Paging; paging?: T.Paging;
secondaryMeasure?: T.Measure; secondaryMeasure?: T.Measure;
selected?: string;
selectedComponent?: T.ComponentMeasureIntern;
} }


export default class MeasureContent extends React.PureComponent<Props, State> { export default class MeasureContent extends React.PureComponent<Props, State> {
measure => measure.metric !== this.props.requestedMetric.key measure => measure.metric !== this.props.requestedMetric.key
); );


this.setState(({ selected }) => ({
this.setState(({ selectedComponent }) => ({
baseComponent: tree.baseComponent, baseComponent: tree.baseComponent,
components, components,
measure, measure,
metric, metric,
paging: tree.paging, paging: tree.paging,
secondaryMeasure, 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
})); }));
} }
}); });
this.props.updateQuery({ view }); 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) { if (this.container) {
this.container.focus(); this.container.focus();
} }
}; };


onSelectComponent = (componentKey: string) => {
this.setState({ selected: componentKey });
onSelectComponent = (component: T.ComponentMeasureIntern) => {
this.setState({ selectedComponent: component });
}; };


getSelectedIndex = () => { 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; return index !== -1 ? index : undefined;
}; };


paging={this.state.paging} paging={this.state.paging}
rootComponent={this.props.rootComponent} rootComponent={this.props.rootComponent}
selectedIdx={selectedIdx} selectedIdx={selectedIdx}
selectedKey={selectedIdx !== undefined ? this.state.selected : undefined}
selectedComponent={
selectedIdx !== undefined
? (this.state.selectedComponent as T.ComponentMeasureEnhanced)
: undefined
}
view={view} 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() { render() {


const measureValue = const measureValue =
measure && (isDiffMetric(measure.metric) ? measure.period?.value : measure.value); measure && (isDiffMetric(measure.metric) ? measure.period?.value : measure.value);
const isFile = isFileType(baseComponent);
const isFileComponent = isFile(baseComponent.qualifier);
const selectedIdx = this.getSelectedIndex(); const selectedIdx = this.getSelectedIndex();


return ( return (
} }
right={ right={
<div className="display-flex-center"> <div className="display-flex-center">
{!isFile && metric && (
{!isFileComponent && metric && (
<> <>
<div>{translate('component_measures.view_as')}</div> <div>{translate('component_measures.view_as')}</div>
<MeasureViewSelect <MeasureViewSelect
metric={metric} metric={metric}
secondaryMeasure={secondaryMeasure} secondaryMeasure={secondaryMeasure}
/> />
{isFile ? (
{isFileComponent ? (
<div className="measure-details-viewer"> <div className="measure-details-viewer">
<SourceViewer <SourceViewer
branchLike={branchLike} branchLike={branchLike}

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

import PageActions from '../../../components/ui/PageActions'; import PageActions from '../../../components/ui/PageActions';
import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like';
import { BranchLike } from '../../../types/branch-like'; import { BranchLike } from '../../../types/branch-like';
import { isFile } from '../../../types/component';
import BubbleChart from '../drilldown/BubbleChart'; 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 Breadcrumbs from './Breadcrumbs';
import LeakPeriodLegend from './LeakPeriodLegend'; import LeakPeriodLegend from './LeakPeriodLegend';
import MeasureContentHeader from './MeasureContentHeader'; import MeasureContentHeader from './MeasureContentHeader';
onIssueChange?: (issue: T.Issue) => void; onIssueChange?: (issue: T.Issue) => void;
rootComponent: T.ComponentMeasure; rootComponent: T.ComponentMeasure;
updateLoading: (param: T.Dict<boolean>) => void; updateLoading: (param: T.Dict<boolean>) => void;
updateSelected: (component: string) => void;
updateSelected: (component: T.ComponentMeasureIntern) => void;
} }


interface State { interface State {


fetchComponents = () => { fetchComponents = () => {
const { branchLike, component, domain, metrics } = this.props; const { branchLike, component, domain, metrics } = this.props;
if (isFileType(component)) {
if (isFile(component.qualifier)) {
this.setState({ components: [], paging: undefined }); this.setState({ components: [], paging: undefined });
return; return;
} }
const { branchLike, component, domain, metrics } = this.props; const { branchLike, component, domain, metrics } = this.props;
const { paging } = this.state; const { paging } = this.state;


if (isFileType(component)) {
if (isFile(component.qualifier)) {
return ( return (
<div className="measure-details-viewer"> <div className="measure-details-viewer">
<SourceViewer <SourceViewer

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

import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like';
import { getProjectUrl } from '../../../helpers/urls'; import { getProjectUrl } from '../../../helpers/urls';
import { BranchLike } from '../../../types/branch-like'; import { BranchLike } from '../../../types/branch-like';
import { isViewType, Query } from '../utils';
import { isView } from '../../../types/component';
import { Query } from '../utils';
import MeasureOverview from './MeasureOverview'; import MeasureOverview from './MeasureOverview';


interface Props { interface 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 { } else {
this.props.updateQuery({ 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

} from '../../../helpers/l10n'; } from '../../../helpers/l10n';
import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
import { isDefined } from '../../../helpers/types'; import { isDefined } from '../../../helpers/types';
import { isProject } from '../../../types/component';
import { import {
BUBBLES_FETCH_LIMIT, BUBBLES_FETCH_LIMIT,
getBubbleMetrics, getBubbleMetrics,
domain: string; domain: string;
metrics: T.Dict<T.Metric>; metrics: T.Dict<T.Metric>;
paging?: T.Paging; paging?: T.Paging;
updateSelected: (component: string) => void;
updateSelected: (component: T.ComponentMeasureIntern) => void;
} }


interface State { interface State {
}; };


getTooltip( getTooltip(
componentName: string,
component: T.ComponentMeasureEnhanced,
values: { x: number; y: number; size: number; colors?: Array<number | undefined> }, values: { x: number; y: number; size: number; colors?: Array<number | undefined> },
metrics: { x: T.Metric; y: T.Metric; size: T.Metric; colors?: T.Metric[] } metrics: { x: T.Metric; y: T.Metric; size: T.Metric; colors?: T.Metric[] }
) { ) {
const inner = [ 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.x.name}: ${formatMeasure(values.x, metrics.x.type)}`,
`${metrics.y.name}: ${formatMeasure(values.y, metrics.y.type)}`, `${metrics.y.name}: ${formatMeasure(values.y, metrics.y.type)}`,
`${metrics.size.name}: ${formatMeasure(values.size, metrics.size.type)}` `${metrics.size.name}: ${formatMeasure(values.size, metrics.size.type)}`
];
].filter(s => !!s);
const { colors: valuesColors } = values; const { colors: valuesColors } = values;
const { colors: metricColors } = metrics; const { colors: metricColors } = metrics;
if (valuesColors && metricColors) { if (valuesColors && metricColors) {
}; };


handleBubbleClick = (component: T.ComponentMeasureEnhanced) => handleBubbleClick = (component: T.ComponentMeasureEnhanced) =>
this.props.updateSelected(component.refKey || component.key);
this.props.updateSelected(component);


getDescription(domain: string) { getDescription(domain: string) {
const description = `component_measures.overview.${domain}.description`; const description = `component_measures.overview.${domain}.description`;
size, size,
color: colorRating !== undefined ? RATING_COLORS[colorRating - 1] : undefined, color: colorRating !== undefined ? RATING_COLORS[colorRating - 1] : undefined,
data: component, data: component,
tooltip: this.getTooltip(component.name, { x, y, size, colors }, metrics)
tooltip: this.getTooltip(component, { x, y, size, colors }, metrics)
}; };
}) })
.filter(isDefined); .filter(isDefined);

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

import BranchIcon from '../../../components/icons/BranchIcon'; import BranchIcon from '../../../components/icons/BranchIcon';
import LinkIcon from '../../../components/icons/LinkIcon'; import LinkIcon from '../../../components/icons/LinkIcon';
import QualifierIcon from '../../../components/icons/QualifierIcon'; import QualifierIcon from '../../../components/icons/QualifierIcon';
import { fillBranchLike } from '../../../helpers/branch-like';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';
import { splitPath } from '../../../helpers/path'; 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 { BranchLike } from '../../../types/branch-like';
import { import {
ComponentQualifier, ComponentQualifier,
} }


let path: LocationDescriptor; 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 ( return (
<QualifierIcon className="little-spacer-right" qualifier={component.qualifier} /> <QualifierIcon className="little-spacer-right" qualifier={component.qualifier} />
{head.length > 0 && <span className="note">{head}/</span>} {head.length > 0 && <span className="note">{head}/</span>}
<span>{tail}</span> <span>{tail}</span>
{isApplication(rootComponent.qualifier) &&
{(isApplication(rootComponent.qualifier) || isPortfolioLike(rootComponent.qualifier)) &&
(component.branch ? ( (component.branch ? (
<> <>
<BranchIcon className="spacer-left little-spacer-right" /> <BranchIcon className="spacer-left little-spacer-right" />

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

* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
import * as React from 'react'; import * as React from 'react';
import { getComponentMeasureUniqueKey } from '../../../helpers/component';
import { getLocalizedMetricName } from '../../../helpers/l10n'; import { getLocalizedMetricName } from '../../../helpers/l10n';
import { BranchLike } from '../../../types/branch-like'; import { BranchLike } from '../../../types/branch-like';
import { MeasurePageView } from '../../../types/measures'; import { MeasurePageView } from '../../../types/measures';
metric: T.Metric; metric: T.Metric;
metrics: T.Dict<T.Metric>; metrics: T.Dict<T.Metric>;
rootComponent: T.ComponentMeasure; rootComponent: T.ComponentMeasure;
selectedComponent?: string;
selectedComponent?: T.ComponentMeasureEnhanced;
view: MeasurePageView; view: MeasurePageView;
} }


{components.map(component => ( {components.map(component => (
<ComponentsListRow <ComponentsListRow
component={component} component={component}
isSelected={component.key === props.selectedComponent}
key={component.key}
isSelected={
getComponentMeasureUniqueKey(component) ===
getComponentMeasureUniqueKey(props.selectedComponent)
}
key={getComponentMeasureUniqueKey(component)}
metric={metric} metric={metric}
otherMetrics={otherMetrics} otherMetrics={otherMetrics}
{...props} {...props}

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

components: T.ComponentMeasureEnhanced[]; components: T.ComponentMeasureEnhanced[];
defaultShowBestMeasures: boolean; defaultShowBestMeasures: boolean;
fetchMore: () => void; fetchMore: () => void;
handleSelect: (component: string) => void;
handleOpen: (component: string) => void;
handleSelect: (component: T.ComponentMeasureEnhanced) => void;
handleOpen: (component: T.ComponentMeasureEnhanced) => void;
loadingMore: boolean; loadingMore: boolean;
metric: T.Metric; metric: T.Metric;
metrics: T.Dict<T.Metric>; metrics: T.Dict<T.Metric>;
paging?: T.Paging; paging?: T.Paging;
rootComponent: T.ComponentMeasure; rootComponent: T.ComponentMeasure;
selectedKey?: string;
selectedComponent?: T.ComponentMeasureEnhanced;
selectedIdx?: number; selectedIdx?: number;
view: MeasurePageView; view: MeasurePageView;
} }


componentDidMount() { componentDidMount() {
this.attachShortcuts(); this.attachShortcuts();
if (this.props.selectedKey !== undefined) {
if (this.props.selectedComponent !== undefined) {
this.scrollToElement(); this.scrollToElement();
} }
} }


componentDidUpdate(prevProps: Props) { componentDidUpdate(prevProps: Props) {
if (this.props.selectedKey !== undefined && prevProps.selectedKey !== this.props.selectedKey) {
if (
this.props.selectedComponent &&
prevProps.selectedComponent !== this.props.selectedComponent
) {
this.scrollToElement(); this.scrollToElement();
} }
if (prevProps.metric.key !== this.props.metric.key || prevProps.view !== this.props.view) { if (prevProps.metric.key !== this.props.metric.key || prevProps.view !== this.props.view) {
}; };


openSelected = () => { openSelected = () => {
if (this.props.selectedKey !== undefined) {
this.props.handleOpen(this.props.selectedKey);
if (this.props.selectedComponent !== undefined) {
this.props.handleOpen(this.props.selectedComponent);
} }
}; };


const { selectedIdx } = this.props; const { selectedIdx } = this.props;
const visibleComponents = this.getVisibleComponents(); const visibleComponents = this.getVisibleComponents();
if (selectedIdx !== undefined && selectedIdx > 0) { if (selectedIdx !== undefined && selectedIdx > 0) {
this.props.handleSelect(visibleComponents[selectedIdx - 1].key);
this.props.handleSelect(visibleComponents[selectedIdx - 1]);
} else { } else {
this.props.handleSelect(visibleComponents[visibleComponents.length - 1].key);
this.props.handleSelect(visibleComponents[visibleComponents.length - 1]);
} }
}; };


const { selectedIdx } = this.props; const { selectedIdx } = this.props;
const visibleComponents = this.getVisibleComponents(); const visibleComponents = this.getVisibleComponents();
if (selectedIdx !== undefined && selectedIdx < visibleComponents.length - 1) { if (selectedIdx !== undefined && selectedIdx < visibleComponents.length - 1) {
this.props.handleSelect(visibleComponents[selectedIdx + 1].key);
this.props.handleSelect(visibleComponents[selectedIdx + 1]);
} else { } else {
this.props.handleSelect(visibleComponents[0].key);
this.props.handleSelect(visibleComponents[0]);
} }
}; };


metric={this.props.metric} metric={this.props.metric}
metrics={this.props.metrics} metrics={this.props.metrics}
rootComponent={this.props.rootComponent} rootComponent={this.props.rootComponent}
selectedComponent={this.props.selectedKey}
selectedComponent={this.props.selectedComponent}
view={this.props.view} view={this.props.view}
/> />
{hidingBestMeasures && this.props.paging && ( {hidingBestMeasures && this.props.paging && (

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

import ColorGradientLegend from '../../../components/charts/ColorGradientLegend'; import ColorGradientLegend from '../../../components/charts/ColorGradientLegend';
import TreeMap, { TreeMapItem } from '../../../components/charts/TreeMap'; import TreeMap, { TreeMapItem } from '../../../components/charts/TreeMap';
import QualifierIcon from '../../../components/icons/QualifierIcon'; import QualifierIcon from '../../../components/icons/QualifierIcon';
import { getComponentMeasureUniqueKey } from '../../../helpers/component';
import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n'; import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n';
import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
import { isDefined } from '../../../helpers/types'; import { isDefined } from '../../../helpers/types';
interface Props { interface Props {
branchLike?: BranchLike; branchLike?: BranchLike;
components: T.ComponentMeasureEnhanced[]; components: T.ComponentMeasureEnhanced[];
handleSelect: (component: string) => void;
handleSelect: (component: T.ComponentMeasureIntern) => void;
metric: T.Metric; metric: T.Metric;
} }


color: colorValue ? (colorScale as Function)(colorValue) : undefined, color: colorValue ? (colorScale as Function)(colorValue) : undefined,
gradient: !colorValue ? NA_GRADIENT : undefined, gradient: !colorValue ? NA_GRADIENT : undefined,
icon: <QualifierIcon fill={colors.baseFontColor} qualifier={component.qualifier} />, 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, size: sizeValue,
measureValue: colorValue, measureValue: colorValue,
metric, metric,
tooltip: this.getTooltip({ tooltip: this.getTooltip({
colorMetric: metric, colorMetric: metric,
colorValue, colorValue,
componentName: component.name,
component,
sizeMetric: sizeMeasure.metric, sizeMetric: sizeMeasure.metric,
sizeValue sizeValue
})
}),
component
}; };
}) })
.filter(isDefined); .filter(isDefined);
getTooltip = ({ getTooltip = ({
colorMetric, colorMetric,
colorValue, colorValue,
componentName,
component,
sizeMetric, sizeMetric,
sizeValue sizeValue
}: { }: {
colorMetric: T.Metric; colorMetric: T.Metric;
colorValue?: string; colorValue?: string;
componentName: string;
component: T.ComponentMeasureEnhanced;
sizeMetric: T.Metric; sizeMetric: T.Metric;
sizeValue: number; sizeValue: number;
}) => { }) => {
colorMetric && colorValue !== undefined ? formatMeasure(colorValue, colorMetric.type) : '—'; colorMetric && colorValue !== undefined ? formatMeasure(colorValue, colorMetric.type) : '—';
return ( return (
<div className="text-left"> <div className="text-left">
{componentName}
{[component.name, component.branch].filter(s => !!s).join(' / ')}
<br /> <br />
{`${getLocalizedMetricName(sizeMetric)}: ${formatMeasure(sizeValue, sizeMetric.type)}`} {`${getLocalizedMetricName(sizeMetric)}: ${formatMeasure(sizeValue, sizeMetric.type)}`}
<br /> <br />

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

*/ */
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import * as React from 'react'; import * as React from 'react';
import { mockBranch, mockPullRequest } from '../../../../helpers/mocks/branch-like';
import { Link } from 'react-router';
import { import {
mockComponentMeasure, mockComponentMeasure,
mockComponentMeasureEnhanced mockComponentMeasureEnhanced


it('should render correctly', () => { it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default'); 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( expect(
shallowRender({ 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( 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({ 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) { function shallowRender(overrides: Partial<ComponentCellProps> = {}, metricKey = MetricKey.bugs) {
const metric = mockMetric({ key: metricKey }); 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

// Jest Snapshot v1, https://goo.gl/fbAQLP // 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 <td
className="measure-details-component-cell" className="measure-details-component-cell"
> >
> >
<Link <Link
className="link-no-underline" className="link-no-underline"
id="component-measures-component-link-foo:src/index.tsx"
id="component-measures-component-link-foo"
onlyActiveOnIndex={false} onlyActiveOnIndex={false}
style={Object {}} style={Object {}}
to={ to={
Object { Object {
"pathname": "/component_measures", "pathname": "/component_measures",
"query": Object { "query": Object {
"branch": "develop",
"id": "foo", "id": "foo",
"metric": "bugs", "metric": "bugs",
"selected": "foo:src/index.tsx",
"selected": "foo",
"view": "list", "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 <span
title="foo" title="foo"
> >
<QualifierIcon <QualifierIcon
className="little-spacer-right" className="little-spacer-right"
qualifier="APP"
qualifier="TRK"
/> />
<span> <span>
Foo Foo
</span> </span>
<BranchIcon
className="spacer-left little-spacer-right"
/>
<span
className="note"
>
develop
</span>
</span> </span>
</Link> </Link>
</div> </div>
</td> </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 <td
className="measure-details-component-cell" className="measure-details-component-cell"
> >
Object { Object {
"pathname": "/component_measures", "pathname": "/component_measures",
"query": Object { "query": Object {
"branch": "branch-6.7",
"id": "app-key",
"metric": "projects",
"id": "foo",
"metric": "bugs",
"selected": "foo",
"view": "list", "view": "list",
}, },
} }
} }
> >
<span
className="big-spacer-right"
>
<LinkIcon />
</span>
<span <span
title="foo" title="foo"
> >
<QualifierIcon <QualifierIcon
className="little-spacer-right" className="little-spacer-right"
qualifier="APP"
qualifier="TRK"
/> />
<span> <span>
Foo Foo
</span> </span>
<span
className="spacer-left badge"
>
branches.main_branch
</span>
</span> </span>
</Link> </Link>
</div> </div>
</td> </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 <td
className="measure-details-component-cell" className="measure-details-component-cell"
> >
Object { Object {
"pathname": "/component_measures", "pathname": "/component_measures",
"query": Object { "query": Object {
"id": "vw-key",
"metric": "alert_status",
"pullRequest": "1001",
"branch": "develop",
"id": "foo",
"metric": "bugs",
"selected": "foo",
"view": "list", "view": "list",
}, },
} }
} }
> >
<span
className="big-spacer-right"
>
<LinkIcon />
</span>
<span <span
title="foo" title="foo"
> >
<QualifierIcon <QualifierIcon
className="little-spacer-right" className="little-spacer-right"
qualifier="VW"
qualifier="TRK"
/> />
<span> <span>
Foo Foo
</td> </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 <td
className="measure-details-component-cell" className="measure-details-component-cell"
> >
Object { Object {
"pathname": "/component_measures", "pathname": "/component_measures",
"query": Object { "query": Object {
"id": "project-key",
"id": "foo",
"metric": "bugs", "metric": "bugs",
"selected": "foo",
"view": "list", "view": "list",
}, },
} }
} }
> >
<span
className="big-spacer-right"
>
<LinkIcon />
</span>
<span <span
title="foo" title="foo"
> >
</td> </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 <td
className="measure-details-component-cell" className="measure-details-component-cell"
> >
style={Object {}} style={Object {}}
to={ to={
Object { Object {
"pathname": "/dashboard",
"pathname": "/component_measures",
"query": Object { "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 <span
title="foo" title="foo"
> >
<span> <span>
Foo Foo
</span> </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>
</span> </span>
</Link> </Link>
</td> </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 <td
className="measure-details-component-cell" className="measure-details-component-cell"
> >
<span> <span>
Foo Foo
</span> </span>
<BranchIcon
className="spacer-left little-spacer-right"
/>
<span <span
className="note"
className="spacer-left badge"
> >
develop
branches.main_branch
</span> </span>
</span> </span>
</Link> </Link>
</td> </td>
`; `;


exports[`should render correctly: root component is application, component is on main branch 1`] = `
exports[`should render correctly: default 1`] = `
<td <td
className="measure-details-component-cell" className="measure-details-component-cell"
> >
<span> <span>
index.tsx index.tsx
</span> </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> </span>
</Link> </Link>
</div> </div>

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

return { ...component, value, leak, measures: enhancedMeasures }; 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 { export function isSecurityReviewMetric(metricKey: MetricKey | string): boolean {
return [ return [
MetricKey.security_hotspots, MetricKey.security_hotspots,

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

* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
import * as React from 'react'; import * as React from 'react';
import BranchIcon from '../../../components/icons/BranchIcon';
import QualifierIcon from '../../../components/icons/QualifierIcon'; import QualifierIcon from '../../../components/icons/QualifierIcon';
import { translateWithParameters } from '../../../helpers/l10n'; import { translateWithParameters } from '../../../helpers/l10n';
import { collapsePath, limitComponentName } from '../../../helpers/path'; import { collapsePath, limitComponentName } from '../../../helpers/path';
import { ComponentQualifier } from '../../../types/component';
import { getSelectedLocation } from '../utils'; import { getSelectedLocation } from '../utils';


interface Props { interface Props {
selectedFlowIndex, selectedFlowIndex,
selectedLocationIndex selectedLocationIndex
}: Props) { }: 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 selectedLocation = getSelectedLocation(issue, selectedFlowIndex, selectedLocationIndex);
const componentName = selectedLocation ? selectedLocation.componentName : issue.componentLongName; const componentName = selectedLocation ? selectedLocation.componentName : issue.componentLongName;
const projectName = [issue.projectName, issue.branch].filter(s => !!s).join(' - ');


return ( return (
<div <div
<QualifierIcon className="spacer-right" qualifier={issue.componentQualifier} /> <QualifierIcon className="spacer-right" qualifier={issue.componentQualifier} />


{displayProject && ( {displayProject && (
<span title={issue.projectName}>
<span title={projectName}>
{limitComponentName(issue.projectName)} {limitComponentName(issue.projectName)}
{issue.branch && (
<>
{' - '}
<BranchIcon />
<span>{issue.branch}</span>
</>
)}
<span className="slash-separator" /> <span className="slash-separator" />
</span> </span>
)} )}

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

render() { render() {
const { branchLike, component, issue, previousIssue } = this.props; 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 ( return (
<li className="issues-workspace-list-item"> <li className="issues-workspace-list-item">

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

componentLongName: 'comp-name', componentLongName: 'comp-name',
componentQualifier: ComponentQualifier.File, componentQualifier: ComponentQualifier.File,
project: 'proj', project: 'proj',
projectName: 'proj-name'
projectName: 'proj-name',
branch: 'test-branch'
}); });


it('renders', () => { it('renders', () => {

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

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

qualifier="FIL" qualifier="FIL"
/> />
<span <span
title="proj-name"
title="proj-name - test-branch"
> >
proj-name proj-name
-
<BranchIcon />
<span>
test-branch
</span>
<span <span
className="slash-separator" className="slash-separator"
/> />
qualifier="FIL" qualifier="FIL"
/> />
<span <span
title="proj-name"
title="proj-name - test-branch"
> >
proj-name proj-name
-
<BranchIcon />
<span>
test-branch
</span>
<span <span
className="slash-separator" className="slash-separator"
/> />

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

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

defaultMessage={translate('portfolio.x_in_y')} defaultMessage={translate('portfolio.x_in_y')}
id="portfolio.x_in_y" id="portfolio.x_in_y"
values={{ values={{
projects: (
project_branches: (
<Link <Link
to={getComponentDrilldownUrl({ to={getComponentDrilldownUrl({
componentKey: component, componentKey: component,
value={String(effort.projects)} value={String(effort.projects)}
/> />
{effort.projects === 1 {effort.projects === 1
? translate('project_singular')
: translate('project_plural')}
? translate('portfolio.project_branch')
: translate('portfolio.project_branches')}
</span> </span>
</Link> </Link>
), ),

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

{metricKey === 'releasability' {metricKey === 'releasability'
? Number(effort) > 0 && ( ? Number(effort) > 0 && (
<> <>
<h3>{translate('portfolio.lowest_rated_projects')}</h3>
<h3>{translate('portfolio.lowest_rated_project_branches')}</h3>
<div className="portfolio-effort"> <div className="portfolio-effort">
<Link <Link
to={getComponentDrilldownUrl({ to={getComponentDrilldownUrl({
value={effort} value={effort}
/> />
{Number(effort) === 1 {Number(effort) === 1
? translate('project_singular')
: translate('project_plural')}
? translate('portfolio.project_branch')
: translate('portfolio.project_branches')}
</span> </span>
</Link> </Link>
<Level <Level
) )
: effort && ( : effort && (
<> <>
<h3>{translate('portfolio.lowest_rated_projects')}</h3>
<h3>{translate('portfolio.lowest_rated_project_branches')}</h3>
<Effort component={component} effort={effort} metricKey={keys.rating} /> <Effort component={component} effort={effort} metricKey={keys.rating} />
</> </>
)} )}

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

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * 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 * as React from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { colors } from '../../../app/theme'; import { colors } from '../../../app/theme';
import BranchIcon from '../../../components/icons/BranchIcon';
import QualifierIcon from '../../../components/icons/QualifierIcon'; import QualifierIcon from '../../../components/icons/QualifierIcon';
import Measure from '../../../components/measure/Measure'; import Measure from '../../../components/measure/Measure';
import { translate, translateWithParameters } from '../../../helpers/l10n'; import { translate, translateWithParameters } from '../../../helpers/l10n';


const projectsPageUrl = { pathname: '/code', query: { id: component } }; const projectsPageUrl = { pathname: '/code', query: { id: component } };


const subCompList = sortBy(
subComponents,
c => c.qualifier,
c => c.name.toLowerCase(),
c => c.branch?.toLowerCase()
);

return ( return (
<div className="panel panel-white portfolio-sub-components" id="portfolio-sub-components"> <div className="panel panel-white portfolio-sub-components" id="portfolio-sub-components">
<table className="data zebra"> <table className="data zebra">
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{subComponents.map(component => (
<tr key={component.key}>
{subCompList.map(comp => (
<tr key={[comp.key, comp.branch].filter(s => !!s).join('/')}>
<td> <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> </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> </tr>
))} ))}
</tbody> </tbody>

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

*/ */
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import * as React from 'react'; import * as React from 'react';
import { ComponentQualifier } from '../../../../types/component';
import WorstProjects from '../WorstProjects'; import WorstProjects from '../WorstProjects';


it('renders', () => { it('renders', () => {
ncloc: '200' ncloc: '200'
}, },
name: 'Foo', 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', key: 'bar',
ncloc: '100' ncloc: '100'
}, },
name: 'Bar', name: 'Bar',
qualifier: 'TRK',
qualifier: ComponentQualifier.Project,
refKey: 'barbar' refKey: 'barbar'
}, },
{ {
ncloc: '150' ncloc: '150'
}, },
name: 'Baz', name: 'Baz',
qualifier: 'TRK',
qualifier: ComponentQualifier.Project,
refKey: 'bazbaz' refKey: 'bazbaz'
} }
]; ];

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

id="portfolio.x_in_y" id="portfolio.x_in_y"
values={ values={
Object { Object {
"projects": <Link
"project_branches": <Link
onlyActiveOnIndex={false} onlyActiveOnIndex={false}
style={Object {}} style={Object {}}
to={ to={
metricType="SHORT_INT" metricType="SHORT_INT"
value="3" value="3"
/> />
project_plural
portfolio.project_branches
</span> </span>
</Link>, </Link>,
"rating": <Rating "rating": <Rating

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

rating="3" rating="3"
/> />
<h3> <h3>
portfolio.lowest_rated_projects
portfolio.lowest_rated_project_branches
</h3> </h3>
<Effort <Effort
component="foo" component="foo"
rating="2" rating="2"
/> />
<h3> <h3>
portfolio.lowest_rated_projects
portfolio.lowest_rated_project_branches
</h3> </h3>
<div <div
className="portfolio-effort" className="portfolio-effort"
metricType="SHORT_INT" metricType="SHORT_INT"
value={5} value={5}
/> />
project_plural
portfolio.project_branches
</span> </span>
</Link> </Link>
<Level <Level
rating="2" rating="2"
/> />
<h3> <h3>
portfolio.lowest_rated_projects
portfolio.lowest_rated_project_branches
</h3> </h3>
<div <div
className="portfolio-effort" className="portfolio-effort"
metricType="SHORT_INT" metricType="SHORT_INT"
value={1} value={1}
/> />
project_singular
portfolio.project_branch
</span> </span>
</Link> </Link>
<Level <Level

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

</thead> </thead>
<tbody> <tbody>
<tr <tr
key="foo"
key="foo_app"
> >
<td> <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 <QualifierIcon
className="spacer-right"
qualifier="SVW" qualifier="SVW"
/> />
Foo
</Link>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/portfolio",
"query": Object {
"id": "foo",
},
}
}
>
Foo
</Link>
</span>
</td> </td>
<td <td
className="text-center" className="text-center"
</td> </td>
</tr> </tr>
<tr <tr
key="bar"
key="bar/branch-1"
> >
<td> <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 <QualifierIcon
className="spacer-right"
qualifier="TRK" 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>
<td <td
className="text-center" className="text-center"
key="baz" key="baz"
> >
<td> <td>
<Link
className="link-with-icon"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/dashboard",
"query": Object {
"id": "bazbaz",
},
}
}
<span
className="display-flex-center"
> >
<QualifierIcon <QualifierIcon
className="spacer-right"
qualifier="TRK" 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>
<td <td
className="text-center" className="text-center"

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

name: string; name: string;
refKey?: string; refKey?: string;
qualifier: string; qualifier: string;
branch?: string;
} }

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

metric?: { key: string; type: string }; metric?: { key: string; type: string };
size: number; size: number;
tooltip?: React.ReactNode; tooltip?: React.ReactNode;
component: T.ComponentMeasureEnhanced;
} }


interface HierarchicalTreemapItem extends TreeMapItem { interface HierarchicalTreemapItem extends TreeMapItem {
interface Props { interface Props {
height: number; height: number;
items: TreeMapItem[]; items: TreeMapItem[];
onRectangleClick?: (item: string) => void;
onRectangleClick?: (item: T.ComponentMeasureEnhanced) => void;
width: number; width: number;
} }


return prefix.substr(0, prefix.length - lastPrefixPart.length); return prefix.substr(0, prefix.length - lastPrefixPart.length);
}; };


handleClick = (component: T.ComponentMeasureEnhanced) => {
if (this.props.onRectangleClick) {
this.props.onRectangleClick(component);
}
};

render() { render() {
const { items, height, width } = this.props; const { items, height, width } = this.props;
const hierarchy = d3Hierarchy({ children: items } as HierarchicalTreemapItem) const hierarchy = d3Hierarchy({ children: items } as HierarchicalTreemapItem)
key={node.data.key} key={node.data.key}
label={node.data.label} label={node.data.label}
link={node.data.link} link={node.data.link}
onClick={this.props.onRectangleClick}
onClick={() => this.handleClick(node.data.component)}
placement={node.x0 === 0 || node.x1 < halfWidth ? 'right' : 'left'} placement={node.x0 === 0 || node.x1 < halfWidth ? 'right' : 'left'}
prefix={prefix} prefix={prefix}
value={ value={

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

*/ */
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import * as React from 'react'; import * as React from 'react';
import { mockComponentMeasureEnhanced } from '../../../helpers/mocks/component';
import TreeMap from '../TreeMap'; import TreeMap from '../TreeMap';
import TreeMapRect from '../TreeMapRect'; import TreeMapRect from '../TreeMapRect';


it('should render correctly', () => { it('should render correctly', () => {
const items = [ 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', key: '3',
size: 20, size: 20,
gradient: '#777', gradient: '#777',
label: 'SonarQube :: Search', label: 'SonarQube :: Search',
metric: { key: 'coverage', type: 'PERCENT' }
metric: { key: 'coverage', type: 'PERCENT' },
component: mockComponentMeasureEnhanced()
} }
]; ];
const onRectClick = jest.fn(); const onRectClick = jest.fn();
expect(event.stopPropagation).toHaveBeenCalled(); expect(event.stopPropagation).toHaveBeenCalled();


(rects.first().instance() as TreeMapRect).handleRectClick(); (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



render() { render() {
return ( return (
<li className={classNames({ 'select-list-list-disabled': this.props.disabled })}>
<li
className={classNames({
'select-list-list-disabled': this.props.disabled
})}>
<Checkbox <Checkbox
checked={this.props.selected} 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} disabled={this.props.disabled}
loading={this.state.loading} loading={this.state.loading}
onCheck={this.handleCheck}> 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> </Checkbox>
</li> </li>
); );

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

> >
<Checkbox <Checkbox
checked={false} checked={false}
className="select-list-list-checkbox"
className="select-list-list-checkbox display-flex-center"
loading={false} loading={false}
onCheck={[Function]} onCheck={[Function]}
thirdState={false} thirdState={false}
> >
<span <span
className="little-spacer-left"
className="little-spacer-left flex-1"
> >
foo foo
</span> </span>
> >
<Checkbox <Checkbox
checked={false} checked={false}
className="select-list-list-checkbox"
className="select-list-list-checkbox display-flex-center"
loading={true} loading={true}
onCheck={[Function]} onCheck={[Function]}
thirdState={false} thirdState={false}
> >
<span <span
className="little-spacer-left"
className="little-spacer-left flex-1"
> >
foo foo
</span> </span>

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

import key from 'keymaster'; import key from 'keymaster';
import * as React from 'react'; import * as React from 'react';
import PageActions from '../../components/ui/PageActions'; import PageActions from '../../components/ui/PageActions';
import { getComponentMeasureUniqueKey } from '../../helpers/component';
import { getWrappedDisplayName } from './utils'; import { getWrappedDisplayName } from './utils';


export interface WithKeyboardNavigationProps { export interface WithKeyboardNavigationProps {


getCurrentIndex = () => { getCurrentIndex = () => {
const { selected, components = [] } = this.props; 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) => { skipIfFile = (handler: () => void) => {

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

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

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

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

): componentQualifier is ComponentQualifier.Project { ): componentQualifier is ComponentQualifier.Project {
return componentQualifier === 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

name: string; name: string;
} }


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

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

projects=Projects projects=Projects
projects_=project(s) projects_=project(s)
x_projects_={0} project(s) x_projects_={0} project(s)
project_singular=project
project_plural=projects project_plural=projects
projects_management=Projects Management projects_management=Projects Management
quality_profile=Quality Profile quality_profile=Quality Profile
metric.profile.name=Profile metric.profile.name=Profile
metric.profile_version.description=Selected Quality Profile version metric.profile_version.description=Selected Quality Profile version
metric.profile_version.name=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.description=Public API
metric.public_api.name=Public API metric.public_api.name=Public API
metric.public_documented_api_density.description=Public documented classes and functions balanced by ncloc metric.public_documented_api_density.description=Public documented classes and functions balanced by ncloc
#------------------------------------------------------------------------------ #------------------------------------------------------------------------------
portfolio.has_always_been_x=has always been {rating} portfolio.has_always_been_x=has always been {rating}
portfolio.was_x_y=was {rating} {date} 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.has_qg_status=Has Quality Gate Status
portfolio.have_qg_status=Have Quality Gate Status portfolio.have_qg_status=Have Quality Gate Status
portfolio.empty=This portfolio is empty. portfolio.empty=This portfolio is empty.
portfolio.app.empty=This application is empty. portfolio.app.empty=This application is empty.
portfolio.app.no_lines_of_code=All projects in this application are empty portfolio.app.no_lines_of_code=All projects in this application are empty
portfolio.metric_trend=Metric trend 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.health_factors=Portfolio health factors
portfolio.activity_link=Activity portfolio.activity_link=Activity
portfolio.measures_link=Measures portfolio.measures_link=Measures
portfolio.language_breakdown_link=Language breakdown portfolio.language_breakdown_link=Language breakdown
portfolio.breakdown=Portfolio 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.number_of_lines=Number of lines of code


portfolio.metric_domain.vulnerabilities=Security Vulnerabilities portfolio.metric_domain.vulnerabilities=Security Vulnerabilities

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

/** /**
* @since 3.0 * @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) .setDirection(Metric.DIRECTION_WORST)
.setQualitative(false) .setQualitative(false)
.setDomain(DOMAIN_SIZE) .setDomain(DOMAIN_SIZE)

Loading…
Cancel
Save