]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9531 Change the application space
authorStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 24 Jul 2017 14:12:39 +0000 (16:12 +0200)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Mon, 31 Jul 2017 09:27:51 +0000 (11:27 +0200)
42 files changed:
server/sonar-web/src/main/js/api/application.js [new file with mode: 0644]
server/sonar-web/src/main/js/api/quality-gates.js
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js
server/sonar-web/src/main/js/app/components/search/SearchResult.js
server/sonar-web/src/main/js/app/components/search/utils.js
server/sonar-web/src/main/js/apps/code/components/App.js
server/sonar-web/src/main/js/apps/code/components/Component.js
server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.js
server/sonar-web/src/main/js/apps/code/components/Search.js
server/sonar-web/src/main/js/apps/code/utils.js
server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js
server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js
server/sonar-web/src/main/js/apps/component-measures/home/Home.js
server/sonar-web/src/main/js/apps/component-measures/home/actions.js
server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js
server/sonar-web/src/main/js/apps/overview/components/App.js
server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js
server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.js
server/sonar-web/src/main/js/apps/overview/components/__tests__/ApplicationLeakPeriodLegend-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/ApplicationLeakPeriodLegend-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js
server/sonar-web/src/main/js/apps/overview/main/enhance.js
server/sonar-web/src/main/js/apps/overview/meta/Meta.js
server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js
server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.css [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/styles.css
server/sonar-web/src/main/js/apps/overview/utils.js
server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js
server/sonar-web/src/main/js/apps/permissions/project/components/App.js
server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js
server/sonar-web/src/main/js/apps/permissions/project/constants.js
server/sonar-web/src/main/js/apps/project-admin/deletion/Header.js
server/sonar-web/src/main/js/apps/projects-admin/constants.js
sonar-core/src/main/resources/org/sonar/l10n/core.properties

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 (file)
index 0000000..bbc9016
--- /dev/null
@@ -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);
index 7c60f3a2a2444bb8fa6a9861d95e5df29333d619..878c2e32dcaf1abc6eaf9d251e7d07f54396760f 100644 (file)
@@ -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 });
+}
index d9963b913ac624fcf1bf62d12388cfb35605fb07..61738902ee9f830e178ca6217b956efd02996541 100644 (file)
@@ -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,
index e7f2ca6d46304d01d0b7d684a8dc40ce7bb59bd9..5e3038d65f59adcb652925ebe36035cf0a06f654 100644 (file)
@@ -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;
     }
 
index 7f6337dcda09bcf95f1a0a43a9e7aebe1757c70d..0488535cba6950b45284e6a3afde5e5e3bdb3a20 100644 (file)
@@ -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;
     }
 
index 5ed66863da26ff2695498a982c9ba989a275e905..ebeea69e645564c40b36ee2d3674f8b2e874b934 100644 (file)
@@ -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));
index a85387a0e5ebf01e317560fe8f3d4b3601d409d2..3a0f994aa1cdd29741b49711ba2798cf60e28477 100644 (file)
@@ -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({
index 6770e4c28a69928b2dcf87e365477cb35418f36e..e36ad13bcf6159b3b4830a6a70b39b4d519fc14d 100644 (file)
@@ -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 })}>
index 1552dfad89d26261f134e8163f97e5544b605c87..a2977ae960b356e9e1a4e1b53d1b43232019c4dd 100644 (file)
@@ -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>
index 12b644d943c07c49bd1dc54596b49f401a66d56d..a34d87e3c9ed623f697fc61b40a488c52c49c4b5 100644 (file)
@@ -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 => {
index 44d539a2122f9941beb0507a0152d8d224c0c881..2a1f5552cc0e3a99e469ec5e332200127f06f798 100644 (file)
@@ -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)
index 302c02959bff2dd357587d133a603b2390c6fa5a..22483f15b54dd4095d0de6847e787dd8784ec9dc 100644 (file)
@@ -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))}
index 392b7e19587bd468c252f7b57fa68a48959a4af5..f2281c24eba5be25123086e25184d59431e7f32a 100644 (file)
@@ -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 }}>
index fa3ddd9666279f5e6d6d108cb1a69944a685d7d2..b81be299a02340966c71c681c713d71f3b0a22d5 100644 (file)
@@ -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">
index ed8e64d79a69859d5be966165031f5b8404ffdcf..6ae4585b20b82b1a244c21026835f7538b09464e 100644 (file)
@@ -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());
     });
   };
index 8d4c9269e1e1fcf1f57d4851c0554548bc932eab..62a1af46084dac89dd57121d589ed59838707aa9 100644 (file)
@@ -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,
index 247a4d8170417535d0f4eda4858a49012d53f6e5..e704e35cd8d46cbb9650459d534b5b8802f9ac36 100644 (file)
@@ -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 (file)
index 0000000..3b43a02
--- /dev/null
@@ -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>
+    );
+  }
+}
index 79ab68e793c37eb106aaa4da4572db6cc19c537a..aea20cf3d40b736ff966bd37b767157302415eae 100644 (file)
@@ -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} />
index 70dd35ee95df57c1ff06e5d1ef152f0bc1b80d35..a39a89c9c995241c5cba1724f5bba6f899059e8a 100644 (file)
  */
 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 (file)
index 0000000..ce900ba
--- /dev/null
@@ -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 (file)
index 0000000..217405a
--- /dev/null
@@ -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>
+`;
index f427b1a9a44c0414c3c78449e226f6cc7739c1be..ec90cafe30f3d25a0ec7d269cc758c874161f8ff 100644 (file)
@@ -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">
index 7c465b46c27cdd5c72d5af373aa7d0f406f19315..b5599d43c00b9822ca61e50ee86111cca7b8f9f5 100644 (file)
@@ -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
index 4d84a3f2e234ddf872680f9e7611f84dbf6d32bd..689db65fbed3d0c300321496797da40a8ca2e0c7 100644 (file)
@@ -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} />
 
index 6c63212875716065ac3e4f9a5a033d57fa3a899f..fe153ca52fdd12fa6de92f8fb3fcd45a0e37a329 100644 (file)
  */
 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 (file)
index 0000000..80229f4
--- /dev/null
@@ -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 (file)
index 0000000..b42aa64
--- /dev/null
@@ -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 (file)
index 0000000..1c96978
--- /dev/null
@@ -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 (file)
index 0000000..ca434ae
--- /dev/null
@@ -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 (file)
index 0000000..9aad736
--- /dev/null
@@ -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 (file)
index 0000000..247c125
--- /dev/null
@@ -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 (file)
index 0000000..c887cc5
--- /dev/null
@@ -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>
+`;
index 8744bb255b3939685fd964b00e20d04c672ab4e8..43437c1062325b1c853aa0e71a73e65f052fd86d 100644 (file)
   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;
index 2ea31331be91c98b572d3eaf59c158ecb89978c6..3c3f1c5a439604e9d7800cdc0d387b7f7a1b120b 100644 (file)
@@ -58,6 +58,7 @@ export const METRICS = [
   // size
   'ncloc',
   'ncloc_language_distribution',
+  'projects',
   'new_lines'
 ];
 
index d324dd362553f783c6055077aaa8725cd0e0a58e..6f8f2e18aa947f550c5dd14c8f48f1118daaba54 100644 (file)
@@ -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);
index d06a080b3e905e126066c2e744fd71cde239d920..13d1290f7216ea25c291f20ee4a6b126f11baa9c 100644 (file)
@@ -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}
index 8b968108a1932ed0e9fb41c162f5d18ee2045472..2cbe207fabbee1d974c14f318efbe6a371fbba74 100644 (file)
@@ -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');
 
index 7a92d73237d0d4e064e4033f19c0526224752733..cc5ed8a4d30037f92950c84c2ce4237706c56353 100644 (file)
@@ -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
 };
index d236654ed98357e964abfec12f52bdf6da604616..8269e2ac6afd8caead9fadbf23f32d3552898123 100644 (file)
@@ -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">
index 6f0f0323b4f1eb7d932fc31e7d23278391cc546f..057d08d910918e5d76e72008fecd8e1022cf154b 100644 (file)
@@ -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',
index 42d69209f392f4de7e7d0bdcc45389a6c4bfd48d..23adb0d3615c9f56db69c8e7a8b1b27f3b90c6ca 100644 (file)
@@ -439,6 +439,7 @@ qualifiers.update.DEV=Update Developer
 qualifiers.update.APP=Update Application
 
 qualifier.description.VW=Potentially multi-level, management-oriented overview aggregation.
+qualifier.description.SVW=Potentially multi-level, management-oriented overview aggregation.
 qualifier.description.APP=Single-level aggregation with a technical focus and a project-like homepage.
 
 #------------------------------------------------------------------------------
@@ -571,6 +572,7 @@ update_key.page.description=Edit the keys of a project and/or its modules. Key c
 deletion.page=Deletion
 project_deletion.page.description=Delete this project from SonarQube. The operation cannot be undone.
 portfolio_deletion.page.description=Delete this portfolio from SonarQube. Component projects and Local Reference Portfolios will not be deleted, but component Standard Portfolios will be deleted. This operation cannot be undone.
+application_deletion.page.description=Delete this application from SonarQube. Application projects will not be deleted. This operation cannot be undone.
 provisioning.page=Provisioning
 provisioning.page.description=Use this page to initialize projects if you would like to configure them before the first analysis. Once a project is provisioned, you have access to perform all project configurations on it.