aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2017-09-15 15:00:45 +0200
committerTeryk Bellahsene <teryk@users.noreply.github.com>2017-09-20 09:15:23 +0200
commit15705813665581d67d67bff133d31d235dd16535 (patch)
treebbe2d5d6be6eeffdc667bab66e584b0f24c851e3 /server/sonar-web
parent4c2dafad1f82d5a79a061335908ca0e7ea80b6ee (diff)
downloadsonarqube-15705813665581d67d67bff133d31d235dd16535.tar.gz
sonarqube-15705813665581d67d67bff133d31d235dd16535.zip
SONAR-9812 display activity page for portfolios (#2510)
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/api/languages.ts7
-rw-r--r--server/sonar-web/src/main/js/api/measures.ts5
-rw-r--r--server/sonar-web/src/main/js/api/metrics.ts3
-rw-r--r--server/sonar-web/src/main/js/api/report.ts55
-rw-r--r--server/sonar-web/src/main/js/app/components/ComponentContainer.tsx14
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx15
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx21
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap646
-rw-r--r--server/sonar-web/src/main/js/app/types.ts1
-rw-r--r--server/sonar-web/src/main/js/app/utils/exposeLibraries.js3
-rw-r--r--server/sonar-web/src/main/js/app/utils/startReactApp.js5
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js11
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js2
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js2
-rw-r--r--server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js3
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main/enhance.js4
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js4
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx121
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/App.tsx150
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx59
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/HistoryButtonLink.tsx38
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/MainRating.tsx37
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/MaintainabilityBox.tsx54
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/MeasuresButtonLink.tsx38
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/RatingFreshness.tsx49
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx68
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/ReliabilityBox.tsx54
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/Report.tsx112
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/SecurityBox.tsx54
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx133
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/SubscriptionContainer.tsx28
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx69
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx140
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx77
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx89
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Effort-test.tsx30
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/HistoryButtonLink-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MainRating-test.tsx28
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MaintainabilityBox-test.tsx31
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MeasuresButtonLink-test.tsx28
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/RatingFreshness-test.tsx31
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReleasabilityBox-test.tsx31
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReliabilityBox-test.tsx31
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Report-test.tsx54
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/SecurityBox-test.tsx31
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Subscription-test.tsx84
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Summary-test.tsx33
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/WorstProjects-test.tsx68
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap42
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap65
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap50
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/HistoryButtonLink-test.tsx.snap24
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MainRating-test.tsx.snap24
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MaintainabilityBox-test.tsx.snap39
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MeasuresButtonLink-test.tsx.snap23
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/RatingFreshness-test.tsx.snap31
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap75
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReliabilityBox-test.tsx.snap39
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Report-test.tsx.snap65
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/SecurityBox-test.tsx.snap39
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Subscription-test.tsx.snap63
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap96
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap395
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/routes.ts30
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/styles.css95
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/types.ts26
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/utils.ts92
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js4
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/GraphHistory.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/GraphsZoom.js4
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js5
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js10
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js39
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js24
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeakMeasures.tsx20
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeakMeasures-test.tsx.snap12
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverallMeasures-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js31
-rw-r--r--server/sonar-web/src/main/js/components/charts/LanguageDistribution.js89
-rw-r--r--server/sonar-web/src/main/js/components/charts/LanguageDistribution.tsx64
-rw-r--r--server/sonar-web/src/main/js/components/charts/LanguageDistributionContainer.tsx (renamed from server/sonar-web/src/main/js/app/components/extensions/PortfolioDashboard.tsx)22
-rw-r--r--server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js3
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/BubblesIcon.tsx (renamed from server/sonar-web/src/main/js/components/icons-components/BubblesIcon.js)12
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/HistoryIcon.tsx (renamed from server/sonar-web/src/main/js/components/icons-components/HistoryIcon.js)13
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/LinkIcon.tsx1
-rw-r--r--server/sonar-web/src/main/js/components/measure/Measure.tsx8
-rw-r--r--server/sonar-web/src/main/js/components/measure/utils.ts15
-rw-r--r--server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js (renamed from server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js)25
-rw-r--r--server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.js (renamed from server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js)6
-rw-r--r--server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltipsContent.js (renamed from server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltipsContent.js)2
-rw-r--r--server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltips-test.js (renamed from server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltips-test.js)4
-rw-r--r--server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltipsContent-test.js (renamed from server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltipsContent-test.js)0
-rw-r--r--server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap (renamed from server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap)0
-rw-r--r--server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.js.snap (renamed from server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.js.snap)0
-rw-r--r--server/sonar-web/src/main/js/helpers/testUtils.ts4
-rw-r--r--server/sonar-web/src/main/js/helpers/urls.ts15
100 files changed, 4170 insertions, 279 deletions
diff --git a/server/sonar-web/src/main/js/api/languages.ts b/server/sonar-web/src/main/js/api/languages.ts
index ee5bd130a5e..cb4722540ae 100644
--- a/server/sonar-web/src/main/js/api/languages.ts
+++ b/server/sonar-web/src/main/js/api/languages.ts
@@ -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);
}
diff --git a/server/sonar-web/src/main/js/api/measures.ts b/server/sonar-web/src/main/js/api/measures.ts
index df194d04a34..c50933f4b21 100644
--- a/server/sonar-web/src/main/js/api/measures.ts
+++ b/server/sonar-web/src/main/js/api/measures.ts
@@ -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(
diff --git a/server/sonar-web/src/main/js/api/metrics.ts b/server/sonar-web/src/main/js/api/metrics.ts
index ea2f78d569c..985a8206403 100644
--- a/server/sonar-web/src/main/js/api/metrics.ts
+++ b/server/sonar-web/src/main/js/api/metrics.ts
@@ -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);
}
diff --git a/server/sonar-web/src/main/js/api/report.ts b/server/sonar-web/src/main/js/api/report.ts
new file mode 100644
index 00000000000..e7c608ee2d6
--- /dev/null
+++ b/server/sonar-web/src/main/js/api/report.ts
@@ -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
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
index 99bd095c9e8..085b4be1979 100644
--- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
+++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
@@ -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) => {
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
index e3c49a90b9a..625043eb923 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
@@ -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>
);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx
index ad86f8f2246..2e4af3fb2ee 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx
@@ -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();
+ }
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
index 1cb875eee66..ea4cd792997 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
@@ -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>
diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts
index 2d0bc46db36..1db17ec0ffd 100644
--- a/server/sonar-web/src/main/js/app/types.ts
+++ b/server/sonar-web/src/main/js/app/types.ts
@@ -67,6 +67,7 @@ export interface Component {
qualifier: string;
}>;
configuration?: ComponentConfiguration;
+ description?: string;
extensions?: ComponentExtension[];
isFavorite?: boolean;
key: string;
diff --git a/server/sonar-web/src/main/js/app/utils/exposeLibraries.js b/server/sonar-web/src/main/js/app/utils/exposeLibraries.js
index e69bb3ab64a..b0d2a4fdd9f 100644
--- a/server/sonar-web/src/main/js/app/utils/exposeLibraries.js
+++ b/server/sonar-web/src/main/js/app/utils/exposeLibraries.js
@@ -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,
diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js
index 15d742e3730..b6577565efd 100644
--- a/server/sonar-web/src/main/js/app/utils/startReactApp.js
+++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js
@@ -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"
diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx
index c765ff3eb59..d21ccaaf032 100644
--- a/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx
+++ b/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx
@@ -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 } }} />
);
}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js
index b2763cbcdc5..de343e95337 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js
@@ -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 &&
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js
index 73619312a76..8164a603717 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js
@@ -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', () => {
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js
index 4411d4743ec..aa7fa92452a 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js
@@ -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';
diff --git a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js
index 1494366789d..2c181d03d8c 100644
--- a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js
+++ b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js
@@ -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)}
diff --git a/server/sonar-web/src/main/js/apps/overview/main/enhance.js b/server/sonar-web/src/main/js/apps/overview/main/enhance.js
index 73ab9386a3e..3e3759dfb33 100644
--- a/server/sonar-web/src/main/js/apps/overview/main/enhance.js
+++ b/server/sonar-web/src/main/js/apps/overview/main/enhance.js
@@ -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>
);
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js
index 686611d5d91..a488f469da6 100644
--- a/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js
+++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js
@@ -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;
};
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx
new file mode 100644
index 00000000000..26a66872a01
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx
new file mode 100644
index 00000000000..e0910baa420
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx
new file mode 100644
index 00000000000..f655af6e4e7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/HistoryButtonLink.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/HistoryButtonLink.tsx
new file mode 100644
index 00000000000..fbf281d704d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/HistoryButtonLink.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/MainRating.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/MainRating.tsx
new file mode 100644
index 00000000000..3a92e0983b3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/MainRating.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/MaintainabilityBox.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/MaintainabilityBox.tsx
new file mode 100644
index 00000000000..e6c3b50b05a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/MaintainabilityBox.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/MeasuresButtonLink.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/MeasuresButtonLink.tsx
new file mode 100644
index 00000000000..a4fc94fcf77
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/MeasuresButtonLink.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/RatingFreshness.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/RatingFreshness.tsx
new file mode 100644
index 00000000000..cd9d28c99e3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/RatingFreshness.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx
new file mode 100644
index 00000000000..e71c0aed894
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/ReliabilityBox.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/ReliabilityBox.tsx
new file mode 100644
index 00000000000..12116d9d5cc
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/ReliabilityBox.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Report.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Report.tsx
new file mode 100644
index 00000000000..596afcba973
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/Report.tsx
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/SecurityBox.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/SecurityBox.tsx
new file mode 100644
index 00000000000..da9076240b4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/SecurityBox.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx
new file mode 100644
index 00000000000..ab0b1d7cf73
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx
@@ -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>;
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/SubscriptionContainer.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/SubscriptionContainer.tsx
new file mode 100644
index 00000000000..4f5bd33034c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/SubscriptionContainer.tsx
@@ -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);
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx
new file mode 100644
index 00000000000..ef13cf58cb8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx
new file mode 100644
index 00000000000..421eea14c96
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx
new file mode 100644
index 00000000000..1959dc4daaf
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx
@@ -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']);
+});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx
new file mode 100644
index 00000000000..fafff182895
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx
@@ -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 }
+ );
+});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Effort-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Effort-test.tsx
new file mode 100644
index 00000000000..1a0189c5815
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Effort-test.tsx
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/HistoryButtonLink-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/HistoryButtonLink-test.tsx
new file mode 100644
index 00000000000..a2f5cb5c8ba
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/HistoryButtonLink-test.tsx
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MainRating-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MainRating-test.tsx
new file mode 100644
index 00000000000..9ff9f372bb1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MainRating-test.tsx
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MaintainabilityBox-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MaintainabilityBox-test.tsx
new file mode 100644
index 00000000000..11e5b0ff065
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MaintainabilityBox-test.tsx
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MeasuresButtonLink-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MeasuresButtonLink-test.tsx
new file mode 100644
index 00000000000..6d8a7a59c0a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/MeasuresButtonLink-test.tsx
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/RatingFreshness-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/RatingFreshness-test.tsx
new file mode 100644
index 00000000000..5490ed7ff29
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/RatingFreshness-test.tsx
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReleasabilityBox-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReleasabilityBox-test.tsx
new file mode 100644
index 00000000000..90c900a2d1e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReleasabilityBox-test.tsx
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReliabilityBox-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReliabilityBox-test.tsx
new file mode 100644
index 00000000000..f2b65993251
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/ReliabilityBox-test.tsx
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Report-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Report-test.tsx
new file mode 100644
index 00000000000..1a21649ae06
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Report-test.tsx
@@ -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');
+});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/SecurityBox-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/SecurityBox-test.tsx
new file mode 100644
index 00000000000..b658ce56b8d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/SecurityBox-test.tsx
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Subscription-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Subscription-test.tsx
new file mode 100644
index 00000000000..4fa146bc940
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Subscription-test.tsx
@@ -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');
+});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Summary-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Summary-test.tsx
new file mode 100644
index 00000000000..0dadfb63e2c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Summary-test.tsx
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/WorstProjects-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/WorstProjects-test.tsx
new file mode 100644
index 00000000000..e4eed115067
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/WorstProjects-test.tsx
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap
new file mode 100644
index 00000000000..1df37032691
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap
new file mode 100644
index 00000000000..4a1077741b9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap
new file mode 100644
index 00000000000..79fce89982d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/HistoryButtonLink-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/HistoryButtonLink-test.tsx.snap
new file mode 100644
index 00000000000..d64b7c80c4e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/HistoryButtonLink-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MainRating-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MainRating-test.tsx.snap
new file mode 100644
index 00000000000..d8cc0a6fd99
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MainRating-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MaintainabilityBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MaintainabilityBox-test.tsx.snap
new file mode 100644
index 00000000000..874b02296e2
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MaintainabilityBox-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MeasuresButtonLink-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MeasuresButtonLink-test.tsx.snap
new file mode 100644
index 00000000000..5b7f1c4bb23
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MeasuresButtonLink-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/RatingFreshness-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/RatingFreshness-test.tsx.snap
new file mode 100644
index 00000000000..ca9124758dc
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/RatingFreshness-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap
new file mode 100644
index 00000000000..3db58b7d539
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReliabilityBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReliabilityBox-test.tsx.snap
new file mode 100644
index 00000000000..e5b35707fa0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReliabilityBox-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Report-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Report-test.tsx.snap
new file mode 100644
index 00000000000..76b6a4056ab
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Report-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/SecurityBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/SecurityBox-test.tsx.snap
new file mode 100644
index 00000000000..b2966238d59
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/SecurityBox-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Subscription-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Subscription-test.tsx.snap
new file mode 100644
index 00000000000..03ba0e5f118
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Subscription-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap
new file mode 100644
index 00000000000..8f934778b8d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap
new file mode 100644
index 00000000000..1eaa2c0bdb9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/routes.ts b/server/sonar-web/src/main/js/apps/portfolio/routes.ts
new file mode 100644
index 00000000000..520805ebac5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/routes.ts
@@ -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;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/styles.css b/server/sonar-web/src/main/js/apps/portfolio/styles.css
new file mode 100644
index 00000000000..6214d02e657
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/styles.css
@@ -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;
+}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/types.ts b/server/sonar-web/src/main/js/apps/portfolio/types.ts
new file mode 100644
index 00000000000..b9cdc0ba7fe
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/types.ts
@@ -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;
+}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/utils.ts b/server/sonar-web/src/main/js/apps/portfolio/utils.ts
new file mode 100644
index 00000000000..37451bf9e05
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/utils.ts
@@ -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;
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js
index d60213ed43e..960b925b21e 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js
@@ -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);
});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphHistory.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphHistory.js
index 11cbe73667d..29fe837acfa 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphHistory.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphHistory.js
@@ -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';
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js
index 2b9d710508a..9b861ce2420 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js
@@ -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,
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsZoom.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsZoom.js
index 0160d85a65b..d9b69a1ccf7 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsZoom.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsZoom.js
@@ -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>,
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js
index 8b6b60adb9c..efc78dbe000 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js
@@ -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}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
index e0b70d68e38..cceffeabb0f 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
@@ -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}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
index acec2a25fa1..d201c600c01 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
@@ -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,
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js
index 51f4df10221..cd1cfd900eb 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js
@@ -40,7 +40,7 @@ import {
/*::
type Props = {
analyses: Array<Analysis>,
- leakPeriodDate: Date,
+ leakPeriodDate?: Date,
loading: boolean,
measuresHistory: Array<MeasureHistory>,
metrics: Array<Metric>,
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js
index 57183279074..f4f9a9a7874 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js
@@ -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}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeakMeasures.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeakMeasures.tsx
index 570ddbe075f..47652952ec2 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeakMeasures.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeakMeasures.tsx
@@ -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']
}}
/>
diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx
index 29b76358034..d0ef2f047ce 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx
@@ -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']
}}
/>
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeakMeasures-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeakMeasures-test.tsx.snap
index 1c98e6df82e..9d82c53b036 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeakMeasures-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeakMeasures-test.tsx.snap
@@ -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",
},
}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverallMeasures-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverallMeasures-test.tsx.snap
index d1c011628f1..341a634b69a 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverallMeasures-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverallMeasures-test.tsx.snap
@@ -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",
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js b/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js
index e9db36ae7b7..e2ab2a305cd 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js
@@ -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);
+ },
+ () => {}
+ );
});
},
diff --git a/server/sonar-web/src/main/js/components/charts/LanguageDistribution.js b/server/sonar-web/src/main/js/components/charts/LanguageDistribution.js
deleted file mode 100644
index 60901a97d98..00000000000
--- a/server/sonar-web/src/main/js/components/charts/LanguageDistribution.js
+++ /dev/null
@@ -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]}
- />
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/components/charts/LanguageDistribution.tsx b/server/sonar-web/src/main/js/components/charts/LanguageDistribution.tsx
new file mode 100644
index 00000000000..6766372207f
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/charts/LanguageDistribution.tsx
@@ -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;
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/components/extensions/PortfolioDashboard.tsx b/server/sonar-web/src/main/js/components/charts/LanguageDistributionContainer.tsx
index 2bb640dae73..1290b077484 100644
--- a/server/sonar-web/src/main/js/app/components/extensions/PortfolioDashboard.tsx
+++ b/server/sonar-web/src/main/js/components/charts/LanguageDistributionContainer.tsx
@@ -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);
diff --git a/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js b/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js
index ec1957e2f5b..e7cbd89f6e8 100644
--- a/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js
+++ b/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js
@@ -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})`}>
diff --git a/server/sonar-web/src/main/js/components/icons-components/BubblesIcon.js b/server/sonar-web/src/main/js/components/icons-components/BubblesIcon.tsx
index 5c0033d036d..2372a606683 100644
--- a/server/sonar-web/src/main/js/components/icons-components/BubblesIcon.js
+++ b/server/sonar-web/src/main/js/components/icons-components/BubblesIcon.tsx
@@ -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"
diff --git a/server/sonar-web/src/main/js/components/icons-components/HistoryIcon.js b/server/sonar-web/src/main/js/components/icons-components/HistoryIcon.tsx
index fa0afc2a5de..aac6843a977 100644
--- a/server/sonar-web/src/main/js/components/icons-components/HistoryIcon.js
+++ b/server/sonar-web/src/main/js/components/icons-components/HistoryIcon.tsx
@@ -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}
diff --git a/server/sonar-web/src/main/js/components/icons-components/LinkIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/LinkIcon.tsx
index 80cc7765a52..cbbb2606f69 100644
--- a/server/sonar-web/src/main/js/components/icons-components/LinkIcon.tsx
+++ b/server/sonar-web/src/main/js/components/icons-components/LinkIcon.tsx
@@ -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"
diff --git a/server/sonar-web/src/main/js/components/measure/Measure.tsx b/server/sonar-web/src/main/js/components/measure/Measure.tsx
index d345aff6cb5..e52c12a7512 100644
--- a/server/sonar-web/src/main/js/components/measure/Measure.tsx
+++ b/server/sonar-web/src/main/js/components/measure/Measure.tsx
@@ -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>;
}
diff --git a/server/sonar-web/src/main/js/components/measure/utils.ts b/server/sonar-web/src/main/js/components/measure/utils.ts
index 017e709491d..36494141589 100644
--- a/server/sonar-web/src/main/js/components/measure/utils.ts
+++ b/server/sonar-web/src/main/js/components/measure/utils.ts
@@ -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);
}
}
diff --git a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js
index 69a7f843797..c7544325517 100644
--- a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js
+++ b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js
@@ -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 (
diff --git a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.js
index c1a898a3bf1..f5e122ac2a1 100644
--- a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js
+++ b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.js
@@ -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 = {
diff --git a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltipsContent.js b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltipsContent.js
index 31f38957d36..ccbce540888 100644
--- a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltipsContent.js
+++ b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltipsContent.js
@@ -19,7 +19,7 @@
*/
// @flow
import React from 'react';
-import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
+import ChartLegendIcon from '../icons-components/ChartLegendIcon';
/*::
type Props = {
diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltips-test.js b/server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltips-test.js
index 1b8d08d2ef1..4cf95aee328 100644
--- a/server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltips-test.js
+++ b/server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltips-test.js
@@ -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 = [
{
diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltipsContent-test.js b/server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltipsContent-test.js
index 5d01a353afe..5d01a353afe 100644
--- a/server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltipsContent-test.js
+++ b/server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltipsContent-test.js
diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap b/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap
index 64d9d39a3e4..64d9d39a3e4 100644
--- a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap
+++ b/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap
diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.js.snap b/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.js.snap
index 4019b80bbe1..4019b80bbe1 100644
--- a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.js.snap
+++ b/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.js.snap
diff --git a/server/sonar-web/src/main/js/helpers/testUtils.ts b/server/sonar-web/src/main/js/helpers/testUtils.ts
index 507fbc44ffb..01b046039dc 100644
--- a/server/sonar-web/src/main/js/helpers/testUtils.ts
+++ b/server/sonar-web/src/main/js/helpers/testUtils.ts
@@ -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 });
}
diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts
index 378d89bb0f9..8deba26fcad 100644
--- a/server/sonar-web/src/main/js/helpers/urls.ts
+++ b/server/sonar-web/src/main/js/helpers/urls.ts
@@ -84,17 +84,20 @@ export function getComponentDrilldownUrl(componentKey: string, metric: string, b
return { pathname: '/component_measures', query: { id: componentKey, metric, branch } };
}
+export function getMeasureTreemapUrl(component: string, metric: string, branch?: string) {
+ return {
+ pathname: '/component_measures',
+ query: { id: component, metric, branch, view: 'treemap' }
+ };
+}
+
/**
* Generate URL for a component's measure history
*/
-export function getComponentMeasureHistory(
- componentKey: string,
- metric: string,
- branch?: string
-): Location {
+export function getMeasureHistoryUrl(component: string, metric: string, branch?: string) {
return {
pathname: '/project/activity',
- query: { id: componentKey, graph: 'custom', custom_metrics: metric, branch }
+ query: { id: component, graph: 'custom', custom_metrics: metric, branch }
};
}