Browse Source

SONAR-9812 display activity page for portfolios (#2510)

tags/6.6-RC1
Stas Vilchik 6 years ago
parent
commit
1570581366
100 changed files with 4161 additions and 273 deletions
  1. 6
    1
      server/sonar-web/src/main/js/api/languages.ts
  2. 3
    2
      server/sonar-web/src/main/js/api/measures.ts
  3. 2
    1
      server/sonar-web/src/main/js/api/metrics.ts
  4. 55
    0
      server/sonar-web/src/main/js/api/report.ts
  5. 7
    7
      server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
  6. 7
    8
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
  7. 21
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx
  8. 646
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
  9. 1
    0
      server/sonar-web/src/main/js/app/types.ts
  10. 2
    1
      server/sonar-web/src/main/js/app/utils/exposeLibraries.js
  11. 2
    3
      server/sonar-web/src/main/js/app/utils/startReactApp.js
  12. 1
    4
      server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx
  13. 7
    4
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js
  14. 1
    1
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js
  15. 1
    1
      server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js
  16. 1
    2
      server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js
  17. 2
    2
      server/sonar-web/src/main/js/apps/overview/main/enhance.js
  18. 2
    2
      server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js
  19. 121
    0
      server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx
  20. 150
    0
      server/sonar-web/src/main/js/apps/portfolio/components/App.tsx
  21. 59
    0
      server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx
  22. 38
    0
      server/sonar-web/src/main/js/apps/portfolio/components/HistoryButtonLink.tsx
  23. 37
    0
      server/sonar-web/src/main/js/apps/portfolio/components/MainRating.tsx
  24. 54
    0
      server/sonar-web/src/main/js/apps/portfolio/components/MaintainabilityBox.tsx
  25. 38
    0
      server/sonar-web/src/main/js/apps/portfolio/components/MeasuresButtonLink.tsx
  26. 49
    0
      server/sonar-web/src/main/js/apps/portfolio/components/RatingFreshness.tsx
  27. 68
    0
      server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx
  28. 54
    0
      server/sonar-web/src/main/js/apps/portfolio/components/ReliabilityBox.tsx
  29. 112
    0
      server/sonar-web/src/main/js/apps/portfolio/components/Report.tsx
  30. 54
    0
      server/sonar-web/src/main/js/apps/portfolio/components/SecurityBox.tsx
  31. 133
    0
      server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx
  32. 28
    0
      server/sonar-web/src/main/js/apps/portfolio/components/SubscriptionContainer.tsx
  33. 69
    0
      server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx
  34. 140
    0
      server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx
  35. 77
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx
  36. 89
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx
  37. 30
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Effort-test.tsx
  38. 26
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/HistoryButtonLink-test.tsx
  39. 28
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MainRating-test.tsx
  40. 31
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MaintainabilityBox-test.tsx
  41. 28
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MeasuresButtonLink-test.tsx
  42. 31
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/RatingFreshness-test.tsx
  43. 31
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReleasabilityBox-test.tsx
  44. 31
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReliabilityBox-test.tsx
  45. 54
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Report-test.tsx
  46. 31
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/SecurityBox-test.tsx
  47. 84
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Subscription-test.tsx
  48. 33
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Summary-test.tsx
  49. 68
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/WorstProjects-test.tsx
  50. 42
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap
  51. 65
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap
  52. 50
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap
  53. 24
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/HistoryButtonLink-test.tsx.snap
  54. 24
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MainRating-test.tsx.snap
  55. 39
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MaintainabilityBox-test.tsx.snap
  56. 23
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MeasuresButtonLink-test.tsx.snap
  57. 31
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/RatingFreshness-test.tsx.snap
  58. 75
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap
  59. 39
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReliabilityBox-test.tsx.snap
  60. 65
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Report-test.tsx.snap
  61. 39
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/SecurityBox-test.tsx.snap
  62. 63
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Subscription-test.tsx.snap
  63. 96
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap
  64. 395
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap
  65. 30
    0
      server/sonar-web/src/main/js/apps/portfolio/routes.ts
  66. 95
    0
      server/sonar-web/src/main/js/apps/portfolio/styles.css
  67. 26
    0
      server/sonar-web/src/main/js/apps/portfolio/types.ts
  68. 92
    0
      server/sonar-web/src/main/js/apps/portfolio/utils.ts
  69. 2
    2
      server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js
  70. 1
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/GraphHistory.js
  71. 1
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js
  72. 2
    2
      server/sonar-web/src/main/js/apps/projectActivity/components/GraphsZoom.js
  73. 3
    2
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js
  74. 7
    3
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
  75. 27
    12
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
  76. 1
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js
  77. 13
    11
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js
  78. 6
    14
      server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeakMeasures.tsx
  79. 3
    7
      server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx
  80. 0
    12
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeakMeasures-test.tsx.snap
  81. 0
    6
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverallMeasures-test.tsx.snap
  82. 15
    16
      server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js
  83. 0
    89
      server/sonar-web/src/main/js/components/charts/LanguageDistribution.js
  84. 64
    0
      server/sonar-web/src/main/js/components/charts/LanguageDistribution.tsx
  85. 7
    15
      server/sonar-web/src/main/js/components/charts/LanguageDistributionContainer.tsx
  86. 2
    1
      server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js
  87. 6
    6
      server/sonar-web/src/main/js/components/icons-components/BubblesIcon.tsx
  88. 6
    7
      server/sonar-web/src/main/js/components/icons-components/HistoryIcon.tsx
  89. 0
    1
      server/sonar-web/src/main/js/components/icons-components/LinkIcon.tsx
  90. 6
    2
      server/sonar-web/src/main/js/components/measure/Measure.tsx
  91. 10
    5
      server/sonar-web/src/main/js/components/measure/utils.ts
  92. 15
    10
      server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js
  93. 3
    3
      server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.js
  94. 1
    1
      server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltipsContent.js
  95. 2
    2
      server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltips-test.js
  96. 0
    0
      server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltipsContent-test.js
  97. 0
    0
      server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap
  98. 0
    0
      server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.js.snap
  99. 2
    2
      server/sonar-web/src/main/js/helpers/testUtils.ts
  100. 0
    0
      server/sonar-web/src/main/js/helpers/urls.ts

+ 6
- 1
server/sonar-web/src/main/js/api/languages.ts View File

@@ -19,6 +19,11 @@
*/
import { getJSON } from '../helpers/request';

export function getLanguages(): Promise<any> {
export interface Language {
key: string;
name: string;
}

export function getLanguages(): Promise<Language[]> {
return getJSON('/api/languages/list').then(r => r.languages);
}

+ 3
- 2
server/sonar-web/src/main/js/api/measures.ts View File

@@ -18,15 +18,16 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { getJSON, RequestData } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';

export function getMeasures(
componentKey: string,
metrics: string[],
branch?: string
): Promise<any> {
): Promise<Array<{ metric: string; value?: string }>> {
const url = '/api/measures/component';
const data = { componentKey, metricKeys: metrics.join(','), branch };
return getJSON(url, data).then(r => r.component.measures);
return getJSON(url, data).then(r => r.component.measures, throwGlobalError);
}

export function getMeasuresAndMeta(

+ 2
- 1
server/sonar-web/src/main/js/api/metrics.ts View File

@@ -18,7 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { getJSON } from '../helpers/request';
import { Metric } from '../app/types';

export function getMetrics(): Promise<any> {
export function getMetrics(): Promise<Metric[]> {
return getJSON('/api/metrics/search', { ps: 9999 }).then(r => r.metrics);
}

+ 55
- 0
server/sonar-web/src/main/js/api/report.ts View File

@@ -0,0 +1,55 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 { getJSON, post } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';

export interface ReportStatus {
canDownload?: boolean;
canSubscribe: boolean;
componentFrequency?: string;
globalFrequency: string;
subscribed?: boolean;
}

export function getReportStatus(component: string): Promise<ReportStatus> {
return getJSON('/api/governance_reports/status', { componentKey: component }).catch(
throwGlobalError
);
}

export function getReportUrl(component: string): string {
return (
(window as any).baseUrl +
'/api/governance_reports/download?componentKey=' +
encodeURIComponent(component)
);
}

export function subscribe(component: string): Promise<void | Response> {
return post('/api/governance_reports/subscribe', { componentKey: component }).catch(
throwGlobalError
);
}

export function unsubscribe(component: string): Promise<void | Response> {
return post('/api/governance_reports/unsubscribe', { componentKey: component }).catch(
throwGlobalError
);
}

+ 7
- 7
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx View File

@@ -49,15 +49,15 @@ export default class ComponentContainer extends React.PureComponent<Props, State

componentDidMount() {
this.mounted = true;
this.fetchComponent();
this.fetchComponent(this.props);
}

componentDidUpdate(prevProps: Props) {
componentWillReceiveProps(nextProps: Props) {
if (
prevProps.location.query.id !== this.props.location.query.id ||
prevProps.location.query.branch !== this.props.location.query.branch
nextProps.location.query.id !== this.props.location.query.id ||
nextProps.location.query.branch !== this.props.location.query.branch
) {
this.fetchComponent();
this.fetchComponent(nextProps);
}
}

@@ -70,8 +70,8 @@ export default class ComponentContainer extends React.PureComponent<Props, State
qualifier: component.breadcrumbs[component.breadcrumbs.length - 1].qualifier
});

fetchComponent() {
const { branch, id } = this.props.location.query;
fetchComponent(props: Props) {
const { branch, id } = props.location.query;
this.setState({ loading: true });

const onError = (error: any) => {

+ 7
- 8
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx View File

@@ -65,7 +65,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
return this.props.component.qualifier === 'DEV';
}

isView() {
isPortfolio() {
const { qualifier } = this.props.component;
return qualifier === 'VW' || qualifier === 'SVW';
}
@@ -79,7 +79,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
return null;
}

const pathname = this.isView() ? '/portfolio' : '/dashboard';
const pathname = this.isPortfolio() ? '/portfolio' : '/dashboard';
return (
<li>
<Link
@@ -113,7 +113,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
}
}}
activeClassName="active">
{this.isView() || this.isApplication() ? (
{this.isPortfolio() || this.isApplication() ? (
translate('view_projects.page')
) : (
translate('code.page')
@@ -124,7 +124,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
}

renderActivityLink() {
if (!this.isProject() && !this.isApplication()) {
if (!this.isProject() && !this.isApplication() && !this.isPortfolio()) {
return null;
}

@@ -252,7 +252,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
}

renderSettingsLink() {
if (!this.props.conf.showSettings || this.isApplication() || this.isView()) {
if (!this.props.conf.showSettings || this.isApplication() || this.isPortfolio()) {
return null;
}
return (
@@ -432,8 +432,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {

renderExtensions() {
const extensions = this.props.component.extensions || [];
const withoutGovernance = extensions.filter(ext => ext.name !== 'Governance');
if (!withoutGovernance.length) {
if (!extensions.length) {
return null;
}

@@ -448,7 +447,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
<i className="icon-dropdown" />
</a>
<ul className="dropdown-menu">
{withoutGovernance.map(e => this.renderExtension(e, false))}
{extensions.map(e => this.renderExtension(e, false))}
</ul>
</li>
);

+ 21
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx View File

@@ -99,3 +99,24 @@ it('should work for long-living branches', () => {
).toMatchSnapshot()
);
});

it('should work for all qualifiers', () => {
['TRK', 'BRC', 'VW', 'SVW', 'APP'].forEach(checkWithQualifier);
expect.assertions(5);

function checkWithQualifier(qualifier: string) {
const component = { key: 'foo', qualifier } as Component;
expect(
shallow(
<ComponentNavMenu
branch={mainBranch}
component={component}
conf={{ showSettings: true }}
/>,
{
context: { branchesEnabled: true }
}
)
).toMatchSnapshot();
}
});

+ 646
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap View File

@@ -1,5 +1,651 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should work for all qualifiers 1`] = `
<NavBarTabs>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
overview.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"branch": undefined,
"id": "foo",
"resolved": "false",
},
}
}
>
issues.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/component_measures",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
layout.measures
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/code",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
code.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/activity",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
project_activity.page
</Link>
</li>
<li
className="dropdown"
>
<a
className="dropdown-toggle is-admin"
data-toggle="dropdown"
href="#"
id="component-navigation-admin"
>
layout.settings
 
<i
className="icon-dropdown"
/>
</a>
<ul
className="dropdown-menu"
>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/settings",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
project_settings.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/branches",
"query": Object {
"id": "foo",
},
}
}
>
project_branches.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/deletion",
"query": Object {
"id": "foo",
},
}
}
>
deletion.page
</Link>
</li>
</ul>
</li>
</NavBarTabs>
`;

exports[`should work for all qualifiers 2`] = `
<NavBarTabs>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
overview.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"branch": undefined,
"id": "foo",
"resolved": "false",
},
}
}
>
issues.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/component_measures",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
layout.measures
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/code",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
code.page
</Link>
</li>
<li
className="dropdown"
>
<a
className="dropdown-toggle is-admin"
data-toggle="dropdown"
href="#"
id="component-navigation-admin"
>
layout.settings
 
<i
className="icon-dropdown"
/>
</a>
<ul
className="dropdown-menu"
>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/settings",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
project_settings.page
</Link>
</li>
</ul>
</li>
</NavBarTabs>
`;

exports[`should work for all qualifiers 3`] = `
<NavBarTabs>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/portfolio",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
overview.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"branch": undefined,
"id": "foo",
"resolved": "false",
},
}
}
>
issues.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/component_measures",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
layout.measures
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/code",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
view_projects.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/activity",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
project_activity.page
</Link>
</li>
<li
className="dropdown"
>
<a
className="dropdown-toggle is-admin"
data-toggle="dropdown"
href="#"
id="component-navigation-admin"
>
layout.settings
 
<i
className="icon-dropdown"
/>
</a>
<ul
className="dropdown-menu"
>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/deletion",
"query": Object {
"id": "foo",
},
}
}
>
deletion.page
</Link>
</li>
</ul>
</li>
</NavBarTabs>
`;

exports[`should work for all qualifiers 4`] = `
<NavBarTabs>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/portfolio",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
overview.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"branch": undefined,
"id": "foo",
"resolved": "false",
},
}
}
>
issues.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/component_measures",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
layout.measures
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/code",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
view_projects.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/activity",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
project_activity.page
</Link>
</li>
</NavBarTabs>
`;

exports[`should work for all qualifiers 5`] = `
<NavBarTabs>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
overview.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"branch": undefined,
"id": "foo",
"resolved": "false",
},
}
}
>
issues.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/component_measures",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
layout.measures
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/code",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
view_projects.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/activity",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
project_activity.page
</Link>
</li>
<li
className="dropdown"
>
<a
className="dropdown-toggle is-admin"
data-toggle="dropdown"
href="#"
id="component-navigation-admin"
>
layout.settings
 
<i
className="icon-dropdown"
/>
</a>
<ul
className="dropdown-menu"
>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/deletion",
"query": Object {
"id": "foo",
},
}
}
>
deletion.page
</Link>
</li>
</ul>
</li>
</NavBarTabs>
`;

exports[`should work for long-living branches 1`] = `
<NavBarTabs>
<li>

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

@@ -67,6 +67,7 @@ export interface Component {
qualifier: string;
}>;
configuration?: ComponentConfiguration;
description?: string;
extensions?: ComponentExtension[];
isFavorite?: boolean;
key: string;

+ 2
- 1
server/sonar-web/src/main/js/app/utils/exposeLibraries.js View File

@@ -21,6 +21,7 @@ import * as ReactRedux from 'react-redux';
import * as ReactRouter from 'react-router';
import Select from 'react-select';
import Modal from 'react-modal';
import throwGlobalError from './throwGlobalError';
import * as measures from '../../helpers/measures';
import * as request from '../../helpers/request';
import * as icons from '../../components/icons-components/icons';
@@ -41,7 +42,7 @@ const exposeLibraries = () => {
window.ReactRouter = ReactRouter;
window.SonarIcons = icons;
window.SonarMeasures = measures;
window.SonarRequest = request;
window.SonarRequest = { ...request, throwGlobalError };
window.SonarComponents = {
DateFromNow,
DateFormatter,

+ 2
- 3
server/sonar-web/src/main/js/app/utils/startReactApp.js View File

@@ -32,7 +32,6 @@ import Landing from '../components/Landing';
import ProjectAdminContainer from '../components/ProjectAdminContainer';
import ProjectPageExtension from '../components/extensions/ProjectPageExtension';
import ProjectAdminPageExtension from '../components/extensions/ProjectAdminPageExtension';
import PortfolioDashboard from '../components/extensions/PortfolioDashboard';
import PortfoliosPage from '../components/extensions/PortfoliosPage';
import AdminContainer from '../components/AdminContainer';
import GlobalPageExtension from '../components/extensions/GlobalPageExtension';
@@ -53,6 +52,7 @@ import metricsRoutes from '../../apps/metrics/routes';
import overviewRoutes from '../../apps/overview/routes';
import organizationsRoutes from '../../apps/organizations/routes';
import permissionTemplatesRoutes from '../../apps/permission-templates/routes';
import portfolioRoutes from '../../apps/portfolio/routes';
import projectActivityRoutes from '../../apps/projectActivity/routes';
import projectAdminRoutes from '../../apps/project-admin/routes';
import projectBranchesRoutes from '../../apps/projectBranches/routes';
@@ -125,7 +125,6 @@ const startReactApp = () => {
<Redirect from="/dashboard/index" to="/dashboard" />
<Redirect from="/governance" to="/portfolio" />
<Redirect from="/groups" to="/admin/groups" />
<Redirect from="/extension/governance/governance" to="/portfolio" />
<Redirect from="/extension/governance/portfolios" to="/portfolios" />
<Redirect from="/metrics" to="/admin/custom_metrics" />
<Redirect from="/permission_templates" to="/admin/permission_templates" />
@@ -189,7 +188,7 @@ const startReactApp = () => {
<Route path="code" childRoutes={codeRoutes} />
<Route path="component_measures" childRoutes={componentMeasuresRoutes} />
<Route path="dashboard" childRoutes={overviewRoutes} />
<Route path="portfolio" component={PortfolioDashboard} />
<Route path="portfolio" childRoutes={portfolioRoutes} />
<Route path="project/activity" childRoutes={projectActivityRoutes} />
<Route
path="project/extension/:pluginKey/:extensionKey"

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

@@ -42,10 +42,7 @@ export default function ComponentMeasure({ component, metricKey, metricType }: P
return <span />;
}

// TODO
const AnyMeasure = Measure as any;

return (
<AnyMeasure measure={{ ...measure, metric: { key: finalMetricKey, type: finalMetricType } }} />
<Measure measure={{ ...measure, metric: { key: finalMetricKey, type: finalMetricType } }} />
);
}

+ 7
- 4
server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js View File

@@ -23,13 +23,13 @@ import { Link } from 'react-router';
import ComplexityDistribution from '../../../components/shared/ComplexityDistribution';
import HistoryIcon from '../../../components/icons-components/HistoryIcon';
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
import LanguageDistribution from '../../../components/charts/LanguageDistribution';
import LanguageDistributionContainer from '../../../components/charts/LanguageDistributionContainer';
import LeakPeriodLegend from './LeakPeriodLegend';
import Measure from '../../../components/measure/Measure';
import Tooltip from '../../../components/controls/Tooltip';
import { isFileType } from '../utils';
import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n';
import { getComponentMeasureHistory } from '../../../helpers/urls';
import { getMeasureHistoryUrl } from '../../../helpers/urls';
import { isDiffMetric } from '../../../helpers/measures';
/*:: import type { Component, Period } from '../types'; */
/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */
@@ -121,7 +121,7 @@ export default class MeasureHeader extends React.PureComponent {
overlay={translate('component_measures.show_metric_history')}>
<Link
className="js-show-history spacer-left button button-small button-compact"
to={getComponentMeasureHistory(component.key, metric.key, branch)}>
to={getMeasureHistoryUrl(component.key, metric.key, branch)}>
<HistoryIcon />
</Link>
</Tooltip>
@@ -137,7 +137,10 @@ export default class MeasureHeader extends React.PureComponent {
{secondaryMeasure &&
secondaryMeasure.metric.key === 'ncloc_language_distribution' && (
<div className="measure-details-secondary">
<LanguageDistribution alignTicks={true} distribution={secondaryMeasure.value} />
<LanguageDistributionContainer
alignTicks={true}
distribution={secondaryMeasure.value}
/>
</div>
)}
{secondaryMeasure &&

+ 1
- 1
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js View File

@@ -82,7 +82,7 @@ it('should render with branch', () => {

it('should display secondary measure too', () => {
const wrapper = shallow(<MeasureHeader {...PROPS} secondaryMeasure={SECONDARY} />);
expect(wrapper.find('LanguageDistribution')).toHaveLength(1);
expect(wrapper.find('Connect(LanguageDistribution)')).toHaveLength(1);
});

it('shohuld display correctly for open file', () => {

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

@@ -19,7 +19,7 @@
*/
// @flow
import React from 'react';
import { AutoSizer } from 'react-virtualized';
import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
import { scaleLinear, scaleOrdinal } from 'd3-scale';
import ColorBoxLegend from '../../../components/charts/ColorBoxLegend';
import ColorGradientLegend from '../../../components/charts/ColorGradientLegend';

+ 1
- 2
server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js View File

@@ -21,9 +21,9 @@
import React from 'react';
import { Link } from 'react-router';
import Analysis from './Analysis';
import PreviewGraph from './PreviewGraph';
import { getMetrics } from '../../../api/metrics';
import { getProjectActivity } from '../../../api/projectActivity';
import PreviewGraph from '../../../components/preview-graph/PreviewGraph';
import { translate } from '../../../helpers/l10n';
/*:: import type { Analysis as AnalysisType } from '../../projectActivity/types'; */
/*:: import type { History, Metric } from '../types'; */
@@ -114,7 +114,6 @@ export default class AnalysesList extends React.PureComponent {
history={this.props.history}
project={this.props.project}
metrics={this.state.metrics}
router={this.props.router}
/>

{this.renderList(analyses)}

+ 2
- 2
server/sonar-web/src/main/js/apps/overview/main/enhance.js View File

@@ -39,7 +39,7 @@ import { getPeriodDate } from '../../../helpers/periods';
import {
getComponentDrilldownUrl,
getComponentIssuesUrl,
getComponentMeasureHistory
getMeasureHistoryUrl
} from '../../../helpers/urls';

export default function enhance(ComposedComponent) {
@@ -175,7 +175,7 @@ export default function enhance(ComposedComponent) {
return (
<Link
className={linkClass}
to={getComponentMeasureHistory(this.props.component.key, metricKey, this.props.branch)}>
to={getMeasureHistoryUrl(this.props.component.key, metricKey, this.props.branch)}>
<HistoryIcon />
</Link>
);

+ 2
- 2
server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js View File

@@ -21,7 +21,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { DrilldownLink } from '../../../components/shared/drilldown-link';
import LanguageDistribution from '../../../components/charts/LanguageDistribution';
import LanguageDistributionContainer from '../../../components/charts/LanguageDistributionContainer';
import SizeRating from '../../../components/ui/SizeRating';
import { formatMeasure } from '../../../helpers/measures';
import { getMetricName } from '../helpers/metrics';
@@ -57,7 +57,7 @@ export default class MetaSize extends React.PureComponent {

return languageDistribution ? (
<div id="overview-language-distribution" className="overview-meta-size-lang-dist">
<LanguageDistribution distribution={languageDistribution.value} />
<LanguageDistributionContainer distribution={languageDistribution.value} />
</div>
) : null;
};

+ 121
- 0
server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx View File

@@ -0,0 +1,121 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { getDisplayedHistoryMetrics, DEFAULT_GRAPH } from '../../projectActivity/utils';
import PreviewGraph from '../../../components/preview-graph/PreviewGraph';
import { getMetrics } from '../../../api/metrics';
import { getAllTimeMachineData } from '../../../api/time-machine';
import { Metric } from '../../../app/types';
import { parseDate } from '../../../helpers/dates';
import { translate } from '../../../helpers/l10n';
import { getCustomGraph, getGraph } from '../../../helpers/storage';

const AnyPreviewGraph = PreviewGraph as any;

interface History {
[metric: string]: Array<{ date: Date; value: string }>;
}

interface Props {
component: string;
}

interface State {
history?: History;
loading: boolean;
metrics?: Metric[];
}

export default class Activity extends React.PureComponent<Props> {
mounted: boolean;
state: State = { loading: true };

componentDidMount() {
this.mounted = true;
this.fetchHistory();
}

componentDidUpdate(prevProps: Props) {
if (prevProps.component !== this.props.component) {
this.fetchHistory();
}
}

componentWillUnmount() {
this.mounted = false;
}

fetchHistory = () => {
const { component } = this.props;

let graphMetrics = getDisplayedHistoryMetrics(getGraph(), getCustomGraph());
if (!graphMetrics || graphMetrics.length <= 0) {
graphMetrics = getDisplayedHistoryMetrics(DEFAULT_GRAPH, []);
}

this.setState({ loading: true });
return Promise.all([getAllTimeMachineData(component, graphMetrics), getMetrics()]).then(
([timeMachine, metrics]) => {
if (this.mounted) {
const history: History = {};
timeMachine.measures.forEach(measure => {
const measureHistory = measure.history.map(analysis => ({
date: parseDate(analysis.date),
value: analysis.value
}));
history[measure.metric] = measureHistory;
});
this.setState({ history, loading: false, metrics });
}
},
() => {
if (this.mounted) {
this.setState({ loading: false });
}
}
);
};

renderWhenEmpty = () => <div className="note">{translate('component_measures.no_history')}</div>;

render() {
return (
<div className="huge-spacer-top">
<header className="page-header">
<h3 className="page-title">{translate('project_activity.page')}</h3>
</header>

{this.state.loading ? (
<i className="spinner" />
) : (
this.state.metrics != undefined &&
this.state.history != undefined && (
<AnyPreviewGraph
history={this.state.history}
metrics={this.state.metrics}
project={this.props.component}
renderWhenEmpty={this.renderWhenEmpty}
/>
)
)}
</div>
);
}
}

+ 150
- 0
server/sonar-web/src/main/js/apps/portfolio/components/App.tsx View File

@@ -0,0 +1,150 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import Summary from './Summary';
import Report from './Report';
import WorstProjects from './WorstProjects';
import ReleasabilityBox from './ReleasabilityBox';
import ReliabilityBox from './ReliabilityBox';
import SecurityBox from './SecurityBox';
import MaintainabilityBox from './MaintainabilityBox';
import Activity from './Activity';
import { getMeasures } from '../../../api/measures';
import { getChildren } from '../../../api/components';
import { PORTFOLIO_METRICS, SUB_COMPONENTS_METRICS, convertMeasures } from '../utils';
import { SubComponent } from '../types';
import '../styles.css';

interface Props {
component: { key: string; name: string };
}

interface State {
loading: boolean;
measures?: { [key: string]: string | undefined };
subComponents?: SubComponent[];
totalSubComponents?: number;
}

export default class App extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = { loading: true };

componentDidMount() {
this.mounted = true;
const html = document.querySelector('html');
if (html) {
html.classList.add('dashboard-page');
}
this.fetchData();
}

componentDidUpdate(prevProps: Props) {
if (prevProps.component !== this.props.component) {
this.fetchData();
}
}

componentWillUnmount() {
this.mounted = false;
const html = document.querySelector('html');
if (html) {
html.classList.remove('dashboard-page');
}
}

fetchData() {
this.setState({ loading: true });
Promise.all([
getMeasures(this.props.component.key, PORTFOLIO_METRICS),
getChildren(this.props.component.key, SUB_COMPONENTS_METRICS, { ps: 20 })
]).then(
([measures, subComponents]) => {
if (this.mounted) {
this.setState({
loading: false,
measures: convertMeasures(measures),
subComponents: subComponents.components.map((component: any) => ({
...component,
measures: convertMeasures(component.measures)
})),
totalSubComponents: subComponents.paging.total
});
}
},
() => {
if (this.mounted) {
this.setState({ loading: false });
}
}
);
}

renderSpinner() {
return (
<div className="page page-limited">
<div className="text-center">
<i className="spinner spinner-margin" />
</div>
</div>
);
}

render() {
const { component } = this.props;
const { loading, measures, subComponents, totalSubComponents } = this.state;

if (loading) {
return this.renderSpinner();
}

return (
<div className="page page-limited">
<div className="page-with-sidebar">
<div className="page-main">
{measures != undefined && (
<div className="portfolio-boxes">
<ReleasabilityBox component={component.key} measures={measures} />
<ReliabilityBox component={component.key} measures={measures} />
<SecurityBox component={component.key} measures={measures} />
<MaintainabilityBox component={component.key} measures={measures} />
</div>
)}

{subComponents != undefined &&
totalSubComponents != undefined && (
<WorstProjects
component={component.key}
subComponents={subComponents}
total={totalSubComponents}
/>
)}
</div>

<aside className="page-sidebar-fixed">
{measures != undefined && <Summary component={component} measures={measures} />}
<Activity component={component.key} />
<Report component={component} />
</aside>
</div>
</div>
);
}
}

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

@@ -0,0 +1,59 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { Link } from 'react-router';
import { FormattedMessage } from 'react-intl';
import Rating from '../../../components/ui/Rating';
import Measure from '../../../components/measure/Measure';
import { translate } from '../../../helpers/l10n';
import { getComponentDrilldownUrl } from '../../../helpers/urls';

interface Props {
component: string;
effort: { projects: number; rating: number };
metricKey: string;
}

export default function Effort({ component, effort, metricKey }: Props) {
return (
<div className="portfolio-effort">
<FormattedMessage
defaultMessage={translate('portfolio.x_in_y')}
id="portfolio.x_in_y"
values={{
projects: (
<Link to={getComponentDrilldownUrl(component, metricKey)}>
<span>
<Measure
measure={{
metric: { key: 'projects', type: 'SHORT_INT' },
value: String(effort.projects)
}}
/>{' '}
{translate('projects_')}
</span>
</Link>
),
rating: <Rating small={true} value={effort.rating} />
}}
/>
</div>
);
}

+ 38
- 0
server/sonar-web/src/main/js/apps/portfolio/components/HistoryButtonLink.tsx View File

@@ -0,0 +1,38 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { Link } from 'react-router';
import { HistoryIcon } from '../../../components/icons-components/icons';
import { getMeasureHistoryUrl } from '../../../helpers/urls';

interface Props {
component: string;
metric: string;
}

export default function HistoryButtonLink({ component, metric }: Props) {
return (
<Link
className="button button-small button-compact spacer-left text-text-bottom"
to={getMeasureHistoryUrl(component, metric)}>
<HistoryIcon size={14} />
</Link>
);
}

+ 37
- 0
server/sonar-web/src/main/js/apps/portfolio/components/MainRating.tsx View File

@@ -0,0 +1,37 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { Link } from 'react-router';
import Rating from '../../../components/ui/Rating';
import { getMeasureTreemapUrl } from '../../../helpers/urls';

interface Props {
component: string;
metric: string;
value: string;
}

export default function MainRating({ component, metric, value }: Props) {
return (
<Link to={getMeasureTreemapUrl(component, metric)} className="portfolio-box-rating">
<Rating value={value} />
</Link>
);
}

+ 54
- 0
server/sonar-web/src/main/js/apps/portfolio/components/MaintainabilityBox.tsx View File

@@ -0,0 +1,54 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import Effort from './Effort';
import MainRating from './MainRating';
import MeasuresButtonLink from './MeasuresButtonLink';
import HistoryButtonLink from './HistoryButtonLink';
import RatingFreshness from './RatingFreshness';
import { translate } from '../../../helpers/l10n';

interface Props {
component: string;
measures: { [key: string]: string | undefined };
}

export default function MaintainabilityBox({ component, measures }: Props) {
const rating = measures['sqale_rating'];
const lastMaintainabilityChange = measures['last_change_on_maintainability_rating'];
const rawEffort = measures['maintainability_rating_effort'];
const effort = rawEffort ? JSON.parse(rawEffort) : undefined;

return (
<div className="portfolio-box portfolio-maintainability">
<h2 className="portfolio-box-title">
{translate('metric_domain.Maintainability')}
<MeasuresButtonLink component={component} metric="Maintainability" />
<HistoryButtonLink component={component} metric="sqale_rating" />
</h2>

{rating && <MainRating component={component} metric={'sqale_rating'} value={rating} />}

<RatingFreshness lastChange={lastMaintainabilityChange} />

{effort && <Effort component={component} effort={effort} metricKey={'sqale_rating'} />}
</div>
);
}

+ 38
- 0
server/sonar-web/src/main/js/apps/portfolio/components/MeasuresButtonLink.tsx View File

@@ -0,0 +1,38 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { Link } from 'react-router';
import BubblesIcon from '../../../components/icons-components/BubblesIcon';
import { getComponentDrilldownUrl } from '../../../helpers/urls';

interface Props {
component: string;
metric: string;
}

export default function MeasuresButtonLink({ component, metric }: Props) {
return (
<Link
className="button button-small button-compact spacer-left text-text-bottom"
to={getComponentDrilldownUrl(component, metric)}>
<BubblesIcon size={14} />
</Link>
);
}

+ 49
- 0
server/sonar-web/src/main/js/apps/portfolio/components/RatingFreshness.tsx View File

@@ -0,0 +1,49 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { FormattedMessage } from 'react-intl';
import DateFromNow from '../../../components/intl/DateFromNow';
import Rating from '../../../components/ui/Rating';
import { translate } from '../../../helpers/l10n';

interface Props {
lastChange?: string;
}

export default function RatingFreshness({ lastChange }: Props) {
if (!lastChange) {
return <div className="portfolio-freshness">&nbsp;</div>;
}

const data = JSON.parse(lastChange);

return (
<div className="portfolio-freshness">
<FormattedMessage
defaultMessage={translate('portfolio.was_x_y')}
id="portfolio.was_x_y"
values={{
rating: <Rating value={data.value} small={true} />,
date: <DateFromNow date={data.date} />
}}
/>
</div>
);
}

+ 68
- 0
server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx View File

@@ -0,0 +1,68 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { Link } from 'react-router';
import RatingFreshness from './RatingFreshness';
import Rating from '../../../components/ui/Rating';
import Measure from '../../../components/measure/Measure';
import { translate } from '../../../helpers/l10n';
import { getComponentDrilldownUrl } from '../../../helpers/urls';

interface Props {
component: string;
measures: { [key: string]: string | undefined };
}

export default function ReleasabilityBox({ component, measures }: Props) {
const rating = measures['releasability_rating'];
const lastReleasabilityChange = measures['last_change_on_releasability_rating'];
const effort = measures['releasability_effort'];

return (
<div className="portfolio-box portfolio-releasability">
<h2 className="portfolio-box-title">{translate('metric_domain.Releasability')}</h2>

{rating && (
<Link
to={getComponentDrilldownUrl(component, 'alert_status')}
className="portfolio-box-rating">
<Rating value={rating} />
</Link>
)}

<RatingFreshness lastChange={lastReleasabilityChange} />

{effort &&
Number(effort) > 0 && (
<div className="portfolio-effort">
<Link to={getComponentDrilldownUrl(component, 'alert_status')}>
<span>
<Measure
measure={{ metric: { key: 'projects', type: 'SHORT_INT' }, value: effort }}
/>{' '}
{Number(effort) === 1 ? 'project' : 'projects'}
</span>
</Link>{' '}
<span className="level level-ERROR level-small">{translate('metric.level.ERROR')}</span>
</div>
)}
</div>
);
}

+ 54
- 0
server/sonar-web/src/main/js/apps/portfolio/components/ReliabilityBox.tsx View File

@@ -0,0 +1,54 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import Effort from './Effort';
import MeasuresButtonLink from './MeasuresButtonLink';
import HistoryButtonLink from './HistoryButtonLink';
import MainRating from './MainRating';
import RatingFreshness from './RatingFreshness';
import { translate } from '../../../helpers/l10n';

interface Props {
component: string;
measures: { [key: string]: string | undefined };
}

export default function ReliabilityBox({ component, measures }: Props) {
const rating = measures['reliability_rating'];
const lastReliabilityChange = measures['last_change_on_reliability_rating'];
const rawEffort = measures['reliability_rating_effort'];
const effort = rawEffort ? JSON.parse(rawEffort) : undefined;

return (
<div className="portfolio-box portfolio-reliability">
<h2 className="portfolio-box-title">
{translate('metric_domain.Reliability')}
<MeasuresButtonLink component={component} metric="Reliability" />
<HistoryButtonLink component={component} metric="reliability_rating" />
</h2>

{rating && <MainRating component={component} metric="reliability_rating" value={rating} />}

<RatingFreshness lastChange={lastReliabilityChange} />

{effort && <Effort component={component} effort={effort} metricKey="reliability_rating" />}
</div>
);
}

+ 112
- 0
server/sonar-web/src/main/js/apps/portfolio/components/Report.tsx View File

@@ -0,0 +1,112 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import SubscriptionContainer from './SubscriptionContainer';
import { getReportStatus, ReportStatus, getReportUrl } from '../../../api/report';
import { translate } from '../../../helpers/l10n';

interface Props {
component: { key: string; name: string };
}

interface State {
loading: boolean;
status?: ReportStatus;
}

export default class Report extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = { loading: true };

componentDidMount() {
this.mounted = true;
this.loadStatus();
}

componentWillUnmount() {
this.mounted = false;
}

loadStatus() {
getReportStatus(this.props.component.key).then(
status => {
if (this.mounted) {
this.setState({ status, loading: false });
}
},
() => {
if (this.mounted) {
this.setState({ loading: false });
}
}
);
}

renderHeader = () => (
<header className="page-header">
<h3 className="page-title">{translate('report.page')}</h3>
</header>
);

render() {
const { component } = this.props;
const { status, loading } = this.state;

if (loading) {
return (
<div className="huge-spacer-top">
{this.renderHeader()}
<i className="spinner" />
</div>
);
}

if (!status) {
return null;
}

return (
<div className="huge-spacer-top">
{this.renderHeader()}

{!status.canDownload && (
<div className="note js-report-cant-download">{translate('report.cant_download')}</div>
)}

{status.canDownload && (
<div className="js-report-can-download">
{translate('report.can_download')}
<div className="spacer-top">
<a
className="button js-report-download"
href={getReportUrl(component.key)}
target="_blank"
download={component.name + ' - Executive Report.pdf'}>
{translate('report.print')}
</a>
</div>
</div>
)}

{status.canSubscribe && <SubscriptionContainer component={component.key} status={status} />}
</div>
);
}
}

+ 54
- 0
server/sonar-web/src/main/js/apps/portfolio/components/SecurityBox.tsx View File

@@ -0,0 +1,54 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import Effort from './Effort';
import MeasuresButtonLink from './MeasuresButtonLink';
import HistoryButtonLink from './HistoryButtonLink';
import RatingFreshness from './RatingFreshness';
import MainRating from './MainRating';
import { translate } from '../../../helpers/l10n';

interface Props {
component: string;
measures: { [key: string]: string | undefined };
}

export default function SecurityBox({ component, measures }: Props) {
const rating = measures['security_rating'];
const lastSecurityChange = measures['last_change_on_security_rating'];
const rawEffort = measures['security_rating_effort'];
const effort = rawEffort ? JSON.parse(rawEffort) : undefined;

return (
<div className="portfolio-box portfolio-security">
<h2 className="portfolio-box-title">
{translate('metric_domain.Security')}
<MeasuresButtonLink component={component} metric="Security" />
<HistoryButtonLink component={component} metric="security_rating" />
</h2>

{rating && <MainRating component={component} metric="security_rating" value={rating} />}

<RatingFreshness lastChange={lastSecurityChange} />

{effort && <Effort component={component} effort={effort} metricKey="security_rating" />}
</div>
);
}

+ 133
- 0
server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx View File

@@ -0,0 +1,133 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { ReportStatus, subscribe, unsubscribe } from '../../../api/report';
import { translate, translateWithParameters } from '../../../helpers/l10n';

interface Props {
component: string;
currentUser: { email?: string };
status: ReportStatus;
}

interface State {
loading: boolean;
subscribed?: boolean;
}

export default class Subscription extends React.PureComponent<Props, State> {
mounted: boolean;

constructor(props: Props) {
super(props);
this.state = { subscribed: props.status.subscribed, loading: false };
}

componentDidMount() {
this.mounted = true;
}

componentWillReceiveProps(nextProps: Props) {
if (nextProps.status.subscribed !== this.props.status.subscribed) {
this.setState({ subscribed: nextProps.status.subscribed });
}
}

componentWillUnmount() {
this.mounted = false;
}

stopLoading = () => {
if (this.mounted) {
this.setState({ loading: false });
}
};

handleSubscription = (subscribed: boolean) => {
if (this.mounted) {
this.setState({ loading: false, subscribed });
}
};

handleSubscribe = (e: React.SyntheticEvent<HTMLButtonElement>) => {
e.preventDefault();
e.currentTarget.blur();
this.setState({ loading: true });
subscribe(this.props.component)
.then(() => this.handleSubscription(true))
.catch(this.stopLoading);
};

handleUnsubscribe = (e: React.SyntheticEvent<HTMLButtonElement>) => {
e.preventDefault();
e.currentTarget.blur();
this.setState({ loading: true });
unsubscribe(this.props.component)
.then(() => this.handleSubscription(false))
.catch(this.stopLoading);
};

getEffectiveFrequencyText = () => {
const effectiveFrequency =
this.props.status.componentFrequency || this.props.status.globalFrequency;
return translate('report.frequency', effectiveFrequency, 'effective');
};

renderLoading = () => this.state.loading && <i className="spacer-left spinner" />;

renderWhenSubscribed = () => (
<div className="js-subscribed">
<div className="spacer-bottom">
<i className="icon-check pull-left spacer-right" />
<div className="overflow-hidden">
{translateWithParameters('report.subscribed', this.getEffectiveFrequencyText())}
</div>
</div>
<button onClick={this.handleUnsubscribe}>{translate('report.unsubscribe')}</button>
{this.renderLoading()}
</div>
);

renderWhenNotSubscribed = () => (
<div className="js-not-subscribed">
<p className="spacer-bottom">
{translateWithParameters('report.unsubscribed', this.getEffectiveFrequencyText())}
</p>
<button className="js-report-subscribe" onClick={this.handleSubscribe}>
{translate('report.subscribe')}
</button>
{this.renderLoading()}
</div>
);

render() {
const hasEmail = !!this.props.currentUser.email;
const { subscribed } = this.state;

let inner;
if (hasEmail) {
inner = subscribed ? this.renderWhenSubscribed() : this.renderWhenNotSubscribed();
} else {
inner = <p className="note js-no-email">{translate('report.no_email_to_subscribe')}</p>;
}

return <div className="big-spacer-top js-report-subscription">{inner}</div>;
}
}

+ 28
- 0
server/sonar-web/src/main/js/apps/portfolio/components/SubscriptionContainer.tsx View File

@@ -0,0 +1,28 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 { connect } from 'react-redux';
import Subscription from './Subscription';
import { getCurrentUser } from '../../../store/rootReducer';

const mapStateToProps = (state: any) => ({
currentUser: getCurrentUser(state)
});

export default connect<any, any, any>(mapStateToProps)(Subscription);

+ 69
- 0
server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx View File

@@ -0,0 +1,69 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { Link } from 'react-router';
import LanguageDistributionContainer from '../../../components/charts/LanguageDistributionContainer';
import Measure from '../../../components/measure/Measure';
import { translate } from '../../../helpers/l10n';
import { getComponentDrilldownUrl } from '../../../helpers/urls';

interface Props {
component: { description?: string; key: string };
measures: { [key: string]: string | undefined };
}

export default function Summary({ component, measures }: Props) {
const projects = measures['projects'];
const ncloc = measures['ncloc'];
const nclocDistribution = measures['ncloc_language_distribution'];

return (
<section id="portfolio-summary" className="portfolio-section portfolio-section-summary">
{component.description && <div className="big-spacer-bottom">{component.description}</div>}

<ul className="portfolio-grid">
<li>
<div className="portfolio-measure-secondary-value">
<Link to={getComponentDrilldownUrl(component.key, 'projects')}>
<Measure
measure={{ metric: { key: 'projects', type: 'SHORT_INT' }, value: projects }}
/>
</Link>
</div>
{translate('projects')}
</li>
<li>
<div className="portfolio-measure-secondary-value">
<Link to={getComponentDrilldownUrl(component.key, 'ncloc')}>
<Measure measure={{ metric: { key: 'ncloc', type: 'SHORT_INT' }, value: ncloc }} />
</Link>
</div>
{translate('metric.ncloc.name')}
</li>
</ul>

{nclocDistribution && (
<div className="huge-spacer-top" style={{ width: 260 }}>
<LanguageDistributionContainer distribution={nclocDistribution} />
</div>
)}
</section>
);
}

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

@@ -0,0 +1,140 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { Link } from 'react-router';
import { max } from 'lodash';
import { SubComponent } from '../types';
import Measure from '../../../components/measure/Measure';
import QualifierIcon from '../../../components/shared/QualifierIcon';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';
import { getProjectUrl } from '../../../helpers/urls';

interface Props {
component: string;
subComponents: SubComponent[];
total: number;
}

export default function WorstProjects({ component, subComponents, total }: Props) {
const count = subComponents.length;

if (!count) {
return null;
}

const maxLoc = max(
subComponents.map(component => Number(component.measures['ncloc'] || 0))
) as number;

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

return (
<div className="panel panel-white portfolio-sub-components" id="portfolio-sub-components">
<table className="data zebra">
<thead>
<tr>
<th>&nbsp;</th>
<th className="text-center portfolio-sub-components-cell">
{translate('metric_domain.Releasability')}
</th>
<th className="text-center portfolio-sub-components-cell">
{translate('metric_domain.Reliability')}
</th>
<th className="text-center portfolio-sub-components-cell">
{translate('metric_domain.Security')}
</th>
<th className="text-center portfolio-sub-components-cell">
{translate('metric_domain.Maintainability')}
</th>
<th className="text-center portfolio-sub-components-cell">
{translate('metric.ncloc.name')}
</th>
</tr>
</thead>
<tbody>
{subComponents.map(component => (
<tr key={component.key}>
<td>
<Link
to={getProjectUrl(component.refKey || component.key)}
className="link-with-icon">
<QualifierIcon qualifier={component.qualifier} /> {component.name}
</Link>
</td>
{component.qualifier === 'TRK' ? (
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, 'sqale_rating', 'RATING')}
{renderNcloc(component.measures, maxLoc)}
</tr>
))}
</tbody>
</table>

{total > count && (
<footer className="spacer-top note text-center">
{translateWithParameters(
'x_of_y_shown',
formatMeasure(count, 'INT'),
formatMeasure(total, 'INT')
)}
<Link to={projectsPageUrl} className="spacer-left">
{translate('show_more')}
</Link>
</footer>
)}
</div>
);
}

function renderCell(measures: { [key: string]: string | undefined }, metric: string, type: string) {
return (
<td className="text-center">
<Measure measure={{ metric: { key: metric, type }, value: measures[metric] }} />
</td>
);
}

function renderNcloc(measures: { [key: string]: string | undefined }, maxLoc: number) {
const ncloc = Number(measures['ncloc'] || 0);
const barWidth = maxLoc > 0 ? Math.max(1, Math.round(ncloc / maxLoc * 50)) : 0;
return (
<td className="text-right">
<span className="note">
<Measure
measure={{
metric: { key: 'ncloc', type: 'SHORT_INT' },
value: measures['ncloc']
}}
/>
</span>
{maxLoc > 0 && (
<svg width="50" height="16" className="spacer-left">
<rect className="bar-chart-bar" x="0" y="3" width={barWidth} height="10" />
</svg>
)}
</td>
);
}

+ 77
- 0
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx View File

@@ -0,0 +1,77 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
jest.mock('../../../../helpers/storage', () => ({
getCustomGraph: () => ['coverage'],
getGraph: () => 'custom'
}));

jest.mock('../../../../api/metrics', () => ({
getMetrics: jest.fn(() => Promise.resolve([]))
}));

jest.mock('../../../../api/time-machine', () => ({
getAllTimeMachineData: jest.fn(() =>
Promise.resolve({
measures: [
{
metric: 'coverage',
history: [
{ date: '2017-01-01T00:00:00.000Z', value: '73' },
{ date: '2017-01-02T00:00:00.000Z', value: '82' }
]
}
]
})
)
}));

import * as React from 'react';
import { mount, shallow } from 'enzyme';
import Activity from '../Activity';

const getMetrics = require('../../../../api/metrics').getMetrics as jest.Mock<any>;
const getAllTimeMachineData = require('../../../../api/time-machine')
.getAllTimeMachineData as jest.Mock<any>;

beforeEach(() => {
getMetrics.mockClear();
getAllTimeMachineData.mockClear();
});

it('renders', () => {
const wrapper = shallow(<Activity component="foo" />);
wrapper.setState({
history: {
coverage: [
{ date: '2017-01-01T00:00:00.000Z', value: '73' },
{ date: '2017-01-02T00:00:00.000Z', value: '82' }
]
},
loading: false,
metrics: [{ key: 'coverage' }]
});
expect(wrapper).toMatchSnapshot();
});

it('fetches history', () => {
mount(<Activity component="foo" />);
expect(getMetrics).toBeCalled();
expect(getAllTimeMachineData).toBeCalledWith('foo', ['coverage']);
});

+ 89
- 0
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx View File

@@ -0,0 +1,89 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
jest.mock('../../../../api/measures', () => ({
getMeasures: jest.fn(() => Promise.resolve([]))
}));

jest.mock('../../../../api/components', () => ({
getChildren: jest.fn(() => Promise.resolve({ components: [], paging: { total: 0 } }))
}));

// mock Activity to not deal with localstorage
jest.mock('../Activity', () => ({
default: function Activity() {
return null;
}
}));

jest.mock('../Report', () => ({
default: function Report() {
return null;
}
}));

import * as React from 'react';
import { shallow, mount } from 'enzyme';
import App from '../App';

const getMeasures = require('../../../../api/measures').getMeasures as jest.Mock<any>;
const getChildren = require('../../../../api/components').getChildren as jest.Mock<any>;

const component = { key: 'foo', name: 'Foo' };

it('renders', () => {
const wrapper = shallow(<App component={component} />);
wrapper.setState({ loading: false, measures: {}, subComponents: [], totalSubComponents: 0 });
expect(wrapper).toMatchSnapshot();
});

it('fetches measures and children components', () => {
getMeasures.mockClear();
getChildren.mockClear();
mount(<App component={component} />);
expect(getMeasures).toBeCalledWith('foo', [
'projects',
'ncloc',
'ncloc_language_distribution',
'releasability_rating',
'releasability_effort',
'sqale_rating',
'maintainability_rating_effort',
'reliability_rating',
'reliability_rating_effort',
'security_rating',
'security_rating_effort',
'last_change_on_releasability_rating',
'last_change_on_maintainability_rating',
'last_change_on_security_rating',
'last_change_on_reliability_rating'
]);
expect(getChildren).toBeCalledWith(
'foo',
[
'ncloc',
'releasability_rating',
'security_rating',
'reliability_rating',
'sqale_rating',
'alert_status'
],
{ ps: 20 }
);
});

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

@@ -0,0 +1,30 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { shallow } from 'enzyme';
import Effort from '../Effort';

it('renders', () => {
expect(
shallow(
<Effort component="foo" effort={{ projects: 3, rating: 2 }} metricKey="security_rating" />
)
).toMatchSnapshot();
});

+ 26
- 0
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/HistoryButtonLink-test.tsx View File

@@ -0,0 +1,26 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { shallow } from 'enzyme';
import HistoryButtonLink from '../HistoryButtonLink';

it('renders', () => {
expect(shallow(<HistoryButtonLink component="foo" metric="security_rating" />)).toMatchSnapshot();
});

+ 28
- 0
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MainRating-test.tsx View File

@@ -0,0 +1,28 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { shallow } from 'enzyme';
import MainRating from '../MainRating';

it('renders', () => {
expect(
shallow(<MainRating component="foo" metric="security_rating" value="3" />)
).toMatchSnapshot();
});

+ 31
- 0
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MaintainabilityBox-test.tsx View File

@@ -0,0 +1,31 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { shallow } from 'enzyme';
import MaintainabilityBox from '../MaintainabilityBox';

it('renders', () => {
const measures = {
sqale_rating: '3',
last_change_on_maintainability_rating: '{"date":"2017-01-02T00:00:00.000Z","value":2}',
maintainability_rating_effort: '{"rating":3,"projects":1}'
};
expect(shallow(<MaintainabilityBox component="foo" measures={measures} />)).toMatchSnapshot();
});

+ 28
- 0
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MeasuresButtonLink-test.tsx View File

@@ -0,0 +1,28 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { shallow } from 'enzyme';
import MeasuresButtonLink from '../MeasuresButtonLink';

it('renders', () => {
expect(
shallow(<MeasuresButtonLink component="foo" metric="security_rating" />)
).toMatchSnapshot();
});

+ 31
- 0
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/RatingFreshness-test.tsx View File

@@ -0,0 +1,31 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { shallow } from 'enzyme';
import RatingFreshness from '../RatingFreshness';

it('renders', () => {
const lastChange = '{"date":"2017-01-02T00:00:00.000Z","value":2}';
expect(shallow(<RatingFreshness lastChange={lastChange} />)).toMatchSnapshot();
});

it('renders empty', () => {
expect(shallow(<RatingFreshness />)).toMatchSnapshot();
});

+ 31
- 0
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReleasabilityBox-test.tsx View File

@@ -0,0 +1,31 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { shallow } from 'enzyme';
import ReleasabilityBox from '../ReleasabilityBox';

it('renders', () => {
const measures = {
releasability_rating: '3',
last_change_on_releasability_rating: '{"date":"2017-01-02T00:00:00.000Z","value":2}',
releasability_effort: '7'
};
expect(shallow(<ReleasabilityBox component="foo" measures={measures} />)).toMatchSnapshot();
});

+ 31
- 0
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReliabilityBox-test.tsx View File

@@ -0,0 +1,31 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { shallow } from 'enzyme';
import ReliabilityBox from '../ReliabilityBox';

it('renders', () => {
const measures = {
reliability_rating: '3',
last_change_on_reliability_rating: '{"date":"2017-01-02T00:00:00.000Z","value":2}',
reliability_rating_effort: '{"rating":3,"projects":1}'
};
expect(shallow(<ReliabilityBox component="foo" measures={measures} />)).toMatchSnapshot();
});

+ 54
- 0
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Report-test.tsx View File

@@ -0,0 +1,54 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
jest.mock('../../../../api/report', () => {
const report = require.requireActual('../../../../api/report');
report.getReportStatus = jest.fn(() => Promise.resolve({}));
return report;
});

import * as React from 'react';
import { mount, shallow } from 'enzyme';
import Report from '../Report';

const getReportStatus = require('../../../../api/report').getReportStatus as jest.Mock<any>;

const component = { key: 'foo', name: 'Foo' };

it('renders', () => {
const wrapper = shallow(<Report component={component} />);
expect(wrapper).toMatchSnapshot();
wrapper.setState({
loading: false,
status: {
canDownload: true,
canSubscribe: true,
componentFrequency: 'montly',
globalFrequency: 'weekly',
subscribed: true
}
});
expect(wrapper).toMatchSnapshot();
});

it('fetches status', () => {
getReportStatus.mockClear();
mount(<Report component={component} />);
expect(getReportStatus).toBeCalledWith('foo');
});

+ 31
- 0
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/SecurityBox-test.tsx View File

@@ -0,0 +1,31 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { shallow } from 'enzyme';
import SecurityBox from '../SecurityBox';

it('renders', () => {
const measures = {
security_rating: '3',
last_change_on_security_rating: '{"date":"2017-01-02T00:00:00.000Z","value":2}',
security_rating_effort: '{"rating":3,"projects":1}'
};
expect(shallow(<SecurityBox component="foo" measures={measures} />)).toMatchSnapshot();
});

+ 84
- 0
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Subscription-test.tsx View File

@@ -0,0 +1,84 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
jest.mock('../../../../api/report', () => {
const report = require.requireActual('../../../../api/report');
report.subscribe = jest.fn(() => Promise.resolve());
report.unsubscribe = jest.fn(() => Promise.resolve());
return report;
});

import * as React from 'react';
import { mount, shallow } from 'enzyme';
import Subscription from '../Subscription';
import { click } from '../../../../helpers/testUtils';

const subscribe = require('../../../../api/report').subscribe as jest.Mock<any>;
const unsubscribe = require('../../../../api/report').unsubscribe as jest.Mock<any>;

const status = {
canDownload: true,
canSubscribe: true,
componentFrequency: 'montly',
globalFrequency: 'weekly',
subscribed: true
};

const currentUser = { email: 'foo@example.com' };

beforeEach(() => {
subscribe.mockClear();
unsubscribe.mockClear();
});

it('renders when subscribed', () => {
expect(
shallow(<Subscription component="foo" currentUser={currentUser} status={status} />)
).toMatchSnapshot();
});

it('renders when not subscribed', () => {
expect(
shallow(
<Subscription
component="foo"
currentUser={currentUser}
status={{ ...status, subscribed: false }}
/>
)
).toMatchSnapshot();
});

it('renders when no email', () => {
expect(
shallow(<Subscription component="foo" currentUser={{}} status={status} />)
).toMatchSnapshot();
});

it('changes subscription', async () => {
const wrapper = mount(<Subscription component="foo" currentUser={currentUser} status={status} />);
click(wrapper.find('button'));
expect(unsubscribe).toBeCalledWith('foo');

await new Promise(setImmediate);
wrapper.update();

click(wrapper.find('button'));
expect(subscribe).toBeCalledWith('foo');
});

+ 33
- 0
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Summary-test.tsx View File

@@ -0,0 +1,33 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { shallow } from 'enzyme';
import Summary from '../Summary';

it('renders', () => {
expect(
shallow(
<Summary
component={{ description: 'blabla', key: 'foo' }}
measures={{ ncloc: '1234', ncloc_language_distribution: 'java=13;js=17', projects: '15' }}
/>
)
).toMatchSnapshot();
});

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

@@ -0,0 +1,68 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { shallow } from 'enzyme';
import WorstProjects from '../WorstProjects';

it('renders', () => {
const subComponents = [
{
key: 'foo',
measures: {
releasability_rating: '3',
reliability_rating: '2',
security_rating: '1',
sqale_rating: '4',
ncloc: '200'
},
name: 'Foo',
qualifier: 'SVW'
},
{
key: 'bar',
measures: {
alert_status: 'ERROR',
reliability_rating: '2',
security_rating: '1',
sqale_rating: '4',
ncloc: '100'
},
name: 'Bar',
qualifier: 'TRK',
refKey: 'barbar'
},
{
key: 'baz',
measures: {
alert_status: 'WARN',
reliability_rating: '2',
security_rating: '1',
sqale_rating: '4',
ncloc: '150'
},
name: 'Baz',
qualifier: 'TRK',
refKey: 'bazbaz'
}
];
expect(
shallow(<WorstProjects component="comp" subComponents={subComponents} total={3} />)
).toMatchSnapshot();
});

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

@@ -0,0 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<div
className="huge-spacer-top"
>
<header
className="page-header"
>
<h3
className="page-title"
>
project_activity.page
</h3>
</header>
<PreviewGraph
history={
Object {
"coverage": Array [
Object {
"date": "2017-01-01T00:00:00.000Z",
"value": "73",
},
Object {
"date": "2017-01-02T00:00:00.000Z",
"value": "82",
},
],
}
}
metrics={
Array [
Object {
"key": "coverage",
},
]
}
project="foo"
renderWhenEmpty={[Function]}
/>
</div>
`;

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

@@ -0,0 +1,65 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<div
className="page page-limited"
>
<div
className="page-with-sidebar"
>
<div
className="page-main"
>
<div
className="portfolio-boxes"
>
<ReleasabilityBox
component="foo"
measures={Object {}}
/>
<ReliabilityBox
component="foo"
measures={Object {}}
/>
<SecurityBox
component="foo"
measures={Object {}}
/>
<MaintainabilityBox
component="foo"
measures={Object {}}
/>
</div>
<WorstProjects
component="foo"
subComponents={Array []}
total={0}
/>
</div>
<aside
className="page-sidebar-fixed"
>
<Summary
component={
Object {
"key": "foo",
"name": "Foo",
}
}
measures={Object {}}
/>
<Activity
component="foo"
/>
<Report
component={
Object {
"key": "foo",
"name": "Foo",
}
}
/>
</aside>
</div>
</div>
`;

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

@@ -0,0 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<div
className="portfolio-effort"
>
<FormattedMessage
defaultMessage="portfolio.x_in_y"
id="portfolio.x_in_y"
values={
Object {
"projects": <Link
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/component_measures",
"query": Object {
"branch": undefined,
"id": "foo",
"metric": "security_rating",
},
}
}
>
<span>
<Measure
measure={
Object {
"metric": Object {
"key": "projects",
"type": "SHORT_INT",
},
"value": "3",
}
}
/>
projects_
</span>
</Link>,
"rating": <Rating
small={true}
value={2}
/>,
}
}
/>
</div>
`;

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

@@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<Link
className="button button-small button-compact spacer-left text-text-bottom"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/activity",
"query": Object {
"branch": undefined,
"custom_metrics": "security_rating",
"graph": "custom",
"id": "foo",
},
}
}
>
<IconHistory
size={14}
/>
</Link>
`;

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

@@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<Link
className="portfolio-box-rating"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/component_measures",
"query": Object {
"branch": undefined,
"id": "foo",
"metric": "security_rating",
"view": "treemap",
},
}
}
>
<Rating
value="3"
/>
</Link>
`;

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

@@ -0,0 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<div
className="portfolio-box portfolio-maintainability"
>
<h2
className="portfolio-box-title"
>
metric_domain.Maintainability
<MeasuresButtonLink
component="foo"
metric="Maintainability"
/>
<HistoryButtonLink
component="foo"
metric="sqale_rating"
/>
</h2>
<MainRating
component="foo"
metric="sqale_rating"
value="3"
/>
<RatingFreshness
lastChange="{\\"date\\":\\"2017-01-02T00:00:00.000Z\\",\\"value\\":2}"
/>
<Effort
component="foo"
effort={
Object {
"projects": 1,
"rating": 3,
}
}
metricKey="sqale_rating"
/>
</div>
`;

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

@@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<Link
className="button button-small button-compact spacer-left text-text-bottom"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/component_measures",
"query": Object {
"branch": undefined,
"id": "foo",
"metric": "security_rating",
},
}
}
>
<BubblesIcon
size={14}
/>
</Link>
`;

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

@@ -0,0 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<div
className="portfolio-freshness"
>
<FormattedMessage
defaultMessage="portfolio.was_x_y"
id="portfolio.was_x_y"
values={
Object {
"date": <DateFromNow
date="2017-01-02T00:00:00.000Z"
/>,
"rating": <Rating
small={true}
value={2}
/>,
}
}
/>
</div>
`;

exports[`renders empty 1`] = `
<div
className="portfolio-freshness"
>
 
</div>
`;

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

@@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<div
className="portfolio-box portfolio-releasability"
>
<h2
className="portfolio-box-title"
>
metric_domain.Releasability
</h2>
<Link
className="portfolio-box-rating"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/component_measures",
"query": Object {
"branch": undefined,
"id": "foo",
"metric": "alert_status",
},
}
}
>
<Rating
value="3"
/>
</Link>
<RatingFreshness
lastChange="{\\"date\\":\\"2017-01-02T00:00:00.000Z\\",\\"value\\":2}"
/>
<div
className="portfolio-effort"
>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/component_measures",
"query": Object {
"branch": undefined,
"id": "foo",
"metric": "alert_status",
},
}
}
>
<span>
<Measure
measure={
Object {
"metric": Object {
"key": "projects",
"type": "SHORT_INT",
},
"value": "7",
}
}
/>
projects
</span>
</Link>
<span
className="level level-ERROR level-small"
>
metric.level.ERROR
</span>
</div>
</div>
`;

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

@@ -0,0 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<div
className="portfolio-box portfolio-reliability"
>
<h2
className="portfolio-box-title"
>
metric_domain.Reliability
<MeasuresButtonLink
component="foo"
metric="Reliability"
/>
<HistoryButtonLink
component="foo"
metric="reliability_rating"
/>
</h2>
<MainRating
component="foo"
metric="reliability_rating"
value="3"
/>
<RatingFreshness
lastChange="{\\"date\\":\\"2017-01-02T00:00:00.000Z\\",\\"value\\":2}"
/>
<Effort
component="foo"
effort={
Object {
"projects": 1,
"rating": 3,
}
}
metricKey="reliability_rating"
/>
</div>
`;

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

@@ -0,0 +1,65 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<div
className="huge-spacer-top"
>
<header
className="page-header"
>
<h3
className="page-title"
>
report.page
</h3>
</header>
<i
className="spinner"
/>
</div>
`;

exports[`renders 2`] = `
<div
className="huge-spacer-top"
>
<header
className="page-header"
>
<h3
className="page-title"
>
report.page
</h3>
</header>
<div
className="js-report-can-download"
>
report.can_download
<div
className="spacer-top"
>
<a
className="button js-report-download"
download="Foo - Executive Report.pdf"
href="/api/governance_reports/download?componentKey=foo"
target="_blank"
>
report.print
</a>
</div>
</div>
<Connect(Subscription)
component="foo"
status={
Object {
"canDownload": true,
"canSubscribe": true,
"componentFrequency": "montly",
"globalFrequency": "weekly",
"subscribed": true,
}
}
/>
</div>
`;

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

@@ -0,0 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<div
className="portfolio-box portfolio-security"
>
<h2
className="portfolio-box-title"
>
metric_domain.Security
<MeasuresButtonLink
component="foo"
metric="Security"
/>
<HistoryButtonLink
component="foo"
metric="security_rating"
/>
</h2>
<MainRating
component="foo"
metric="security_rating"
value="3"
/>
<RatingFreshness
lastChange="{\\"date\\":\\"2017-01-02T00:00:00.000Z\\",\\"value\\":2}"
/>
<Effort
component="foo"
effort={
Object {
"projects": 1,
"rating": 3,
}
}
metricKey="security_rating"
/>
</div>
`;

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

@@ -0,0 +1,63 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders when no email 1`] = `
<div
className="big-spacer-top js-report-subscription"
>
<p
className="note js-no-email"
>
report.no_email_to_subscribe
</p>
</div>
`;

exports[`renders when not subscribed 1`] = `
<div
className="big-spacer-top js-report-subscription"
>
<div
className="js-not-subscribed"
>
<p
className="spacer-bottom"
>
report.unsubscribed.report.frequency.montly.effective
</p>
<button
className="js-report-subscribe"
onClick={[Function]}
>
report.subscribe
</button>
</div>
</div>
`;

exports[`renders when subscribed 1`] = `
<div
className="big-spacer-top js-report-subscription"
>
<div
className="js-subscribed"
>
<div
className="spacer-bottom"
>
<i
className="icon-check pull-left spacer-right"
/>
<div
className="overflow-hidden"
>
report.subscribed.report.frequency.montly.effective
</div>
</div>
<button
onClick={[Function]}
>
report.unsubscribe
</button>
</div>
</div>
`;

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

@@ -0,0 +1,96 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<section
className="portfolio-section portfolio-section-summary"
id="portfolio-summary"
>
<div
className="big-spacer-bottom"
>
blabla
</div>
<ul
className="portfolio-grid"
>
<li>
<div
className="portfolio-measure-secondary-value"
>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/component_measures",
"query": Object {
"branch": undefined,
"id": "foo",
"metric": "projects",
},
}
}
>
<Measure
measure={
Object {
"metric": Object {
"key": "projects",
"type": "SHORT_INT",
},
"value": "15",
}
}
/>
</Link>
</div>
projects
</li>
<li>
<div
className="portfolio-measure-secondary-value"
>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/component_measures",
"query": Object {
"branch": undefined,
"id": "foo",
"metric": "ncloc",
},
}
}
>
<Measure
measure={
Object {
"metric": Object {
"key": "ncloc",
"type": "SHORT_INT",
},
"value": "1234",
}
}
/>
</Link>
</div>
metric.ncloc.name
</li>
</ul>
<div
className="huge-spacer-top"
style={
Object {
"width": 260,
}
}
>
<Connect(LanguageDistribution)
distribution="java=13;js=17"
/>
</div>
</section>
`;

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

@@ -0,0 +1,395 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<div
className="panel panel-white portfolio-sub-components"
id="portfolio-sub-components"
>
<table
className="data zebra"
>
<thead>
<tr>
<th>
 
</th>
<th
className="text-center portfolio-sub-components-cell"
>
metric_domain.Releasability
</th>
<th
className="text-center portfolio-sub-components-cell"
>
metric_domain.Reliability
</th>
<th
className="text-center portfolio-sub-components-cell"
>
metric_domain.Security
</th>
<th
className="text-center portfolio-sub-components-cell"
>
metric_domain.Maintainability
</th>
<th
className="text-center portfolio-sub-components-cell"
>
metric.ncloc.name
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<Link
className="link-with-icon"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "foo",
},
}
}
>
<QualifierIcon
qualifier="SVW"
/>
Foo
</Link>
</td>
<td
className="text-center"
>
<Measure
measure={
Object {
"metric": Object {
"key": "releasability_rating",
"type": "RATING",
},
"value": "3",
}
}
/>
</td>
<td
className="text-center"
>
<Measure
measure={
Object {
"metric": Object {
"key": "reliability_rating",
"type": "RATING",
},
"value": "2",
}
}
/>
</td>
<td
className="text-center"
>
<Measure
measure={
Object {
"metric": Object {
"key": "security_rating",
"type": "RATING",
},
"value": "1",
}
}
/>
</td>
<td
className="text-center"
>
<Measure
measure={
Object {
"metric": Object {
"key": "sqale_rating",
"type": "RATING",
},
"value": "4",
}
}
/>
</td>
<td
className="text-right"
>
<span
className="note"
>
<Measure
measure={
Object {
"metric": Object {
"key": "ncloc",
"type": "SHORT_INT",
},
"value": "200",
}
}
/>
</span>
<svg
className="spacer-left"
height="16"
width="50"
>
<rect
className="bar-chart-bar"
height="10"
width={50}
x="0"
y="3"
/>
</svg>
</td>
</tr>
<tr>
<td>
<Link
className="link-with-icon"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "barbar",
},
}
}
>
<QualifierIcon
qualifier="TRK"
/>
Bar
</Link>
</td>
<td
className="text-center"
>
<Measure
measure={
Object {
"metric": Object {
"key": "alert_status",
"type": "LEVEL",
},
"value": "ERROR",
}
}
/>
</td>
<td
className="text-center"
>
<Measure
measure={
Object {
"metric": Object {
"key": "reliability_rating",
"type": "RATING",
},
"value": "2",
}
}
/>
</td>
<td
className="text-center"
>
<Measure
measure={
Object {
"metric": Object {
"key": "security_rating",
"type": "RATING",
},
"value": "1",
}
}
/>
</td>
<td
className="text-center"
>
<Measure
measure={
Object {
"metric": Object {
"key": "sqale_rating",
"type": "RATING",
},
"value": "4",
}
}
/>
</td>
<td
className="text-right"
>
<span
className="note"
>
<Measure
measure={
Object {
"metric": Object {
"key": "ncloc",
"type": "SHORT_INT",
},
"value": "100",
}
}
/>
</span>
<svg
className="spacer-left"
height="16"
width="50"
>
<rect
className="bar-chart-bar"
height="10"
width={25}
x="0"
y="3"
/>
</svg>
</td>
</tr>
<tr>
<td>
<Link
className="link-with-icon"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "bazbaz",
},
}
}
>
<QualifierIcon
qualifier="TRK"
/>
Baz
</Link>
</td>
<td
className="text-center"
>
<Measure
measure={
Object {
"metric": Object {
"key": "alert_status",
"type": "LEVEL",
},
"value": "WARN",
}
}
/>
</td>
<td
className="text-center"
>
<Measure
measure={
Object {
"metric": Object {
"key": "reliability_rating",
"type": "RATING",
},
"value": "2",
}
}
/>
</td>
<td
className="text-center"
>
<Measure
measure={
Object {
"metric": Object {
"key": "security_rating",
"type": "RATING",
},
"value": "1",
}
}
/>
</td>
<td
className="text-center"
>
<Measure
measure={
Object {
"metric": Object {
"key": "sqale_rating",
"type": "RATING",
},
"value": "4",
}
}
/>
</td>
<td
className="text-right"
>
<span
className="note"
>
<Measure
measure={
Object {
"metric": Object {
"key": "ncloc",
"type": "SHORT_INT",
},
"value": "150",
}
}
/>
</span>
<svg
className="spacer-left"
height="16"
width="50"
>
<rect
className="bar-chart-bar"
height="10"
width={38}
x="0"
y="3"
/>
</svg>
</td>
</tr>
</tbody>
</table>
</div>
`;

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

@@ -0,0 +1,30 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 { RouterState, IndexRouteProps } from 'react-router';

const routes = [
{
getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) {
import('./components/App').then(i => callback(null, { component: (i as any).default }));
}
}
];

export default routes;

+ 95
- 0
server/sonar-web/src/main/js/apps/portfolio/styles.css View File

@@ -0,0 +1,95 @@
.portfolio-measure-secondary-value {
line-height: 1.4;
margin-bottom: 4px;
font-size: 24px;
font-weight: 300;
}

.portfolio-grid {
position: relative;
z-index: 10;
display: flex;
height: 80px;
justify-content: space-around;
align-items: center;
}

.portfolio-grid > li {
vertical-align: top;
width: 50%;
text-align: center;
}

.portfolio-grid > li.text-middle {
vertical-align: middle;
}

.portfolio-freshness {
line-height: 24px;
margin-top: 12px;
color: #777;
font-size: 12px;
white-space: nowrap;
}

.portfolio-effort {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e6e6e6;
}

.portfolio-boxes {
display: flex;
justify-content: space-between;
align-items: stretch;
margin-bottom: 20px;
padding: 15px 0;
border: 1px solid #e6e6e6;
background-color: #fff;
}

.portfolio-box {
position: relative;
width: 25%;
padding: 0 5px;
border-radius: 3px;
box-sizing: border-box;
text-align: center;
}

.portfolio-box-title {
margin-bottom: 25px;
font-size: 16px;
}

.portfolio-box-title > .button-small > svg {
margin-top: 0;
}

.portfolio-box-rating,
.portfolio-box-rating .rating {
display: block;
width: 120px;
height: 120px;
line-height: 120px;
}

.portfolio-box-rating {
margin: 0 auto;
border: none;
}

.portfolio-box-rating .rating {
border-radius: 120px;
font-size: 60px;
text-align: center;
}

.portfolio-sub-components table.data > thead > tr > th {
font-size: 13px;
text-transform: none;
}

.portfolio-sub-components-cell {
width: 90px;
}

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

@@ -0,0 +1,26 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 interface SubComponent {
key: string;
measures: { [key: string]: string | undefined };
name: string;
refKey?: string;
qualifier: string;
}

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

@@ -0,0 +1,92 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 getNextRating(rating: number): number | undefined {
return rating > 1 ? rating - 1 : undefined;
}

function getWorstSeverity(data: string): { severity: string; count: number } | undefined {
const SEVERITY_ORDER = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'];

const severities: { [key: string]: number } = {};
data.split(';').forEach(equality => {
const [key, count] = equality.split('=');
severities[key] = Number(count);
});

for (let i = 0; i < SEVERITY_ORDER.length; i++) {
const count = severities[SEVERITY_ORDER[i]];
if (count > 0) {
return { severity: SEVERITY_ORDER[i], count };
}
}

return undefined;
}

export function getEffortToNextRating(
measures: Array<{ metric: { key: string }; value: string }>,
metricKey: string
) {
const measure = measures.find(measure => measure.metric.key === metricKey);
if (!measure) {
return undefined;
}
return getWorstSeverity(measure.value);
}

export const PORTFOLIO_METRICS = [
'projects',
'ncloc',
'ncloc_language_distribution',

'releasability_rating',
'releasability_effort',

'sqale_rating',
'maintainability_rating_effort',

'reliability_rating',
'reliability_rating_effort',

'security_rating',
'security_rating_effort',

'last_change_on_releasability_rating',
'last_change_on_maintainability_rating',
'last_change_on_security_rating',
'last_change_on_reliability_rating'
];

export const SUB_COMPONENTS_METRICS = [
'ncloc',
'releasability_rating',
'security_rating',
'reliability_rating',
'sqale_rating',
'alert_status'
];

export function convertMeasures(measures: Array<{ metric: string; value?: string }>) {
const result: { [key: string]: string | undefined } = {};
measures.forEach(measure => {
result[measure.metric] = measure.value;
});
return result;
}

+ 2
- 2
server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js View File

@@ -221,8 +221,8 @@ describe('parseQuery', () => {
expect(
utils.parseQuery({
from: '2017-04-27T08:21:32.000Z',
id: 'foo',
custom_metrics: 'foo,bar,baz'
custom_metrics: 'foo,bar,baz',
id: 'foo'
})
).toEqual(QUERY);
});

+ 1
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/GraphHistory.js View File

@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import { AutoSizer } from 'react-virtualized';
import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
import GraphsTooltips from './GraphsTooltips';
import GraphsLegendCustom from './GraphsLegendCustom';

+ 1
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js View File

@@ -35,7 +35,7 @@ type Props = {
graphs: Array<Array<Serie>>,
graphEndDate: ?Date,
graphStartDate: ?Date,
leakPeriodDate: Date,
leakPeriodDate?: Date,
loading: boolean,
measuresHistory: Array<MeasureHistory>,
removeCustomMetric: (metric: string) => void,

+ 2
- 2
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsZoom.js View File

@@ -19,7 +19,7 @@
*/
// @flow
import React from 'react';
import { AutoSizer } from 'react-virtualized';
import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
import ZoomTimeLine from '../../../components/charts/ZoomTimeLine';
import { hasHistoryData } from '../utils';
/*:: import type { Serie } from '../../../components/charts/AdvancedTimeline'; */
@@ -28,7 +28,7 @@ import { hasHistoryData } from '../utils';
type Props = {
graphEndDate: ?Date,
graphStartDate: ?Date,
leakPeriodDate: Date,
leakPeriodDate?: Date,
loading: boolean,
metricsType: string,
series: Array<Serie>,

+ 3
- 2
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js View File

@@ -170,12 +170,13 @@ export default class ProjectActivityAnalysesList extends React.PureComponent {
const selectedDate = this.props.query.selectedDate
? this.props.query.selectedDate.valueOf()
: null;

return (
<ul
className={classNames('project-activity-versions-list', this.props.className)}
onScroll={this.handleScroll}
ref={element => (this.scrollContainer = element)}
style={{ paddingTop: this.props.project.qualifier === 'APP' ? undefined : 52 }}>
style={{ paddingTop: this.props.project.qualifier === 'TRK' ? 52 : undefined }}>
{byVersionByDay.map((version, idx) => {
const days = Object.keys(version.byDay);
if (days.length <= 0) {
@@ -205,7 +206,7 @@ export default class ProjectActivityAnalysesList extends React.PureComponent {
addVersion={this.props.addVersion}
analysis={analysis}
canAdmin={this.props.canAdmin}
canCreateVersion={this.props.project.qualifier !== 'APP'}
canCreateVersion={this.props.project.qualifier === 'TRK'}
changeEvent={this.props.changeEvent}
deleteAnalysis={this.props.deleteAnalysis}
deleteEvent={this.props.deleteEvent}

+ 7
- 3
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js View File

@@ -42,7 +42,7 @@ type Props = {
project: {
configuration?: { showHistory: boolean },
key: string,
leakPeriodDate: string,
leakPeriodDate?: string,
qualifier: string
},
metrics: Array<Metric>,
@@ -55,7 +55,9 @@ type Props = {
export default function ProjectActivityApp(props /*: Props */) {
const { analyses, measuresHistory, query } = props;
const { configuration } = props.project;
const canAdmin = configuration ? configuration.showHistory : false;
const canAdmin =
(props.project.qualifier === 'TRK' || props.project.qualifier === 'APP') &&
(configuration ? configuration.showHistory : false);
return (
<div id="project-activity" className="page page-limited">
<Helmet title={translate('project_activity.page')} />
@@ -89,7 +91,9 @@ export default function ProjectActivityApp(props /*: Props */) {
<div className="project-activity-layout-page-main">
<ProjectActivityGraphs
analyses={analyses}
leakPeriodDate={parseDate(props.project.leakPeriodDate)}
leakPeriodDate={
props.project.leakPeriodDate ? parseDate(props.project.leakPeriodDate) : undefined
}
loading={props.graphLoading}
measuresHistory={measuresHistory}
metrics={props.metrics}

+ 27
- 12
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js View File

@@ -42,15 +42,18 @@ import {
/*:: import type { Analysis, MeasureHistory, Metric, Paging, Query } from '../types'; */

/*::
type Component = {
breadcrumbs: Array<{ key: string, qualifier: string}>,
configuration?: { showHistory: boolean },
key: string,
leakPeriodDate?: string,
qualifier: string
};

type Props = {
branch?: {},
location: { pathname: string, query: RawQuery },
component: {
configuration?: { showHistory: boolean },
key: string,
leakPeriodDate: string,
qualifier: string
}
component: Component
};
*/

@@ -106,7 +109,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
}
});
} else {
this.firstLoadData(this.state.query);
this.firstLoadData(this.state.query, this.props.component);
}
}

@@ -117,7 +120,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
if (this.state.initialized) {
this.updateGraphData(query.graph, query.customMetrics);
} else {
this.firstLoadData(query);
this.firstLoadData(query, nextProps.component);
}
}
this.setState({ query });
@@ -177,7 +180,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
branch: this.props.branch && getBranchName(this.props.branch)
};
return api
.getProjectActivity({ ...parameters, ...additional })
.getProjectActivity({ ...additional, ...parameters })
.then(({ analyses, paging }) => ({
analyses: analyses.map(analysis => ({ ...analysis, date: parseDate(analysis.date) })),
paging
@@ -227,10 +230,22 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
});
};

firstLoadData(query /*: Query */) {
getTopLevelComponent = (component /*: Component */) => {
let current = component.breadcrumbs.length - 1;
while (
current > 0 &&
!['TRK', 'VW', 'APP'].includes(component.breadcrumbs[current].qualifier)
) {
current--;
}
return component.breadcrumbs[current].key;
};

firstLoadData(query /*: Query */, component /*: Component */) {
const graphMetrics = getHistoryMetrics(query.graph, query.customMetrics);
const topLevelComponent = this.getTopLevelComponent(component);
Promise.all([
this.fetchActivity(query.project, 1, 100, serializeQuery(query)),
this.fetchActivity(topLevelComponent, 1, 100, serializeQuery(query)),
this.fetchMetrics(),
this.fetchMeasuresHistory(graphMetrics)
]).then(
@@ -246,7 +261,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
paging: response[0].paging
});

this.loadAllActivities(query.project).then(({ analyses, paging }) => {
this.loadAllActivities(topLevelComponent).then(({ analyses, paging }) => {
if (this.mounted) {
this.setState({
analyses,

+ 1
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js View File

@@ -40,7 +40,7 @@ import {
/*::
type Props = {
analyses: Array<Analysis>,
leakPeriodDate: Date,
leakPeriodDate?: Date,
loading: boolean,
measuresHistory: Array<MeasureHistory>,
metrics: Array<Metric>,

+ 13
- 11
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js View File

@@ -54,17 +54,19 @@ export default class ProjectActivityPageHeader extends React.PureComponent {

return (
<header className="page-header">
<Select
className="input-medium pull-left big-spacer-right"
placeholder={translate('project_activity.filter_events') + '...'}
clearable={true}
searchable={false}
value={this.props.category}
optionComponent={ProjectActivityEventSelectOption}
valueComponent={ProjectActivityEventSelectValue}
options={this.options}
onChange={this.handleCategoryChange}
/>
{!['VW', 'SVW'].includes(this.props.project.qualifier) && (
<Select
className="input-medium pull-left big-spacer-right"
placeholder={translate('project_activity.filter_events') + '...'}
clearable={true}
searchable={false}
value={this.props.category}
optionComponent={ProjectActivityEventSelectOption}
valueComponent={ProjectActivityEventSelectValue}
options={this.options}
onChange={this.handleCategoryChange}
/>
)}
<ProjectActivityDateInput
className="pull-left"
from={this.props.from}

+ 6
- 14
server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeakMeasures.tsx View File

@@ -38,7 +38,7 @@ export default function ProjectCardLeakMeasures({ measures }: Props) {
<Measure
className="spacer-right"
measure={{
metric: { key: 'new_bugs', name: 'new_bugs', type: 'SHORT_INT' },
metric: { key: 'new_bugs', type: 'SHORT_INT' },
leak: measures['new_bugs']
}}
/>
@@ -57,11 +57,7 @@ export default function ProjectCardLeakMeasures({ measures }: Props) {
<Measure
className="spacer-right"
measure={{
metric: {
key: 'new_vulnerabilities',
name: 'new_vulnerabilities',
type: 'SHORT_INT'
},
metric: { key: 'new_vulnerabilities', type: 'SHORT_INT' },
leak: measures['new_vulnerabilities']
}}
/>
@@ -80,7 +76,7 @@ export default function ProjectCardLeakMeasures({ measures }: Props) {
<Measure
className="spacer-right"
measure={{
metric: { key: 'new_code_smells', name: 'new_code_smells', type: 'SHORT_INT' },
metric: { key: 'new_code_smells', type: 'SHORT_INT' },
leak: measures['new_code_smells']
}}
/>
@@ -98,7 +94,7 @@ export default function ProjectCardLeakMeasures({ measures }: Props) {
<div className="project-card-measure-number">
<Measure
measure={{
metric: { key: 'new_coverage', name: 'new_coverage', type: 'PERCENT' },
metric: { key: 'new_coverage', type: 'PERCENT' },
leak: measures['new_coverage']
}}
/>
@@ -112,11 +108,7 @@ export default function ProjectCardLeakMeasures({ measures }: Props) {
<div className="project-card-measure-number">
<Measure
measure={{
metric: {
key: 'new_duplicated_lines_density',
name: 'new_duplicated_lines_density',
type: 'PERCENT'
},
metric: { key: 'new_duplicated_lines_density', type: 'PERCENT' },
leak: measures['new_duplicated_lines_density']
}}
/>
@@ -132,7 +124,7 @@ export default function ProjectCardLeakMeasures({ measures }: Props) {
<div className="project-card-measure-number">
<Measure
measure={{
metric: { key: 'new_lines', name: 'new_lines', type: 'SHORT_INT' },
metric: { key: 'new_lines', type: 'SHORT_INT' },
leak: measures['new_lines']
}}
/>

+ 3
- 7
server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx View File

@@ -76,7 +76,7 @@ export default function ProjectCardOverallMeasures({ measures }: Props) {
)}
<Measure
measure={{
metric: { key: 'coverage', name: 'coverage', type: 'PERCENT' },
metric: { key: 'coverage', type: 'PERCENT' },
value: measures['coverage']
}}
/>
@@ -95,11 +95,7 @@ export default function ProjectCardOverallMeasures({ measures }: Props) {
)}
<Measure
measure={{
metric: {
key: 'duplicated_lines_density',
name: 'duplicated_lines_density',
type: 'PERCENT'
},
metric: { key: 'duplicated_lines_density', type: 'PERCENT' },
value: measures['duplicated_lines_density']
}}
/>
@@ -119,7 +115,7 @@ export default function ProjectCardOverallMeasures({ measures }: Props) {
</span>
<Measure
measure={{
metric: { key: 'ncloc', name: 'ncloc', type: 'SHORT_INT' },
metric: { key: 'ncloc', type: 'SHORT_INT' },
value: measures['ncloc']
}}
/>

+ 0
- 12
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeakMeasures-test.tsx.snap View File

@@ -21,7 +21,6 @@ exports[`should render correctly with all data 1`] = `
"leak": "8",
"metric": Object {
"key": "new_bugs",
"name": "new_bugs",
"type": "SHORT_INT",
},
}
@@ -58,7 +57,6 @@ exports[`should render correctly with all data 1`] = `
"leak": "2",
"metric": Object {
"key": "new_vulnerabilities",
"name": "new_vulnerabilities",
"type": "SHORT_INT",
},
}
@@ -95,7 +93,6 @@ exports[`should render correctly with all data 1`] = `
"leak": "0",
"metric": Object {
"key": "new_code_smells",
"name": "new_code_smells",
"type": "SHORT_INT",
},
}
@@ -131,7 +128,6 @@ exports[`should render correctly with all data 1`] = `
"leak": "26.55",
"metric": Object {
"key": "new_coverage",
"name": "new_coverage",
"type": "PERCENT",
},
}
@@ -161,7 +157,6 @@ exports[`should render correctly with all data 1`] = `
"leak": "0.55",
"metric": Object {
"key": "new_duplicated_lines_density",
"name": "new_duplicated_lines_density",
"type": "PERCENT",
},
}
@@ -191,7 +186,6 @@ exports[`should render correctly with all data 1`] = `
"leak": "87",
"metric": Object {
"key": "new_lines",
"name": "new_lines",
"type": "SHORT_INT",
},
}
@@ -229,7 +223,6 @@ exports[`should render no data style new coverage, new duplications and new line
"leak": "8",
"metric": Object {
"key": "new_bugs",
"name": "new_bugs",
"type": "SHORT_INT",
},
}
@@ -266,7 +259,6 @@ exports[`should render no data style new coverage, new duplications and new line
"leak": "2",
"metric": Object {
"key": "new_vulnerabilities",
"name": "new_vulnerabilities",
"type": "SHORT_INT",
},
}
@@ -303,7 +295,6 @@ exports[`should render no data style new coverage, new duplications and new line
"leak": "0",
"metric": Object {
"key": "new_code_smells",
"name": "new_code_smells",
"type": "SHORT_INT",
},
}
@@ -339,7 +330,6 @@ exports[`should render no data style new coverage, new duplications and new line
"leak": undefined,
"metric": Object {
"key": "new_coverage",
"name": "new_coverage",
"type": "PERCENT",
},
}
@@ -369,7 +359,6 @@ exports[`should render no data style new coverage, new duplications and new line
"leak": undefined,
"metric": Object {
"key": "new_duplicated_lines_density",
"name": "new_duplicated_lines_density",
"type": "PERCENT",
},
}
@@ -399,7 +388,6 @@ exports[`should render no data style new coverage, new duplications and new line
"leak": undefined,
"metric": Object {
"key": "new_lines",
"name": "new_lines",
"type": "SHORT_INT",
},
}

+ 0
- 6
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverallMeasures-test.tsx.snap View File

@@ -16,7 +16,6 @@ exports[`should not render coverage 1`] = `
Object {
"metric": Object {
"key": "coverage",
"name": "coverage",
"type": "PERCENT",
},
"value": undefined,
@@ -49,7 +48,6 @@ exports[`should not render duplications 1`] = `
Object {
"metric": Object {
"key": "duplicated_lines_density",
"name": "duplicated_lines_density",
"type": "PERCENT",
},
"value": undefined,
@@ -155,7 +153,6 @@ exports[`should render correctly with all data 1`] = `
Object {
"metric": Object {
"key": "coverage",
"name": "coverage",
"type": "PERCENT",
},
"value": "88.3",
@@ -192,7 +189,6 @@ exports[`should render correctly with all data 1`] = `
Object {
"metric": Object {
"key": "duplicated_lines_density",
"name": "duplicated_lines_density",
"type": "PERCENT",
},
"value": "9.8",
@@ -229,7 +225,6 @@ exports[`should render correctly with all data 1`] = `
Object {
"metric": Object {
"key": "ncloc",
"name": "ncloc",
"type": "SHORT_INT",
},
"value": "2053",
@@ -270,7 +265,6 @@ exports[`should render ncloc correctly 1`] = `
Object {
"metric": Object {
"key": "ncloc",
"name": "ncloc",
"type": "SHORT_INT",
},
"value": "16549887",

+ 15
- 16
server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js View File

@@ -152,22 +152,21 @@ export default ModalView.extend({
.filter(metric => metric.type !== 'DATA' && !metric.hidden)
.map(metric => metric.key);

return getMeasures(
this.options.component.key,
metricsToRequest,
this.options.branch
).then(measures => {
let nextMeasures = this.options.component.measures || {};
measures.forEach(measure => {
const metric = metrics.find(metric => metric.key === measure.metric);
nextMeasures[metric.key] = formatMeasure(measure.value, metric.type);
nextMeasures[metric.key + '_raw'] = measure.value;
metric.value = nextMeasures[metric.key];
});
nextMeasures = this.calcAdditionalMeasures(nextMeasures);
this.measures = nextMeasures;
this.measuresToDisplay = this.prepareMetrics(metrics);
});
return getMeasures(this.options.component.key, metricsToRequest, this.options.branch).then(
measures => {
let nextMeasures = this.options.component.measures || {};
measures.forEach(measure => {
const metric = metrics.find(metric => metric.key === measure.metric);
nextMeasures[metric.key] = formatMeasure(measure.value, metric.type);
nextMeasures[metric.key + '_raw'] = measure.value;
metric.value = nextMeasures[metric.key];
});
nextMeasures = this.calcAdditionalMeasures(nextMeasures);
this.measures = nextMeasures;
this.measuresToDisplay = this.prepareMetrics(metrics);
},
() => {}
);
});
},


+ 0
- 89
server/sonar-web/src/main/js/components/charts/LanguageDistribution.js View File

@@ -1,89 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 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 { find, sortBy } from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import { Histogram } from './histogram';
import { formatMeasure } from '../../helpers/measures';
import { getLanguages } from '../../api/languages';
import { translate } from '../../helpers/l10n';

export default class LanguageDistribution extends React.PureComponent {
static propTypes = {
alignTicks: PropTypes.bool,
distribution: PropTypes.string.isRequired
};

state = {};

componentDidMount() {
this.mounted = true;
this.requestLanguages();
}

componentWillUnmount() {
this.mounted = false;
}

requestLanguages() {
getLanguages().then(languages => {
if (this.mounted) {
this.setState({ languages });
}
});
}

getLanguageName(langKey) {
if (this.state.languages) {
const lang = find(this.state.languages, { key: langKey });
return lang ? lang.name : translate('unknown');
} else {
return langKey;
}
}

cutLanguageName(name) {
return name.length > 10 ? `${name.substr(0, 7)}...` : name;
}

render() {
let data = this.props.distribution.split(';').map((point, index) => {
const tokens = point.split('=');
return { x: parseInt(tokens[1], 10), y: index, value: tokens[0] };
});

data = sortBy(data, d => -d.x);

const yTicks = data.map(point => this.getLanguageName(point.value)).map(this.cutLanguageName);
const yValues = data.map(point => formatMeasure(point.x, 'SHORT_INT'));

return (
<Histogram
alignTicks={this.props.alignTicks}
data={data}
yTicks={yTicks}
yValues={yValues}
barsWidth={10}
height={data.length * 25}
padding={[0, 60, 0, 80]}
/>
);
}
}

+ 64
- 0
server/sonar-web/src/main/js/components/charts/LanguageDistribution.tsx View File

@@ -0,0 +1,64 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* 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.
*
* 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 program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { find, sortBy } from 'lodash';
import { Histogram } from './histogram';
import { formatMeasure } from '../../helpers/measures';
import { Language } from '../../api/languages';
import { translate } from '../../helpers/l10n';

interface Props {
alignTicks?: boolean;
distribution: string;
languages?: Language[];
}

export default function LanguageDistribution(props: Props) {
let data = props.distribution.split(';').map((point, index) => {
const tokens = point.split('=');
return { x: parseInt(tokens[1], 10), y: index, value: tokens[0] };
});

data = sortBy(data, d => -d.x);

const yTicks = data.map(point => getLanguageName(point.value)).map(cutLanguageName);
const yValues = data.map(point => formatMeasure(point.x, 'SHORT_INT'));

return (
<Histogram
alignTicks={props.alignTicks}
data={data}
yTicks={yTicks}
yValues={yValues}
barsWidth={10}
height={data.length * 25}
padding={[0, 60, 0, 80]}
/>
);

function getLanguageName(langKey: string) {
const lang = find(props.languages, { key: langKey });
return lang ? lang.name : translate('unknown');
}

function cutLanguageName(name: string) {
return name.length > 10 ? `${name.substr(0, 7)}...` : name;
}
}

server/sonar-web/src/main/js/app/components/extensions/PortfolioDashboard.tsx → server/sonar-web/src/main/js/components/charts/LanguageDistributionContainer.tsx View File

@@ -17,20 +17,12 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import ProjectPageExtension from './ProjectPageExtension';
import { Component } from '../../types';
import { connect } from 'react-redux';
import { getLanguages } from '../../store/rootReducer';
import LanguageDistribution from './LanguageDistribution';

interface Props {
component: Component;
location: { query: { id: string } };
}
const mapStateToProps = (state: any) => ({
languages: getLanguages(state)
});

export default function PortfolioDashboard(props: Props) {
return (
<ProjectPageExtension
{...props}
params={{ pluginKey: 'governance', extensionKey: 'governance' }}
/>
);
}
export default connect<any, any, any>(mapStateToProps)(LanguageDistribution);

+ 2
- 1
server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js View File

@@ -38,7 +38,7 @@ type Props = {
endDate: ?Date,
height: number,
width: number,
leakPeriodDate: Date,
leakPeriodDate?: Date,
padding: Array<number>,
series: Array<Serie>,
showAreas?: boolean,
@@ -394,6 +394,7 @@ export default class ZoomTimeLine extends React.PureComponent {
}

const { xScale, yScale } = this.getScales();

return (
<svg className="line-chart " width={this.props.width} height={this.props.height}>
<g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0] + 2})`}>

server/sonar-web/src/main/js/components/icons-components/BubblesIcon.js → server/sonar-web/src/main/js/components/icons-components/BubblesIcon.tsx View File

@@ -17,14 +17,14 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';

/*::
type Props = { className?: string, size?: number };
*/
interface Props {
className?: string;
size?: number;
}

export default function BubblesIcon({ className, size = 16 } /*: Props */) {
export default function BubblesIcon({ className, size = 16 }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"

server/sonar-web/src/main/js/components/icons-components/HistoryIcon.js → server/sonar-web/src/main/js/components/icons-components/HistoryIcon.tsx View File

@@ -17,15 +17,14 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';

/*::
type Props = { className?: string, size?: number };
*/
interface Props {
className?: string;
size?: number;
}

export default function IconHistory({ className, size = 16 } /*: Props */) {
/* eslint max-len: 0 */
export default function IconHistory({ className, size = 16 }: Props) {
return (
<svg
className={className}

+ 0
- 1
server/sonar-web/src/main/js/components/icons-components/LinkIcon.tsx View File

@@ -25,7 +25,6 @@ interface Props {
}

export default function LinkIcon({ className, size = 14 }: Props) {
/* eslint-disable max-len */
return (
<svg
xmlns="http://www.w3.org/2000/svg"

+ 6
- 2
server/sonar-web/src/main/js/components/measure/Measure.tsx View File

@@ -27,10 +27,14 @@ import { formatLeak, getRatingTooltip, MeasureEnhanced } from './utils';
interface Props {
className?: string;
decimals?: number | null;
measure: MeasureEnhanced;
measure?: MeasureEnhanced;
}

export default function Measure({ className, decimals, measure }: Props) {
if (measure == undefined) {
return <span>{'–'}</span>;
}

const metric = measure.metric;
const value = isDiffMetric(metric.key) ? measure.leak : measure.value;

@@ -44,7 +48,7 @@ export default function Measure({ className, decimals, measure }: Props) {

if (metric.type !== 'RATING') {
const formattedValue = isDiffMetric(metric.key)
? formatLeak(measure.leak, metric, { decimals })
? formatLeak(measure.leak, metric.key, metric.type, { decimals })
: formatMeasure(measure.value, metric.type, { decimals });
return <span className={className}>{formattedValue != null ? formattedValue : '–'}</span>;
}

+ 10
- 5
server/sonar-web/src/main/js/components/measure/utils.ts View File

@@ -37,7 +37,7 @@ export interface Measure extends MeasureIntern {
}

export interface MeasureEnhanced extends MeasureIntern {
metric: Metric;
metric: { key: string; type: string };
leak?: string | undefined | undefined;
}

@@ -53,11 +53,16 @@ export function enhanceMeasure(
};
}

export function formatLeak(value: string | undefined, metric: Metric, options: any): string {
if (isDiffMetric(metric.key)) {
return formatMeasure(value, metric.type, options);
export function formatLeak(
value: string | undefined,
metricKey: string,
metricType: string,
options: any
): string {
if (isDiffMetric(metricKey)) {
return formatMeasure(value, metricType, options);
} else {
return formatMeasureVariation(value, metric.type, options);
return formatMeasureVariation(value, metricType, options);
}
}


server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js → server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js View File

@@ -20,8 +20,9 @@
// @flow
import React from 'react';
import { minBy } from 'lodash';
import { AutoSizer } from 'react-virtualized';
import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
import * as PropTypes from 'prop-types';
import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
import AdvancedTimeline from '../charts/AdvancedTimeline';
import PreviewGraphTooltips from './PreviewGraphTooltips';
import {
DEFAULT_GRAPH,
@@ -30,11 +31,11 @@ import {
getSeriesMetricType,
hasHistoryDataValue,
splitSeriesInGraphs
} from '../../projectActivity/utils';
import { getCustomGraph, getGraph } from '../../../helpers/storage';
import { formatMeasure, getShortType } from '../../../helpers/measures';
/*:: import type { Serie } from '../../../components/charts/AdvancedTimeline'; */
/*:: import type { History, Metric } from '../types'; */
} from '../../apps/projectActivity/utils';
import { getCustomGraph, getGraph } from '../../helpers/storage';
import { formatMeasure, getShortType } from '../../helpers/measures';
/*:: import type { Serie } from '../charts/AdvancedTimeline'; */
/*:: import type { History, Metric } from '../../apps/overview/types'; */

/*::
type Props = {
@@ -42,7 +43,7 @@ type Props = {
history: ?History,
metrics: Array<Metric>,
project: string,
router: { push: ({ pathname: string, query?: {} }) => void }
renderWhenEmpty?: () => void
};
*/

@@ -65,6 +66,10 @@ export default class PreviewGraph extends React.PureComponent {
/*:: props: Props; */
/*:: state: State; */

static contextTypes = {
router: PropTypes.object
};

constructor(props /*: Props */) {
super(props);
const graph = getGraph();
@@ -137,7 +142,7 @@ export default class PreviewGraph extends React.PureComponent {
};

handleClick = () => {
this.props.router.push({
this.context.router.push({
pathname: '/project/activity',
query: { id: this.props.project, branch: this.props.branch }
});
@@ -192,7 +197,7 @@ export default class PreviewGraph extends React.PureComponent {
render() {
const { series } = this.state;
if (!hasHistoryDataValue(series)) {
return null;
return this.props.renderWhenEmpty ? this.props.renderWhenEmpty() : null;
}

return (

server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js → server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.js View File

@@ -18,11 +18,11 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import BubblePopup from '../../../components/common/BubblePopup';
import DateFormatter from '../../../components/intl/DateFormatter';
import BubblePopup from '../common/BubblePopup';
import DateFormatter from '../intl/DateFormatter';
import PreviewGraphTooltipsContent from './PreviewGraphTooltipsContent';
/*:: import type { Metric } from '../types'; */
/*:: import type { Serie } from '../../../components/charts/AdvancedTimeline'; */
/*:: import type { Serie } from '../charts/AdvancedTimeline'; */

/*::
type Props = {

server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltipsContent.js → server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltipsContent.js View File

@@ -19,7 +19,7 @@
*/
// @flow
import React from 'react';
import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
import ChartLegendIcon from '../icons-components/ChartLegendIcon';

/*::
type Props = {

server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltips-test.js → server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltips-test.js View File

@@ -20,8 +20,8 @@
import React from 'react';
import { shallow } from 'enzyme';
import PreviewGraphTooltips from '../PreviewGraphTooltips';
import { DEFAULT_GRAPH } from '../../../projectActivity/utils';
import { parseDate } from '../../../../helpers/dates';
import { DEFAULT_GRAPH } from '../../../apps/projectActivity/utils';
import { parseDate } from '../../../helpers/dates';

const SERIES_ISSUES = [
{

server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltipsContent-test.js → server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltipsContent-test.js View File


server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap → server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap View File


server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.js.snap → server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.js.snap View File


+ 2
- 2
server/sonar-web/src/main/js/helpers/testUtils.ts View File

@@ -17,7 +17,7 @@
* 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, ShallowRendererProps, ShallowWrapper } from 'enzyme';
import { shallow, ShallowRendererProps, ShallowWrapper, ReactWrapper } from 'enzyme';
import { IntlProvider } from 'react-intl';

export const mockEvent = {
@@ -27,7 +27,7 @@ export const mockEvent = {
stopPropagation() {}
};

export function click(element: ShallowWrapper, event = {}): void {
export function click(element: ShallowWrapper | ReactWrapper, event = {}): void {
element.simulate('click', { ...mockEvent, ...event });
}


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


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save