aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src')
-rw-r--r--server/sonar-web/src/main/js/api/application.js31
-rw-r--r--server/sonar-web/src/main/js/api/quality-gates.js4
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js14
-rw-r--r--server/sonar-web/src/main/js/app/components/search/SearchResult.js5
-rw-r--r--server/sonar-web/src/main/js/app/components/search/utils.js2
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/App.js14
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Component.js8
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.js8
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Search.js4
-rw-r--r--server/sonar-web/src/main/js/apps/code/utils.js30
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js12
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js2
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/home/Home.js2
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/home/actions.js27
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js2
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/App.js25
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.js93
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js11
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.js12
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/__tests__/ApplicationLeakPeriodLegend-test.js35
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/ApplicationLeakPeriodLegend-test.js.snap54
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js7
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main/enhance.js5
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/Meta.js12
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js71
-rw-r--r--server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.js108
-rw-r--r--server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.css15
-rw-r--r--server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.js103
-rw-r--r--server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.js39
-rw-r--r--server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.js73
-rw-r--r--server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.js.snap62
-rw-r--r--server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap84
-rw-r--r--server/sonar-web/src/main/js/apps/overview/styles.css5
-rw-r--r--server/sonar-web/src/main/js/apps/overview/utils.js1
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js3
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/App.js30
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js2
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/constants.js1
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/deletion/Header.js7
-rw-r--r--server/sonar-web/src/main/js/apps/projects-admin/constants.js2
41 files changed, 912 insertions, 115 deletions
diff --git a/server/sonar-web/src/main/js/api/application.js b/server/sonar-web/src/main/js/api/application.js
new file mode 100644
index 00000000000..bbc9016c1cb
--- /dev/null
+++ b/server/sonar-web/src/main/js/api/application.js
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+// @flow
+import { getJSON } from '../helpers/request';
+import throwGlobalError from '../app/utils/throwGlobalError';
+
+type GetApplicationLeakResponse = Array<{
+ date: string,
+ project: string,
+ projectName: string
+}>;
+
+export const getApplicationLeak = (application: string): Promise<GetApplicationLeakResponse> =>
+ getJSON('/api/views/show_leak', { application }).then(r => r.leaks, throwGlobalError);
diff --git a/server/sonar-web/src/main/js/api/quality-gates.js b/server/sonar-web/src/main/js/api/quality-gates.js
index 7c60f3a2a24..878c2e32dca 100644
--- a/server/sonar-web/src/main/js/api/quality-gates.js
+++ b/server/sonar-web/src/main/js/api/quality-gates.js
@@ -105,3 +105,7 @@ export function dissociateGateWithProject(gateId, projectKey) {
const data = { gateId, projectKey };
return post(url, data);
}
+
+export function getApplicationQualityGate(application) {
+ return getJSON('/api/qualitygates/application_status', { application });
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js
index d9963b913ac..61738902ee9 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js
@@ -56,7 +56,7 @@ export default class ComponentNav extends React.PureComponent {
populateRecentHistory = () => {
const { breadcrumbs } = this.props.component;
const { qualifier } = breadcrumbs[breadcrumbs.length - 1];
- if (['TRK', 'VW', 'DEV'].indexOf(qualifier) !== -1) {
+ if (['TRK', 'VW', 'APP', 'DEV'].indexOf(qualifier) !== -1) {
RecentHistory.add(
this.props.component.key,
this.props.component.name,
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js
index e7f2ca6d463..5e3038d65f5 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js
@@ -57,6 +57,10 @@ export default class ComponentNavMenu extends React.PureComponent {
return qualifier === 'VW' || qualifier === 'SVW';
}
+ isApplication() {
+ return this.props.component.qualifier === 'APP';
+ }
+
renderDashboardLink() {
const pathname = this.isView() ? '/portfolio' : '/dashboard';
return (
@@ -78,14 +82,16 @@ export default class ComponentNavMenu extends React.PureComponent {
<Link
to={{ pathname: '/code', query: { id: this.props.component.key } }}
activeClassName="active">
- {this.isView() ? translate('view_projects.page') : translate('code.page')}
+ {this.isView() || this.isApplication()
+ ? translate('view_projects.page')
+ : translate('code.page')}
</Link>
</li>
);
}
renderActivityLink() {
- if (!this.isProject()) {
+ if (!this.isProject() && !this.isApplication()) {
return null;
}
@@ -167,7 +173,7 @@ export default class ComponentNavMenu extends React.PureComponent {
}
renderSettingsLink() {
- if (!this.props.conf.showSettings) {
+ if (!this.props.conf.showSettings || this.isApplication()) {
return null;
}
return (
@@ -293,7 +299,7 @@ export default class ComponentNavMenu extends React.PureComponent {
return null;
}
- if (qualifier !== 'TRK' && qualifier !== 'VW') {
+ if (qualifier !== 'TRK' && qualifier !== 'VW' && qualifier !== 'APP') {
return null;
}
diff --git a/server/sonar-web/src/main/js/app/components/search/SearchResult.js b/server/sonar-web/src/main/js/app/components/search/SearchResult.js
index 7f6337dcda0..0488535cba6 100644
--- a/server/sonar-web/src/main/js/app/components/search/SearchResult.js
+++ b/server/sonar-web/src/main/js/app/components/search/SearchResult.js
@@ -87,7 +87,10 @@ export default class SearchResult extends React.PureComponent {
return null;
}
- if (!['VW', 'SVW', 'TRK'].includes(component.qualifier) || component.organization == null) {
+ if (
+ !['VW', 'SVW', 'APP', 'TRK'].includes(component.qualifier) ||
+ component.organization == null
+ ) {
return null;
}
diff --git a/server/sonar-web/src/main/js/app/components/search/utils.js b/server/sonar-web/src/main/js/app/components/search/utils.js
index 5ed66863da2..ebeea69e645 100644
--- a/server/sonar-web/src/main/js/app/components/search/utils.js
+++ b/server/sonar-web/src/main/js/app/components/search/utils.js
@@ -20,7 +20,7 @@
// @flow
import { sortBy } from 'lodash';
-const ORDER = ['DEV', 'VW', 'SVW', 'TRK', 'BRC', 'FIL', 'UTS'];
+const ORDER = ['DEV', 'VW', 'SVW', 'APP', 'TRK', 'BRC', 'FIL', 'UTS'];
export function sortQualifiers(qualifiers: Array<string>) {
return sortBy(qualifiers, qualifier => ORDER.indexOf(qualifier));
diff --git a/server/sonar-web/src/main/js/apps/code/components/App.js b/server/sonar-web/src/main/js/apps/code/components/App.js
index a85387a0e5e..3a0f994aa1c 100644
--- a/server/sonar-web/src/main/js/apps/code/components/App.js
+++ b/server/sonar-web/src/main/js/apps/code/components/App.js
@@ -74,8 +74,8 @@ class App extends React.PureComponent {
addComponentBreadcrumbs(component.key, component.breadcrumbs);
this.setState({ loading: true });
- const isView = component.qualifier === 'VW' || component.qualifier === 'SVW';
- retrieveComponentChildren(component.key, isView)
+ const isPortfolio = ['VW', 'SVW'].includes(component.qualifier);
+ retrieveComponentChildren(component.key, isPortfolio)
.then(r => {
addComponent(r.baseComponent);
this.handleUpdate();
@@ -91,9 +91,8 @@ class App extends React.PureComponent {
loadComponent(componentKey) {
this.setState({ loading: true });
- const isView =
- this.props.component.qualifier === 'VW' || this.props.component.qualifier === 'SVW';
- retrieveComponent(componentKey, isView)
+ const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier);
+ retrieveComponent(componentKey, isPortfolio)
.then(r => {
if (this.mounted) {
if (['FIL', 'UTS'].includes(r.component.qualifier)) {
@@ -135,9 +134,8 @@ class App extends React.PureComponent {
handleLoadMore() {
const { baseComponent, page } = this.state;
- const isView =
- this.props.component.qualifier === 'VW' || this.props.component.qualifier === 'SVW';
- loadMoreChildren(baseComponent.key, page + 1, isView)
+ const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier);
+ loadMoreChildren(baseComponent.key, page + 1, isPortfolio)
.then(r => {
if (this.mounted) {
this.setState({
diff --git a/server/sonar-web/src/main/js/apps/code/components/Component.js b/server/sonar-web/src/main/js/apps/code/components/Component.js
index 6770e4c28a6..e36ad13bcf6 100644
--- a/server/sonar-web/src/main/js/apps/code/components/Component.js
+++ b/server/sonar-web/src/main/js/apps/code/components/Component.js
@@ -61,7 +61,8 @@ export default class Component extends React.PureComponent {
render() {
const { component, rootComponent, selected, previous, canBrowse } = this.props;
- const isView = ['VW', 'SVW'].includes(rootComponent.qualifier);
+ const isPortfolio = ['VW', 'SVW'].includes(rootComponent.qualifier);
+ const isApplication = rootComponent.qualifier === 'APP';
let componentAction = null;
@@ -76,7 +77,7 @@ export default class Component extends React.PureComponent {
}
}
- const columns = isView
+ const columns = isPortfolio
? [
{ metric: 'releasability_rating', type: 'RATING' },
{ metric: 'reliability_rating', type: 'RATING' },
@@ -85,13 +86,14 @@ export default class Component extends React.PureComponent {
{ metric: 'ncloc', type: 'SHORT_INT' }
]
: [
+ isApplication && { metric: 'alert_status', type: 'LEVEL' },
{ metric: 'ncloc', type: 'SHORT_INT' },
{ metric: 'bugs', type: 'SHORT_INT' },
{ metric: 'vulnerabilities', type: 'SHORT_INT' },
{ metric: 'code_smells', type: 'SHORT_INT' },
{ metric: 'coverage', type: 'PERCENT' },
{ metric: 'duplicated_lines_density', type: 'PERCENT' }
- ];
+ ].filter(Boolean);
return (
<tr className={classNames({ selected })}>
diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.js b/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.js
index 1552dfad89d..a2977ae960b 100644
--- a/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.js
+++ b/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.js
@@ -21,9 +21,10 @@ import React from 'react';
import { translate } from '../../../helpers/l10n';
const ComponentsHeader = ({ baseComponent, rootComponent }) => {
- const isView = rootComponent.qualifier === 'VW' || rootComponent.qualifier === 'SVW';
+ const isPortfolio = rootComponent.qualifier === 'VW' || rootComponent.qualifier === 'SVW';
+ const isApplication = rootComponent.qualifier === 'APP';
- const columns = isView
+ const columns = isPortfolio
? [
translate('metric_domain.Releasability'),
translate('metric_domain.Reliability'),
@@ -32,13 +33,14 @@ const ComponentsHeader = ({ baseComponent, rootComponent }) => {
translate('metric', 'ncloc', 'name')
]
: [
+ isApplication && translate('metric.alert_status.name'),
translate('metric', 'ncloc', 'name'),
translate('metric', 'bugs', 'name'),
translate('metric', 'vulnerabilities', 'name'),
translate('metric', 'code_smells', 'name'),
translate('metric', 'coverage', 'name'),
translate('metric', 'duplicated_lines_density', 'short_name')
- ];
+ ].filter(Boolean);
return (
<thead>
diff --git a/server/sonar-web/src/main/js/apps/code/components/Search.js b/server/sonar-web/src/main/js/apps/code/components/Search.js
index 12b644d943c..a34d87e3c9e 100644
--- a/server/sonar-web/src/main/js/apps/code/components/Search.js
+++ b/server/sonar-web/src/main/js/apps/code/components/Search.js
@@ -132,8 +132,8 @@ export default class Search extends React.PureComponent {
const { component, onError } = this.props;
this.setState({ loading: true });
- const isView = component.qualifier === 'VW' || component.qualifier === 'SVW';
- const qualifiers = isView ? 'SVW,TRK' : 'BRC,UTS,FIL';
+ const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier);
+ const qualifiers = isPortfolio ? 'SVW,TRK' : 'BRC,UTS,FIL';
getTree(component.key, { q: query, s: 'qualifier,name', qualifiers })
.then(r => {
diff --git a/server/sonar-web/src/main/js/apps/code/utils.js b/server/sonar-web/src/main/js/apps/code/utils.js
index 44d539a2122..2a1f5552cc0 100644
--- a/server/sonar-web/src/main/js/apps/code/utils.js
+++ b/server/sonar-web/src/main/js/apps/code/utils.js
@@ -39,7 +39,7 @@ const METRICS = [
'alert_status'
];
-const VIEW_METRICS = [
+const PORTFOLIO_METRICS = [
'releasability_rating',
'alert_status',
'reliability_rating',
@@ -111,22 +111,22 @@ function storeChildrenBreadcrumbs(parentComponentKey, children) {
}
}
-function getMetrics(isView) {
- return isView ? VIEW_METRICS : METRICS;
+function getMetrics(isPortfolio) {
+ return isPortfolio ? PORTFOLIO_METRICS : METRICS;
}
/**
* @param {string} componentKey
- * @param {boolean} isView
+ * @param {boolean} isPortfolio
* @returns {Promise}
*/
-function retrieveComponentBase(componentKey, isView) {
+function retrieveComponentBase(componentKey, isPortfolio) {
const existing = getComponentFromBucket(componentKey);
if (existing) {
return Promise.resolve(existing);
}
- const metrics = getMetrics(isView);
+ const metrics = getMetrics(isPortfolio);
return getComponent(componentKey, metrics).then(component => {
addComponent(component);
@@ -136,10 +136,10 @@ function retrieveComponentBase(componentKey, isView) {
/**
* @param {string} componentKey
- * @param {boolean} isView
+ * @param {boolean} isPortfolio
* @returns {Promise}
*/
-export function retrieveComponentChildren(componentKey, isView) {
+export function retrieveComponentChildren(componentKey, isPortfolio) {
const existing = getComponentChildren(componentKey);
if (existing) {
return Promise.resolve({
@@ -149,7 +149,7 @@ export function retrieveComponentChildren(componentKey, isView) {
});
}
- const metrics = getMetrics(isView);
+ const metrics = getMetrics(isPortfolio);
return getChildren(componentKey, metrics, { ps: PAGE_SIZE, s: 'qualifier,name' })
.then(prepareChildren)
@@ -176,13 +176,13 @@ function retrieveComponentBreadcrumbs(componentKey) {
/**
* @param {string} componentKey
- * @param {boolean} isView
+ * @param {boolean} isPortfolio
* @returns {Promise}
*/
-export function retrieveComponent(componentKey, isView) {
+export function retrieveComponent(componentKey, isPortfolio) {
return Promise.all([
- retrieveComponentBase(componentKey, isView),
- retrieveComponentChildren(componentKey, isView),
+ retrieveComponentBase(componentKey, isPortfolio),
+ retrieveComponentChildren(componentKey, isPortfolio),
retrieveComponentBreadcrumbs(componentKey)
]).then(r => {
return {
@@ -195,8 +195,8 @@ export function retrieveComponent(componentKey, isView) {
});
}
-export function loadMoreChildren(componentKey, page, isView) {
- const metrics = getMetrics(isView);
+export function loadMoreChildren(componentKey, page, isPortfolio) {
+ const metrics = getMetrics(isPortfolio);
return getChildren(componentKey, metrics, { ps: PAGE_SIZE, p: page })
.then(prepareChildren)
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js b/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js
index 302c02959bf..22483f15b54 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js
@@ -21,9 +21,17 @@ import React from 'react';
import moment from 'moment';
import Tooltip from '../../../components/controls/Tooltip';
import { getPeriodLabel, getPeriodDate } from '../../../helpers/periods';
-import { translateWithParameters } from '../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+export default function LeakPeriodLegend({ component, period }) {
+ if (component.qualifier === 'APP') {
+ return (
+ <div className="measures-domains-leak-header">
+ {translate('issues.leak_period')}
+ </div>
+ );
+ }
-export default function LeakPeriodLegend({ period }) {
const label = (
<div className="measures-domains-leak-header">
{translateWithParameters('overview.leak_period_x', getPeriodLabel(period))}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js
index 392b7e19587..f2281c24eba 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js
@@ -56,7 +56,7 @@ export default function MeasureDetailsHeader({
{isDiff &&
<div className="pull-right">
- <LeakPeriodLegend period={leakPeriod} />
+ <LeakPeriodLegend component={component} period={leakPeriod} />
</div>}
<TooltipsContainer options={{ html: false }}>
diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/Home.js b/server/sonar-web/src/main/js/apps/component-measures/home/Home.js
index fa3ddd96662..b81be299a02 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/home/Home.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/home/Home.js
@@ -70,7 +70,7 @@ export default class Home extends React.PureComponent {
</ul>
</nav>
- {leakPeriod != null && <LeakPeriodLegend period={leakPeriod} />}
+ {leakPeriod != null && <LeakPeriodLegend component={component} period={leakPeriod} />}
</header>
<main id="component-measures-home-main">
diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/actions.js b/server/sonar-web/src/main/js/apps/component-measures/home/actions.js
index ed8e64d79a6..6ae4585b20b 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/home/actions.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/home/actions.js
@@ -19,7 +19,6 @@
*/
import { startFetching, stopFetching } from '../store/statusActions';
import { getMeasuresAndMeta } from '../../../api/measures';
-import { getLeakPeriod } from '../../../helpers/periods';
import { getLeakValue } from '../utils';
import { getMeasuresAppComponent, getMeasuresAppAllMetrics } from '../../../store/rootReducer';
@@ -30,10 +29,20 @@ export function receiveMeasures(measures, periods) {
}
function banQualityGate(component, measures) {
- if (['VW', 'SVW'].includes(component.qualifier)) {
- return measures;
+ let newMeasures = [...measures];
+
+ if (!['VW', 'SVW', 'APP'].includes(component.qualifier)) {
+ newMeasures = newMeasures.filter(measure => measure.metric !== 'alert_status');
+ }
+
+ if (component.qualifier === 'APP') {
+ newMeasures = newMeasures.filter(
+ measure =>
+ measure.metric !== 'releasability_rating' && measure.metric !== 'releasability_effort'
+ );
}
- return measures.filter(measure => measure.metric !== 'alert_status');
+
+ return newMeasures;
}
export function fetchMeasures() {
@@ -50,7 +59,6 @@ export function fetchMeasures() {
.map(metric => metric.key);
getMeasuresAndMeta(component.key, metricKeys, { additionalFields: 'periods' }).then(r => {
- const leakPeriod = getLeakPeriod(r.periods);
const measures = banQualityGate(component, r.component.measures)
.map(measure => {
const metric = metrics.find(metric => metric.key === measure.metric);
@@ -59,11 +67,16 @@ export function fetchMeasures() {
})
.filter(measure => {
const hasValue = measure.value != null;
- const hasLeakValue = !!leakPeriod && measure.leak != null;
+ const hasLeakValue = measure.leak != null;
return hasValue || hasLeakValue;
});
- dispatch(receiveMeasures(measures, r.periods));
+ const newBugs = measures.find(measure => measure.metric.key === 'new_bugs');
+
+ const applicationPeriods = newBugs ? [{ index: 1 }] : [];
+ const periods = component.qualifier === 'APP' ? applicationPeriods : r.periods;
+
+ dispatch(receiveMeasures(measures, periods));
dispatch(stopFetching());
});
};
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js
index 8d4c9269e1e..62a1af46084 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js
@@ -70,7 +70,7 @@ export default class ProjectFacet extends React.PureComponent {
handleSearch = (query: string) => {
const { component, organization } = this.props;
- if (component != null && ['VW', 'SVW'].includes(component.qualifier)) {
+ if (component != null && ['VW', 'SVW', 'APP'].includes(component.qualifier)) {
return getTree(component.key, { ps: 50, q: query, qualifiers: 'TRK' }).then(response =>
response.components.map(component => ({
label: component.name,
diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.js b/server/sonar-web/src/main/js/apps/overview/components/App.js
index 247a4d81704..e704e35cd8d 100644
--- a/server/sonar-web/src/main/js/apps/overview/components/App.js
+++ b/server/sonar-web/src/main/js/apps/overview/components/App.js
@@ -19,7 +19,6 @@
*/
// @flow
import React from 'react';
-import { withRouter } from 'react-router';
import OverviewApp from './OverviewApp';
import EmptyOverview from './EmptyOverview';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
@@ -35,20 +34,32 @@ type Props = {
router: Object
};
-class App extends React.PureComponent {
+export default class App extends React.PureComponent {
props: Props;
state: Object;
+ static contextTypes = {
+ router: React.PropTypes.object
+ };
+
componentDidMount() {
- if (['VW', 'SVW'].includes(this.props.component.qualifier)) {
- this.props.router.replace({
+ if (this.isPortfolio()) {
+ this.context.router.replace({
pathname: '/portfolio',
query: { id: this.props.component.key }
});
}
}
+ isPortfolio() {
+ return this.props.component.qualifier === 'VW' || this.props.component.qualifier === 'SVW';
+ }
+
render() {
+ if (this.isPortfolio()) {
+ return null;
+ }
+
const { component } = this.props;
if (['FIL', 'UTS'].includes(component.qualifier)) {
@@ -63,10 +74,6 @@ class App extends React.PureComponent {
return <EmptyOverview component={component} />;
}
- return <OverviewApp {...this.props} leakPeriodIndex="1" />;
+ return <OverviewApp component={component} />;
}
}
-
-export default withRouter(App);
-
-export const UnconnectedApp = App;
diff --git a/server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.js b/server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.js
new file mode 100644
index 00000000000..3b43a02e39f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.js
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+// @flow
+import React from 'react';
+import Tooltip from '../../../components/controls/Tooltip';
+import FormattedDate from '../../../components/ui/FormattedDate';
+import { getApplicationLeak } from '../../../api/application';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {
+ component: { key: string }
+};
+
+type State = {
+ leaks: ?Array<{ date: string, project: string, projectName: string }>
+};
+
+export default class ApplicationLeakPeriodLegend extends React.Component {
+ mounted: boolean;
+ props: Props;
+ state: State = {
+ leaks: null
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ if (nextProps.component.key !== this.props.component.key) {
+ this.setState({ leaks: null });
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchLeaks = (visible: boolean) => {
+ if (visible && this.state.leaks == null) {
+ getApplicationLeak(this.props.component.key).then(
+ leaks => {
+ if (this.mounted) {
+ this.setState({ leaks });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ leaks: [] });
+ }
+ }
+ );
+ }
+ };
+
+ renderOverlay = () =>
+ this.state.leaks != null
+ ? <ul className="text-left">
+ {this.state.leaks.map(leak =>
+ <li key={leak.project}>
+ {leak.projectName}: <FormattedDate date={leak.date} format="LL" />
+ </li>
+ )}
+ </ul>
+ : <i className="spinner spinner-margin" />;
+
+ render() {
+ return (
+ <Tooltip onVisibleChange={this.fetchLeaks} overlay={this.renderOverlay()}>
+ <div className="overview-legend overview-legend-spaced-line">
+ {translate('issues.leak_period')}
+ </div>
+ </Tooltip>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js
index 79ab68e793c..aea20cf3d40 100644
--- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js
+++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js
@@ -22,6 +22,7 @@ import React from 'react';
import { uniq } from 'lodash';
import moment from 'moment';
import QualityGate from '../qualityGate/QualityGate';
+import ApplicationQualityGate from '../qualityGate/ApplicationQualityGate';
import BugsAndVulnerabilities from '../main/BugsAndVulnerabilities';
import CodeSmells from '../main/CodeSmells';
import Coverage from '../main/Coverage';
@@ -122,6 +123,9 @@ export default class OverviewApp extends React.PureComponent {
}, throwGlobalError);
}
+ getApplicationLeakPeriod = () =>
+ this.state.measures.find(measure => measure.metric.key === 'new_bugs') ? { index: 1 } : null;
+
renderLoading() {
return (
<div className="text-center">
@@ -138,14 +142,17 @@ export default class OverviewApp extends React.PureComponent {
return this.renderLoading();
}
- const leakPeriod = getLeakPeriod(periods);
+ const leakPeriod =
+ component.qualifier === 'APP' ? this.getApplicationLeakPeriod() : getLeakPeriod(periods);
const domainProps = { component, measures, leakPeriod, history, historyStartDate };
return (
<div className="page page-limited">
<div className="overview page-with-sidebar">
<div className="overview-main page-main">
- <QualityGate component={component} measures={measures} />
+ {component.qualifier === 'APP'
+ ? <ApplicationQualityGate component={component} />
+ : <QualityGate component={component} measures={measures} />}
<div className="overview-domains-list">
<BugsAndVulnerabilities {...domainProps} />
diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.js b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.js
index 70dd35ee95d..a39a89c9c99 100644
--- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.js
+++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.js
@@ -19,24 +19,18 @@
*/
import React from 'react';
import { shallow } from 'enzyme';
-import { UnconnectedApp } from '../App';
+import App from '../App';
import OverviewApp from '../OverviewApp';
import EmptyOverview from '../EmptyOverview';
it('should render OverviewApp', () => {
const component = { id: 'id', analysisDate: '2016-01-01' };
- const output = shallow(<UnconnectedApp component={component} />);
+ const output = shallow(<App component={component} />);
expect(output.type()).toBe(OverviewApp);
});
it('should render EmptyOverview', () => {
const component = { id: 'id' };
- const output = shallow(<UnconnectedApp component={component} />);
+ const output = shallow(<App component={component} />);
expect(output.type()).toBe(EmptyOverview);
});
-
-it('should pass leakPeriodIndex', () => {
- const component = { id: 'id', analysisDate: '2016-01-01' };
- const output = shallow(<UnconnectedApp component={component} />);
- expect(output.prop('leakPeriodIndex')).toBe('1');
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/ApplicationLeakPeriodLegend-test.js b/server/sonar-web/src/main/js/apps/overview/components/__tests__/ApplicationLeakPeriodLegend-test.js
new file mode 100644
index 00000000000..ce900ba1fb7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/ApplicationLeakPeriodLegend-test.js
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import ApplicationLeakPeriodLegend from '../ApplicationLeakPeriodLegend';
+
+it('renders', () => {
+ const wrapper = shallow(<ApplicationLeakPeriodLegend component={{ key: 'foo' }} />);
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({
+ leaks: [
+ { date: '2017-01-01T11:39:03+0100', project: 'foo', projectName: 'Foo' },
+ { date: '2017-02-01T11:39:03+0100', project: 'bar', projectName: 'Bar' }
+ ]
+ });
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/ApplicationLeakPeriodLegend-test.js.snap b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/ApplicationLeakPeriodLegend-test.js.snap
new file mode 100644
index 00000000000..217405a6565
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/ApplicationLeakPeriodLegend-test.js.snap
@@ -0,0 +1,54 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<Tooltip
+ onVisibleChange={[Function]}
+ overlay={
+ <i
+ className="spinner spinner-margin"
+ />
+ }
+ placement="bottom"
+>
+ <div
+ className="overview-legend overview-legend-spaced-line"
+ >
+ issues.leak_period
+ </div>
+</Tooltip>
+`;
+
+exports[`renders 2`] = `
+<Tooltip
+ onVisibleChange={[Function]}
+ overlay={
+ <ul
+ className="text-left"
+ >
+ <li>
+ Foo
+ :
+ <FormattedDate
+ date="2017-01-01T11:39:03+0100"
+ format="LL"
+ />
+ </li>
+ <li>
+ Bar
+ :
+ <FormattedDate
+ date="2017-02-01T11:39:03+0100"
+ format="LL"
+ />
+ </li>
+ </ul>
+ }
+ placement="bottom"
+>
+ <div
+ className="overview-legend overview-legend-spaced-line"
+ >
+ issues.leak_period
+ </div>
+</Tooltip>
+`;
diff --git a/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js b/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js
index f427b1a9a44..ec90cafe30f 100644
--- a/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js
+++ b/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js
@@ -21,6 +21,7 @@ import React from 'react';
import { Link } from 'react-router';
import enhance from './enhance';
import LeakPeriodLegend from '../components/LeakPeriodLegend';
+import ApplicationLeakPeriodLegend from '../components/ApplicationLeakPeriodLegend';
import { getMetricName } from '../helpers/metrics';
import { translate } from '../../../helpers/l10n';
import BugIcon from '../../../components/icons-components/BugIcon';
@@ -54,7 +55,7 @@ class BugsAndVulnerabilities extends React.PureComponent {
}
renderLeak() {
- const { leakPeriod } = this.props;
+ const { component, leakPeriod } = this.props;
if (leakPeriod == null) {
return null;
@@ -62,7 +63,9 @@ class BugsAndVulnerabilities extends React.PureComponent {
return (
<div className="overview-domain-leak">
- <LeakPeriodLegend period={leakPeriod} />
+ {component.qualifier === 'APP'
+ ? <ApplicationLeakPeriodLegend component={component} />
+ : <LeakPeriodLegend period={leakPeriod} />}
<div className="overview-domain-measures">
<div className="overview-domain-measure">
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 7c465b46c27..b5599d43c00 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
@@ -118,6 +118,7 @@ export default function enhance(ComposedComponent) {
</div>
);
};
+
renderRating = metricKey => {
const { component, measures } = this.props;
const measure = measures.find(measure => measure.metric.key === metricKey);
@@ -139,6 +140,7 @@ export default function enhance(ComposedComponent) {
</Tooltip>
);
};
+
renderIssues = (metric, type) => {
const { measures, component } = this.props;
const measure = measures.find(measure => measure.metric.key === metric);
@@ -160,6 +162,7 @@ export default function enhance(ComposedComponent) {
</Tooltip>
);
};
+
renderHistoryLink = metricKey => {
const linkClass =
'button button-small button-compact spacer-left overview-domain-measure-history-link';
@@ -171,6 +174,7 @@ export default function enhance(ComposedComponent) {
</Link>
);
};
+
renderTimeline = (metricKey, range, children) => {
if (!this.props.history) {
return null;
@@ -190,6 +194,7 @@ export default function enhance(ComposedComponent) {
</div>
);
};
+
render() {
return (
<ComposedComponent
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js
index 4d84a3f2e23..689db65fbed 100644
--- a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js
+++ b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js
@@ -34,15 +34,14 @@ const Meta = ({ component, history, measures, areThereCustomOrganizations, route
const { qualifier, description, qualityProfiles, qualityGate } = component;
const isProject = qualifier === 'TRK';
- const isView = qualifier === 'VW' || qualifier === 'SVW';
- const isDeveloper = qualifier === 'DEV';
+ const isApplication = qualifier === 'APP';
const hasDescription = !!description;
const hasQualityProfiles = Array.isArray(qualityProfiles) && qualityProfiles.length > 0;
const hasQualityGate = !!qualityGate;
- const shouldShowQualityProfiles = !isView && !isDeveloper && hasQualityProfiles;
- const shouldShowQualityGate = !isView && !isDeveloper && hasQualityGate;
+ const shouldShowQualityProfiles = isProject && hasQualityProfiles;
+ const shouldShowQualityGate = isProject && hasQualityGate;
const hasOrganization = component.organization != null && areThereCustomOrganizations;
return (
@@ -56,7 +55,8 @@ const Meta = ({ component, history, measures, areThereCustomOrganizations, route
{isProject && <MetaTags component={component} />}
- {isProject && <AnalysesList project={component.key} history={history} router={router} />}
+ {(isProject || isApplication) &&
+ <AnalysesList project={component.key} history={history} router={router} />}
{shouldShowQualityGate &&
<MetaQualityGate
@@ -71,7 +71,7 @@ const Meta = ({ component, history, measures, areThereCustomOrganizations, route
profiles={qualityProfiles}
/>}
- <MetaLinks component={component} />
+ {isProject && <MetaLinks component={component} />}
<MetaKey component={component} />
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 6c632128757..fe153ca52fd 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
@@ -19,11 +19,13 @@
*/
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 SizeRating from '../../../components/ui/SizeRating';
import { formatMeasure } from '../../../helpers/measures';
import { getMetricName } from '../helpers/metrics';
-import SizeRating from '../../../components/ui/SizeRating';
+import { translate } from '../../../helpers/l10n';
export default class MetaSize extends React.PureComponent {
static propTypes = {
@@ -31,32 +33,65 @@ export default class MetaSize extends React.PureComponent {
measures: PropTypes.array.isRequired
};
- render() {
- const ncloc = this.props.measures.find(measure => measure.metric.key === 'ncloc');
+ renderLoC = ncloc =>
+ <div
+ id="overview-ncloc"
+ className={classNames('overview-meta-size-ncloc', {
+ 'is-half-width': this.props.component.qualifier === 'APP'
+ })}>
+ <span className="spacer-right">
+ <SizeRating value={ncloc.value} />
+ </span>
+ <DrilldownLink component={this.props.component.key} metric="ncloc">
+ {formatMeasure(ncloc.value, 'SHORT_INT')}
+ </DrilldownLink>
+ <div className="overview-domain-measure-label text-muted">
+ {getMetricName('ncloc')}
+ </div>
+ </div>;
+
+ renderLoCDistribution = () => {
const languageDistribution = this.props.measures.find(
measure => measure.metric.key === 'ncloc_language_distribution'
);
- if (ncloc == null || languageDistribution == null) {
- return null;
- }
+ return languageDistribution
+ ? <div id="overview-language-distribution" className="overview-meta-size-lang-dist">
+ <LanguageDistribution distribution={languageDistribution.value} />
+ </div>
+ : null;
+ };
- return (
- <div id="overview-size" className="overview-meta-card">
- <div id="overview-ncloc" className="overview-meta-size-ncloc">
- <span className="spacer-right">
- <SizeRating value={ncloc.value} />
- </span>
- <DrilldownLink component={this.props.component.key} metric="ncloc">
- {formatMeasure(ncloc.value, 'SHORT_INT')}
+ renderProjects = () => {
+ const projects = this.props.measures.find(measure => measure.metric.key === 'projects');
+
+ return projects
+ ? <div
+ id="overview-projects"
+ className="overview-meta-size-ncloc is-half-width bordered-left">
+ <DrilldownLink component={this.props.component.key} metric="projects">
+ {formatMeasure(projects.value, 'SHORT_INT')}
</DrilldownLink>
<div className="overview-domain-measure-label text-muted">
- {getMetricName('ncloc')}
+ {translate('metric.projects.name')}
</div>
</div>
- <div id="overview-language-distribution" className="overview-meta-size-lang-dist">
- <LanguageDistribution distribution={languageDistribution.value} />
- </div>
+ : null;
+ };
+
+ render() {
+ const ncloc = this.props.measures.find(measure => measure.metric.key === 'ncloc');
+
+ if (ncloc == null) {
+ return null;
+ }
+
+ return (
+ <div id="overview-size" className="overview-meta-card">
+ {this.renderLoC(ncloc)}
+ {this.props.component.qualifier === 'APP'
+ ? this.renderProjects()
+ : this.renderLoCDistribution()}
</div>
);
}
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.js
new file mode 100644
index 00000000000..80229f4cc22
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.js
@@ -0,0 +1,108 @@
+/*
+ * 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.
+ */
+// @flow
+import React from 'react';
+import { keyBy } from 'lodash';
+import ApplicationQualityGateProject from './ApplicationQualityGateProject';
+import Level from '../../../components/ui/Level';
+import { getApplicationQualityGate } from '../../../api/quality-gates';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {
+ component: { key: string }
+};
+
+type State = {
+ loading: boolean,
+ metrics?: { [string]: Object },
+ projects?: Array<{
+ conditions: Array<Object>,
+ key: string,
+ name: string,
+ status: string
+ }>,
+ status?: string
+};
+
+export default class ApplicationQualityGate extends React.PureComponent {
+ mounted: boolean;
+ props: Props;
+ state: State = {
+ loading: true
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchDetails();
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.component.key !== this.props.component.key) {
+ this.fetchDetails();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchDetails = () => {
+ this.setState({ loading: true });
+ getApplicationQualityGate(this.props.component.key).then(({ status, projects, metrics }) => {
+ if (this.mounted) {
+ this.setState({
+ loading: false,
+ metrics: keyBy(metrics, 'key'),
+ status,
+ projects
+ });
+ }
+ });
+ };
+
+ render() {
+ const { metrics, status, projects } = this.state;
+
+ return (
+ <div className="overview-quality-gate" id="overview-quality-gate">
+ <h2 className="overview-title">
+ {translate('overview.quality_gate')}
+ {this.state.loading && <i className="spinner spacer-left" />}
+ {status != null && <Level level={status} />}
+ </h2>
+
+ {projects != null &&
+ <div
+ id="overview-quality-gate-conditions-list"
+ className="overview-quality-gate-conditions-list clearfix">
+ {projects
+ .filter(project => project.status !== 'OK')
+ .map(project =>
+ <ApplicationQualityGateProject
+ key={project.key}
+ metrics={metrics}
+ project={project}
+ />
+ )}
+ </div>}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.css b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.css
new file mode 100644
index 00000000000..b42aa641aff
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.css
@@ -0,0 +1,15 @@
+.application-quality-gate-project {
+ padding: 10px;
+}
+
+.overview-quality-gate-condition:hover .application-quality-gate-project {
+ padding: 9px;
+}
+
+.application-quality-gate-project-conditions {
+ margin-top: 4px;
+}
+
+.application-quality-gate-project-conditions > li {
+ margin-top: 4px;
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.js
new file mode 100644
index 00000000000..1c96978d4ae
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.js
@@ -0,0 +1,103 @@
+/*
+ * 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.
+ */
+// @flow
+import React from 'react';
+import { Link } from 'react-router';
+import classNames from 'classnames';
+import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
+import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
+import { getProjectUrl } from '../../../helpers/urls';
+import './ApplicationQualityGateProject.css';
+
+type Condition = {
+ comparator: string,
+ errorThreshold?: string,
+ metricKey: string,
+ onLeak: boolean,
+ status: string,
+ value: string,
+ warningThreshold?: string
+};
+
+type Props = {
+ metrics: {
+ [string]: {
+ key: string,
+ name: string,
+ type: string
+ }
+ },
+ project: {
+ conditions: Array<Condition>,
+ key: string,
+ name: string,
+ status: string
+ }
+};
+
+export default class ApplicationQualityGateProject extends React.PureComponent {
+ props: Props;
+
+ renderCondition = (condition: Condition) => {
+ const metric = this.props.metrics[condition.metricKey];
+ const metricName = getLocalizedMetricName(metric);
+ const threshold = condition.errorThreshold || condition.warningThreshold;
+ const isDiff = isDiffMetric(condition.metricKey);
+
+ return (
+ <li key={condition.metricKey}>
+ <span className="text-limited">
+ <strong>{formatMeasure(condition.value, metric.type)}</strong> {metricName}
+ {!isDiff && condition.onLeak && ' ' + translate('quality_gates.conditions.leak')}
+ </span>
+ <span
+ className={classNames('pull-right', 'big-spacer-left', {
+ 'text-danger': condition.status === 'ERROR',
+ 'text-warning': condition.status === 'WARN'
+ })}>
+ {translate('quality_gates.operator', condition.comparator, 'short')}{' '}
+ {formatMeasure(threshold, metric.type)}
+ </span>
+ </li>
+ );
+ };
+
+ render() {
+ const { project } = this.props;
+
+ return (
+ <Link
+ className={classNames(
+ 'overview-quality-gate-condition',
+ 'overview-quality-gate-condition-' + project.status.toLowerCase()
+ )}
+ to={getProjectUrl(project.key)}>
+ <div className="application-quality-gate-project">
+ <h4>
+ {project.name}
+ </h4>
+ <ul className="application-quality-gate-project-conditions">
+ {project.conditions.filter(c => c.status !== 'OK').map(this.renderCondition)}
+ </ul>
+ </div>
+ </Link>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.js
new file mode 100644
index 00000000000..ca434aebb90
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.js
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import ApplicationQualityGate from '../ApplicationQualityGate';
+
+it('renders', () => {
+ const wrapper = shallow(<ApplicationQualityGate component={{ key: 'foo' }} />);
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({
+ loading: false,
+ metrics: {},
+ status: 'ERROR',
+ projects: [
+ { conditions: [], key: 'project1', name: 'project1', status: 'ERROR' },
+ { conditions: [], key: 'project2', name: 'project2', status: 'OK' },
+ { conditions: [], key: 'project3', name: 'project3', status: 'WARN' }
+ ]
+ });
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.js
new file mode 100644
index 00000000000..9aad7367621
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.js
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import ApplicationQualityGateProject from '../ApplicationQualityGateProject';
+
+const metrics = {
+ bugs: { key: 'bugs', name: 'Bugs', type: 'INT' },
+ new_coverage: { key: 'new_coverage', name: 'Coverage on New Code', type: 'PERCENT' },
+ skipped_tests: { key: 'skipped_tests', name: 'Skipped Tests', type: 'INT' }
+};
+
+it('renders', () => {
+ const project = {
+ key: 'foo',
+ name: 'Foo',
+ status: 'ERROR',
+ conditions: [
+ {
+ status: 'ERROR',
+ metricKey: 'new_coverage',
+ comparator: 'LT',
+ onLeak: true,
+ errorThreshold: '85',
+ value: '82.50562381034781'
+ },
+ {
+ status: 'WARN',
+ metricKey: 'bugs',
+ comparator: 'GT',
+ onLeak: false,
+ warningThreshold: '0',
+ value: '17'
+ },
+ {
+ status: 'ERROR',
+ metricKey: 'bugs',
+ comparator: 'GT',
+ onLeak: true,
+ warningThreshold: '0',
+ value: '3'
+ },
+ {
+ status: 'OK',
+ metricKey: 'skipped_tests',
+ comparator: 'GT',
+ onLeak: false,
+ warningThreshold: '0',
+ value: '0'
+ }
+ ]
+ };
+ const wrapper = shallow(<ApplicationQualityGateProject metrics={metrics} project={project} />);
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.js.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.js.snap
new file mode 100644
index 00000000000..247c1251986
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.js.snap
@@ -0,0 +1,62 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<div
+ className="overview-quality-gate"
+ id="overview-quality-gate"
+>
+ <h2
+ className="overview-title"
+ >
+ overview.quality_gate
+ <i
+ className="spinner spacer-left"
+ />
+ </h2>
+</div>
+`;
+
+exports[`renders 2`] = `
+<div
+ className="overview-quality-gate"
+ id="overview-quality-gate"
+>
+ <h2
+ className="overview-title"
+ >
+ overview.quality_gate
+ <Level
+ level="ERROR"
+ muted={false}
+ small={false}
+ />
+ </h2>
+ <div
+ className="overview-quality-gate-conditions-list clearfix"
+ id="overview-quality-gate-conditions-list"
+ >
+ <ApplicationQualityGateProject
+ metrics={Object {}}
+ project={
+ Object {
+ "conditions": Array [],
+ "key": "project1",
+ "name": "project1",
+ "status": "ERROR",
+ }
+ }
+ />
+ <ApplicationQualityGateProject
+ metrics={Object {}}
+ project={
+ Object {
+ "conditions": Array [],
+ "key": "project3",
+ "name": "project3",
+ "status": "WARN",
+ }
+ }
+ />
+ </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap
new file mode 100644
index 00000000000..c887cc5062e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap
@@ -0,0 +1,84 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<Link
+ className="overview-quality-gate-condition overview-quality-gate-condition-error"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+>
+ <div
+ className="application-quality-gate-project"
+ >
+ <h4>
+ Foo
+ </h4>
+ <ul
+ className="application-quality-gate-project-conditions"
+ >
+ <li>
+ <span
+ className="text-limited"
+ >
+ <strong>
+ 82.5%
+ </strong>
+
+ Coverage on New Code
+ </span>
+ <span
+ className="pull-right big-spacer-left text-danger"
+ >
+ quality_gates.operator.LT.short
+
+ 85.0%
+ </span>
+ </li>
+ <li>
+ <span
+ className="text-limited"
+ >
+ <strong>
+ 17
+ </strong>
+
+ Bugs
+ </span>
+ <span
+ className="pull-right big-spacer-left text-warning"
+ >
+ quality_gates.operator.GT.short
+
+ 0
+ </span>
+ </li>
+ <li>
+ <span
+ className="text-limited"
+ >
+ <strong>
+ 3
+ </strong>
+
+ Bugs
+ quality_gates.conditions.leak
+ </span>
+ <span
+ className="pull-right big-spacer-left text-danger"
+ >
+ quality_gates.operator.GT.short
+
+ 0
+ </span>
+ </li>
+ </ul>
+ </div>
+</Link>
+`;
diff --git a/server/sonar-web/src/main/js/apps/overview/styles.css b/server/sonar-web/src/main/js/apps/overview/styles.css
index 8744bb255b3..43437c10623 100644
--- a/server/sonar-web/src/main/js/apps/overview/styles.css
+++ b/server/sonar-web/src/main/js/apps/overview/styles.css
@@ -339,6 +339,11 @@
text-align: center;
}
+.overview-meta-size-ncloc.is-half-width {
+ width: 50%;
+ box-sizing: border-box;
+}
+
.overview-meta-size-ncloc a {
line-height: 24px;
font-size: 18px;
diff --git a/server/sonar-web/src/main/js/apps/overview/utils.js b/server/sonar-web/src/main/js/apps/overview/utils.js
index 2ea31331be9..3c3f1c5a439 100644
--- a/server/sonar-web/src/main/js/apps/overview/utils.js
+++ b/server/sonar-web/src/main/js/apps/overview/utils.js
@@ -58,6 +58,7 @@ export const METRICS = [
// size
'ncloc',
'ncloc_language_distribution',
+ 'projects',
'new_lines'
];
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js b/server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js
index d324dd36255..6f8f2e18aa9 100644
--- a/server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js
+++ b/server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js
@@ -23,7 +23,8 @@ import { getAppState } from '../../../store/rootReducer';
import { getRootQualifiers } from '../../../store/appState/duck';
const mapStateToProps = state => ({
- topQualifiers: getRootQualifiers(getAppState(state))
+ // treat applications as portfolios
+ topQualifiers: getRootQualifiers(getAppState(state)).filter(q => q !== 'APP')
});
export default connect(mapStateToProps)(App);
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/App.js b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js
index d06a080b3e9..13d1290f721 100644
--- a/server/sonar-web/src/main/js/apps/permissions/project/components/App.js
+++ b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js
@@ -349,20 +349,22 @@ export default class App extends React.PureComponent {
/>
<PageError />
{this.props.component.qualifier === 'TRK' &&
- <VisibilitySelector
- canTurnToPrivate={canTurnToPrivate}
- className="big-spacer-top big-spacer-bottom"
- onChange={this.handleVisibilityChange}
- visibility={this.props.component.visibility}
- />}
- {!canTurnToPrivate &&
- <UpgradeOrganizationBox organization={this.props.component.organization} />}
- {this.state.disclaimer &&
- <PublicProjectDisclaimer
- component={this.props.component}
- onClose={this.closeDisclaimer}
- onConfirm={this.turnProjectToPublic}
- />}
+ <div>
+ <VisibilitySelector
+ canTurnToPrivate={canTurnToPrivate}
+ className="big-spacer-top big-spacer-bottom"
+ onChange={this.handleVisibilityChange}
+ visibility={this.props.component.visibility}
+ />
+ {!canTurnToPrivate &&
+ <UpgradeOrganizationBox organization={this.props.component.organization} />}
+ {this.state.disclaimer &&
+ <PublicProjectDisclaimer
+ component={this.props.component}
+ onClose={this.closeDisclaimer}
+ onConfirm={this.turnProjectToPublic}
+ />}
+ </div>}
<AllHoldersList
component={this.props.component}
filter={this.state.filter}
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js
index 8b968108a19..2cbe207fabb 100644
--- a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js
+++ b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js
@@ -55,7 +55,7 @@ export default class PageHeader extends React.PureComponent {
const canApplyPermissionTemplate =
configuration != null && configuration.canApplyPermissionTemplate;
- const description = ['VW', 'SVW'].includes(component.qualifier)
+ const description = ['VW', 'SVW', 'APP'].includes(component.qualifier)
? translate('roles.page.description_portfolio')
: translate('roles.page.description2');
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/constants.js b/server/sonar-web/src/main/js/apps/permissions/project/constants.js
index 7a92d73237d..cc5ed8a4d30 100644
--- a/server/sonar-web/src/main/js/apps/permissions/project/constants.js
+++ b/server/sonar-web/src/main/js/apps/permissions/project/constants.js
@@ -27,5 +27,6 @@ export const PERMISSIONS_ORDER_BY_QUALIFIER = {
TRK: PERMISSIONS_ORDER_FOR_PROJECT,
VW: PERMISSIONS_ORDER_FOR_VIEW,
SVW: PERMISSIONS_ORDER_FOR_VIEW,
+ APP: PERMISSIONS_ORDER_FOR_VIEW,
DEV: PERMISSIONS_ORDER_FOR_DEV
};
diff --git a/server/sonar-web/src/main/js/apps/project-admin/deletion/Header.js b/server/sonar-web/src/main/js/apps/project-admin/deletion/Header.js
index d236654ed98..8269e2ac6af 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/deletion/Header.js
+++ b/server/sonar-web/src/main/js/apps/project-admin/deletion/Header.js
@@ -22,9 +22,12 @@ import React from 'react';
import { translate } from '../../../helpers/l10n';
export default function Header(props: { component: { qualifier: string } }) {
- const description = ['VW', 'SVW'].includes(props.component.qualifier)
+ const { qualifier } = props.component;
+ const description = ['VW', 'SVW'].includes(qualifier)
? translate('portfolio_deletion.page.description')
- : translate('project_deletion.page.description');
+ : qualifier === 'APP'
+ ? translate('application_deletion.page.description')
+ : translate('project_deletion.page.description');
return (
<header className="page-header">
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/constants.js b/server/sonar-web/src/main/js/apps/projects-admin/constants.js
index 6f0f0323b4f..057d08d9109 100644
--- a/server/sonar-web/src/main/js/apps/projects-admin/constants.js
+++ b/server/sonar-web/src/main/js/apps/projects-admin/constants.js
@@ -19,7 +19,7 @@
*/
export const PAGE_SIZE = 50;
-export const QUALIFIERS_ORDER = ['TRK', 'VW', 'DEV'];
+export const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP', 'DEV'];
export const TYPE = {
ALL: 'ALL',