aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/overview
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src/main/js/apps/overview')
-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
19 files changed, 769 insertions, 46 deletions
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'
];