]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11693 New Overview tab for PRs and SLBs
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Wed, 6 Feb 2019 06:14:39 +0000 (07:14 +0100)
committerSonarTech <sonartech@sonarsource.com>
Mon, 11 Mar 2019 19:21:02 +0000 (20:21 +0100)
49 files changed:
server/sonar-web/src/main/js/api/quality-gates.ts
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap
server/sonar-web/src/main/js/app/theme.js
server/sonar-web/src/main/js/app/types.d.ts
server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskComponent-test.tsx.snap
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap
server/sonar-web/src/main/js/apps/overview/components/App.tsx
server/sonar-web/src/main/js/apps/overview/pullRequests/AfterMergeEstimate.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/IssueLabel.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/IssueRating.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/LargeQualityGateBadge.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/MeasurementLabel.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/ReviewApp.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/AfterMergeEstimate-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/IssueLabel-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/IssueRating-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/LargeQualityGateBadge-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/MeasurementLabel-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/ReviewApp-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/AfterMergeEstimate-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/IssueLabel-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/IssueRating-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/LargeQualityGateBadge-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/MeasurementLabel-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/ReviewApp-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.tsx
server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateConditions.tsx
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/QualityGateCondition-test.tsx
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/QualityGateConditions-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/styles.css
server/sonar-web/src/main/js/apps/overview/utils.ts
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap
server/sonar-web/src/main/js/components/common/BranchStatus.css [deleted file]
server/sonar-web/src/main/js/components/common/BranchStatus.tsx
server/sonar-web/src/main/js/components/measure/utils.ts
server/sonar-web/src/main/js/helpers/measures.ts
server/sonar-web/src/main/js/helpers/qualityGates.ts [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/testMocks.ts
server/sonar-web/src/main/js/helpers/urls.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index b1568744d4b05e66dabc77994f0cfcb4d3ec7379..c6310ae13b7148ff9bc7b4ee388ec6a76cf0e6c5 100644 (file)
@@ -166,3 +166,12 @@ export function getApplicationQualityGate(data: {
 }): Promise<ApplicationQualityGate> {
   return getJSON('/api/qualitygates/application_status', data).catch(throwGlobalError);
 }
+
+export function getQualityGateProjectStatus(
+  data: {
+    projectKey?: string;
+    projectId?: string;
+  } & T.BranchParameters
+): Promise<T.QualityGateProjectStatus> {
+  return getJSON('/api/qualitygates/project_status', data).catch(throwGlobalError);
+}
index 2b66e1c6cbb1bffad1dd8c34fd25562361fd5f19..1c59dad23387427fef77cda16ec0a8e2caf7453b 100644 (file)
@@ -83,12 +83,6 @@ export class ComponentNavMenu extends React.PureComponent<Props> {
   };
 
   renderDashboardLink() {
-    const { branchLike } = this.props;
-
-    if (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) {
-      return null;
-    }
-
     const pathname = this.isPortfolio() ? '/portfolio' : '/dashboard';
     return (
       <li>
index ffa16a1e0f9856a47203ccb6e841cd744a75d1ad..21a8a1ccb3b45906a588a38aee130f09a5c98227 100644 (file)
@@ -51,7 +51,7 @@ it('renders short-living branch', () => {
     isMain: false,
     mergeBranch: 'master',
     name: 'foo',
-    status: { bugs: 0, codeSmells: 0, qualityGateStatus: 'OK', vulnerabilities: 0 },
+    status: { qualityGateStatus: 'OK' },
     type: 'SHORT'
   };
   const component = {} as T.Component;
index 50acdebf05f5043f9fbc9aea96b8744fadf5303f..96ec59ddb2ecf03b364d3a26d6a08d2804a3e111 100644 (file)
@@ -95,7 +95,7 @@ function shortBranch(name: string, isOrphan?: true): T.ShortLivingBranch {
     isOrphan,
     mergeBranch: 'master',
     name,
-    status: { bugs: 0, codeSmells: 0, qualityGateStatus: 'OK', vulnerabilities: 0 },
+    status: { qualityGateStatus: 'OK' },
     type: 'SHORT'
   };
 }
@@ -109,7 +109,7 @@ function pullRequest(title: string): T.PullRequest {
     base: 'master',
     branch: 'feature',
     key: '1234',
-    status: { bugs: 0, codeSmells: 0, qualityGateStatus: 'OK', vulnerabilities: 0 },
+    status: { qualityGateStatus: 'OK' },
     title
   };
 }
index 690105720b664cda56fc0f3405c7ffeb17f0a8df..0de990f31f759a87a6fcc1111992bebca2f2b1e7 100644 (file)
@@ -27,7 +27,7 @@ const shortBranch: T.ShortLivingBranch = {
   isMain: false,
   mergeBranch: 'master',
   name: 'foo',
-  status: { bugs: 1, codeSmells: 2, qualityGateStatus: 'ERROR', vulnerabilities: 3 },
+  status: { qualityGateStatus: 'ERROR' },
   type: 'SHORT'
 };
 
index aa8efb0ba7a4f0d160e91dd794509b265336fd62..1c854d25195574d59baeddcce734eb17d940505b 100644 (file)
@@ -40,7 +40,6 @@ it('renders meta for pull request', () => {
   expect(
     shallowRender({
       branchLike: mockPullRequest({
-        status: { bugs: 0, codeSmells: 2, qualityGateStatus: 'ERROR', vulnerabilities: 3 },
         url: 'https://example.com/pull/1234'
       })
     })
@@ -50,9 +49,7 @@ it('renders meta for pull request', () => {
 function shallowRender(props = {}) {
   return shallow(
     <ComponentNavMeta
-      branchLike={mockShortLivingBranch({
-        status: { bugs: 0, codeSmells: 2, qualityGateStatus: 'ERROR', vulnerabilities: 3 }
-      })}
+      branchLike={mockShortLivingBranch()}
       component={mockComponent({ analysisDate: '2017-01-02T00:00:00.000Z', version: '0.0.1' })}
       currentUser={mockCurrentUser({ isLoggedIn: false })}
       warnings={[]}
index 6bad17ad6b4ddf3ce76daf5d2430b10f6f94f4be..f9e722af3053118f2b6cf237406a31bd76ca70a7 100644 (file)
@@ -188,10 +188,7 @@ exports[`renders short-living branch 1`] = `
                 "mergeBranch": "master",
                 "name": "foo",
                 "status": Object {
-                  "bugs": 0,
-                  "codeSmells": 0,
                   "qualityGateStatus": "OK",
-                  "vulnerabilities": 0,
                 },
                 "type": "SHORT",
               },
@@ -209,10 +206,7 @@ exports[`renders short-living branch 1`] = `
               "mergeBranch": "master",
               "name": "foo",
               "status": Object {
-                "bugs": 0,
-                "codeSmells": 0,
                 "qualityGateStatus": "OK",
-                "vulnerabilities": 0,
               },
               "type": "SHORT",
             }
@@ -233,10 +227,7 @@ exports[`renders short-living branch 1`] = `
               "mergeBranch": "master",
               "name": "foo",
               "status": Object {
-                "bugs": 0,
-                "codeSmells": 0,
                 "qualityGateStatus": "OK",
-                "vulnerabilities": 0,
               },
               "type": "SHORT",
             }
index e88cec71b417a5e7714c19bc2f316e38afeaed8d..c4bc94a269012f2c7222af9ebb0eeba16b909330 100644 (file)
@@ -47,10 +47,7 @@ exports[`renders list 1`] = `
           "branch": "feature",
           "key": "1234",
           "status": Object {
-            "bugs": 0,
-            "codeSmells": 0,
             "qualityGateStatus": "OK",
-            "vulnerabilities": 0,
           },
           "title": "qux",
         }
@@ -89,10 +86,7 @@ exports[`renders list 1`] = `
           "mergeBranch": "master",
           "name": "baz",
           "status": Object {
-            "bugs": 0,
-            "codeSmells": 0,
             "qualityGateStatus": "OK",
-            "vulnerabilities": 0,
           },
           "type": "SHORT",
         }
@@ -115,10 +109,7 @@ exports[`renders list 1`] = `
           "mergeBranch": "master",
           "name": "foo",
           "status": Object {
-            "bugs": 0,
-            "codeSmells": 0,
             "qualityGateStatus": "OK",
-            "vulnerabilities": 0,
           },
           "type": "SHORT",
         }
@@ -178,10 +169,7 @@ exports[`renders list 1`] = `
           "mergeBranch": "master",
           "name": "baz",
           "status": Object {
-            "bugs": 0,
-            "codeSmells": 0,
             "qualityGateStatus": "OK",
-            "vulnerabilities": 0,
           },
           "type": "SHORT",
         }
@@ -231,10 +219,7 @@ exports[`searches 1`] = `
           "mergeBranch": "master",
           "name": "foobar",
           "status": Object {
-            "bugs": 0,
-            "codeSmells": 0,
             "qualityGateStatus": "OK",
-            "vulnerabilities": 0,
           },
           "type": "SHORT",
         }
index 3df35d2137ea2612c89da1850a8e23b7d666b820..3d0620b4377c9adbc22af3e4b6e4631aa6f1ebd5 100644 (file)
@@ -70,7 +70,6 @@ exports[`renders short-living branch 1`] = `
         "query": Object {
           "branch": "foo",
           "id": "component",
-          "resolved": "false",
         },
       }
     }
@@ -86,10 +85,7 @@ exports[`renders short-living branch 1`] = `
             "mergeBranch": "master",
             "name": "foo",
             "status": Object {
-              "bugs": 1,
-              "codeSmells": 2,
               "qualityGateStatus": "ERROR",
-              "vulnerabilities": 3,
             },
             "type": "SHORT",
           }
@@ -108,10 +104,7 @@ exports[`renders short-living branch 1`] = `
             "mergeBranch": "master",
             "name": "foo",
             "status": Object {
-              "bugs": 1,
-              "codeSmells": 2,
               "qualityGateStatus": "ERROR",
-              "vulnerabilities": 3,
             },
             "type": "SHORT",
           }
@@ -137,7 +130,6 @@ exports[`renders short-living orhpan branch 1`] = `
         "query": Object {
           "branch": "foo",
           "id": "component",
-          "resolved": "false",
         },
       }
     }
@@ -154,10 +146,7 @@ exports[`renders short-living orhpan branch 1`] = `
             "mergeBranch": "master",
             "name": "foo",
             "status": Object {
-              "bugs": 1,
-              "codeSmells": 2,
               "qualityGateStatus": "ERROR",
-              "vulnerabilities": 3,
             },
             "type": "SHORT",
           }
@@ -177,10 +166,7 @@ exports[`renders short-living orhpan branch 1`] = `
             "mergeBranch": "master",
             "name": "foo",
             "status": Object {
-              "bugs": 1,
-              "codeSmells": 2,
               "qualityGateStatus": "ERROR",
-              "vulnerabilities": 3,
             },
             "type": "SHORT",
           }
index 8d1984cd7ebce6db5d6977baa25e0503a5a7dd9a..221e7dd6f26ad9778496a4319fee1b7cccc859d6 100644 (file)
@@ -976,6 +976,24 @@ exports[`should work for long-living branches 2`] = `
 
 exports[`should work for short-living branches 1`] = `
 <NavBarTabs>
+  <li>
+    <Link
+      activeClassName="active"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/dashboard",
+          "query": Object {
+            "branch": "feature",
+            "id": "foo",
+          },
+        }
+      }
+    >
+      overview.page
+    </Link>
+  </li>
   <li>
     <Link
       activeClassName="active"
index c7637342d5c1caf978d4845db51ad0c0d84b35f6..efe7cceabd82990904e70183aca641eaf2c27860 100644 (file)
@@ -59,12 +59,6 @@ exports[`renders meta for pull request 1`] = `
           "base": "master",
           "branch": "feature/foo/bar",
           "key": "1001",
-          "status": Object {
-            "bugs": 0,
-            "codeSmells": 2,
-            "qualityGateStatus": "ERROR",
-            "vulnerabilities": 3,
-          },
           "title": "Foo Bar feature",
           "url": "https://example.com/pull/1234",
         }
@@ -95,12 +89,6 @@ exports[`renders status of short-living branch 1`] = `
           "isMain": false,
           "mergeBranch": "master",
           "name": "release-1.0",
-          "status": Object {
-            "bugs": 0,
-            "codeSmells": 2,
-            "qualityGateStatus": "ERROR",
-            "vulnerabilities": 3,
-          },
           "type": "SHORT",
         }
       }
index 4fd00b72e363534f2a7b464b7e341c4f604e7175..22869b60a77bf6e8f480061b81525b7fbf1e0e68 100644 (file)
@@ -30,6 +30,7 @@ module.exports = {
   green: '#00aa00',
   lineCoverageGreen: '#b4dd78',
   lightGreen: '#b0d513',
+  veryLightGreen: '#f5f9fc',
   yellow: '#eabe06',
   orange: '#ed7d20',
   red: '#d4333f',
@@ -43,6 +44,8 @@ module.exports = {
   gray60: '#999',
   gray40: '#404040',
 
+  transparentWhite: 'rgba(255,255,255,0.62)',
+
   disableGrayText: '#bbb',
   disableGrayBorder: '#ddd',
   disableGrayBg: '#ebebeb',
index 816f12440e745059360b92246367c321cec5b469..c0df280656acf2ef963fca8919c25fb20d2c1d55 100644 (file)
@@ -618,12 +618,7 @@ declare namespace T {
     branch: string;
     key: string;
     isOrphan?: true;
-    status?: {
-      bugs: number;
-      codeSmells: number;
-      qualityGateStatus: string;
-      vulnerabilities: number;
-    };
+    status?: { qualityGateStatus: string };
     title: string;
     url?: string;
   }
@@ -644,6 +639,35 @@ declare namespace T {
     name: string;
   }
 
+  export interface QualityGateProjectStatus {
+    projectStatus: {
+      conditions?: Array<{
+        status: 'ERROR' | 'OK';
+        metricKey: string;
+        comparator: string;
+        periodIndex: number;
+        errorThreshold: string;
+        actualValue: string;
+      }>;
+      ignoredConditions: boolean;
+      status: string;
+    };
+  }
+
+  export interface QualityGateStatusCondition {
+    actual?: string;
+    error?: string;
+    level: string;
+    metric: string;
+    op: string;
+    period?: number;
+    warning?: string;
+  }
+
+  export interface QualityGateStatusConditionEnhanced extends QualityGateStatusCondition {
+    measure: T.MeasureEnhanced;
+  }
+
   export interface Rule {
     isTemplate?: boolean;
     key: string;
@@ -755,12 +779,6 @@ declare namespace T {
     isMain: false;
     isOrphan?: true;
     mergeBranch: string;
-    status?: {
-      bugs: number;
-      codeSmells: number;
-      qualityGateStatus: string;
-      vulnerabilities: number;
-    };
     type: 'SHORT';
   }
 
index 4b510fa8d43daa72d113cc8702ccca47de82683b..8d5dacc3a4eb8ae779171e24899f3db540063cda 100644 (file)
@@ -65,7 +65,6 @@ exports[`renders correctly for branches and pullrequest 1`] = `
         "query": Object {
           "branch": "feature",
           "id": "foo",
-          "resolved": "false",
         },
       }
     }
@@ -158,7 +157,6 @@ exports[`renders correctly for branches and pullrequest 3`] = `
         "query": Object {
           "id": "foo",
           "pullRequest": "pr-89",
-          "resolved": "false",
         },
       }
     }
index 7de2ed12e3b72daf07ede7afebc79493d3205af7..ebd567750168b0e83625a7ad5383ccdafe2cadbb 100644 (file)
@@ -169,7 +169,6 @@ exports[`should render with short living branch 1`] = `
       >
         <strong>
           <Measure
-            className="leak-box"
             metricKey="new_reliability_rating"
             metricType="RATING"
             value="3.0"
index 10df203d893dee649a26307c96020832524c4ffc..58098c3dc9817c395819c47531aecc11b1d53982 100644 (file)
@@ -21,16 +21,12 @@ import * as React from 'react';
 import { Helmet } from 'react-helmet';
 import OverviewApp from './OverviewApp';
 import EmptyOverview from './EmptyOverview';
+import ReviewApp from '../pullRequests/ReviewApp';
 import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
-import { isShortLivingBranch } from '../../../helpers/branches';
-import {
-  getShortLivingBranchUrl,
-  getProjectUrl,
-  getBaseUrl,
-  getPathUrlAsString
-} from '../../../helpers/urls';
-import { isSonarCloud } from '../../../helpers/system';
 import { withRouter, Router } from '../../../components/hoc/withRouter';
+import { getProjectUrl, getBaseUrl, getPathUrlAsString } from '../../../helpers/urls';
+import { isSonarCloud } from '../../../helpers/system';
+import { isShortLivingBranch, isPullRequest } from '../../../helpers/branches';
 
 interface Props {
   branchLike?: T.BranchLike;
@@ -44,24 +40,24 @@ interface Props {
 
 export class App extends React.PureComponent<Props> {
   componentDidMount() {
-    const { branchLike, component } = this.props;
+    const { component } = this.props;
 
     if (this.isPortfolio()) {
       this.props.router.replace({
         pathname: '/portfolio',
         query: { id: component.key }
       });
-    } else if (isShortLivingBranch(branchLike)) {
-      this.props.router.replace(getShortLivingBranchUrl(component.key, branchLike.name));
     }
   }
 
-  isPortfolio = () => ['VW', 'SVW'].includes(this.props.component.qualifier);
+  isPortfolio = () => {
+    return ['VW', 'SVW'].includes(this.props.component.qualifier);
+  };
 
   render() {
     const { branchLike, branchLikes, component } = this.props;
 
-    if (this.isPortfolio() || isShortLivingBranch(branchLike)) {
+    if (this.isPortfolio()) {
       return null;
     }
 
@@ -75,23 +71,29 @@ export class App extends React.PureComponent<Props> {
             />
           </Helmet>
         )}
-        <Suggestions suggestions="overview" />
 
-        {!component.analysisDate && (
-          <EmptyOverview
-            branchLike={branchLike}
-            branchLikes={branchLikes}
-            component={component}
-            hasAnalyses={this.props.isPending || this.props.isInProgress}
-            onComponentChange={this.props.onComponentChange}
-          />
-        )}
-        {component.analysisDate && (
-          <OverviewApp
-            branchLike={branchLike}
-            component={component}
-            onComponentChange={this.props.onComponentChange}
-          />
+        {isShortLivingBranch(branchLike) || isPullRequest(branchLike) ? (
+          <ReviewApp branchLike={branchLike} component={component} />
+        ) : (
+          <>
+            <Suggestions suggestions="overview" />
+
+            {!component.analysisDate ? (
+              <EmptyOverview
+                branchLike={branchLike}
+                branchLikes={branchLikes}
+                component={component}
+                hasAnalyses={this.props.isPending || this.props.isInProgress}
+                onComponentChange={this.props.onComponentChange}
+              />
+            ) : (
+              <OverviewApp
+                branchLike={branchLike}
+                component={component}
+                onComponentChange={this.props.onComponentChange}
+              />
+            )}
+          </>
         )}
       </>
     );
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/AfterMergeEstimate.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/AfterMergeEstimate.tsx
new file mode 100644 (file)
index 0000000..5ff3615
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { formatMeasure, findMeasure } from '../../../helpers/measures';
+import { translate } from '../../../helpers/l10n';
+import { MEASUREMENTS_MAP, MeasurementType } from '../utils';
+
+interface Props {
+  measures: T.Measure[];
+  type: MeasurementType;
+}
+
+export default function AfterMergeEstimate({ measures, type }: Props) {
+  const { afterMergeMetric } = MEASUREMENTS_MAP[type];
+
+  const measure = findMeasure(measures, afterMergeMetric);
+
+  if (!measure || measure.value === undefined) {
+    return null;
+  }
+
+  return (
+    <>
+      <span className="huge">{formatMeasure(measure.value, 'PERCENT')}</span>
+      <span className="label flex-1">
+        {translate('component_measures.facet_category.overall_category.estimated')}
+      </span>
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/IssueLabel.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/IssueLabel.tsx
new file mode 100644 (file)
index 0000000..0ce97d2
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { Link } from 'react-router';
+import * as classNames from 'classnames';
+import { getMetricName, ISSUETYPE_MAP, IssueType } from '../utils';
+import { getLeakValue } from '../../../components/measure/utils';
+import { getBranchLikeQuery } from '../../../helpers/branches';
+import { getComponentIssuesUrl } from '../../../helpers/urls';
+import { formatMeasure, findMeasure } from '../../../helpers/measures';
+
+interface Props {
+  branchLike?: T.ShortLivingBranch | T.PullRequest;
+  className?: string;
+  component: T.Component;
+  measures: T.Measure[];
+  type: IssueType;
+}
+
+export default function IssueLabel({ branchLike, className, component, measures, type }: Props) {
+  const { metric, iconClass } = ISSUETYPE_MAP[type];
+
+  const measure = findMeasure(measures, metric);
+
+  let value;
+  if (measure) {
+    value = getLeakValue(measure);
+  }
+
+  const params = {
+    ...getBranchLikeQuery(branchLike),
+    resolved: 'false',
+    types: type
+  };
+
+  return (
+    <>
+      {value === undefined ? (
+        <span className={classNames(className, 'measure-empty')}>—</span>
+      ) : (
+        <Link className={className} to={getComponentIssuesUrl(component.key, params)}>
+          {formatMeasure(value, 'SHORT_INT')}
+        </Link>
+      )}
+      {React.createElement(iconClass, { className: 'big-spacer-left little-spacer-right' })}
+      {getMetricName(metric)}
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/IssueRating.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/IssueRating.tsx
new file mode 100644 (file)
index 0000000..e3624aa
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import DrilldownLink from '../../../components/shared/DrilldownLink';
+import Rating from '../../../components/ui/Rating';
+import Tooltip from '../../../components/controls/Tooltip';
+import { getRatingName, ISSUETYPE_MAP, IssueType } from '../utils';
+import { getLeakValue, getRatingTooltip } from '../../../components/measure/utils';
+import { findMeasure } from '../../../helpers/measures';
+
+interface Props {
+  branchLike?: T.ShortLivingBranch | T.PullRequest;
+  component: T.Component;
+  measures: T.Measure[];
+  type: IssueType;
+}
+
+export default function IssueRating({ branchLike, component, measures, type }: Props) {
+  const { rating } = ISSUETYPE_MAP[type];
+  const measure = findMeasure(measures, rating);
+
+  if (measure === undefined) {
+    return null;
+  }
+
+  const value = getLeakValue(measure);
+  const tooltip = value && getRatingTooltip(rating, Number(value));
+
+  return (
+    <>
+      <span className="big-spacer-right flex-1">{getRatingName(type)}</span>
+      <Tooltip overlay={tooltip}>
+        <span>
+          <DrilldownLink
+            branchLike={branchLike}
+            className="link-no-underline"
+            component={component.key}
+            metric={rating}>
+            <Rating value={value} />
+          </DrilldownLink>
+        </span>
+      </Tooltip>
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/LargeQualityGateBadge.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/LargeQualityGateBadge.tsx
new file mode 100644 (file)
index 0000000..78bd963
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import { Link } from 'react-router';
+import { FormattedMessage } from 'react-intl';
+import HelpTooltip from '../../../components/controls/HelpTooltip';
+import HelpIcon from '../../../components/icons-components/HelpIcon';
+import { getQualityGateUrl, getQualityGatesUrl } from '../../../helpers/urls';
+import { isSonarCloud } from '../../../helpers/system';
+import { translate } from '../../../helpers/l10n';
+import { transparentWhite } from '../../../app/theme';
+
+interface Props {
+  component: T.Component;
+  level?: string;
+}
+
+export default function LargeQualityGateBadge({ component, level }: Props) {
+  const success = level !== 'ERROR';
+
+  let path;
+  if (isSonarCloud()) {
+    path =
+      component.qualityGate === undefined
+        ? getQualityGatesUrl(component.organization)
+        : getQualityGateUrl(component.qualityGate.key, component.organization);
+  } else {
+    path =
+      component.qualityGate === undefined
+        ? getQualityGatesUrl()
+        : getQualityGateUrl(component.qualityGate.key);
+  }
+
+  return (
+    <div
+      className={classNames('quality-gate-badge-large small', {
+        failed: !success,
+        success
+      })}>
+      <div className="display-flex-center">
+        <span>{translate('overview.on_new_code_long')}</span>
+
+        <HelpTooltip
+          className="little-spacer-left"
+          overlay={
+            <FormattedMessage
+              defaultMessage={translate('overview.quality_gate.conditions_on_new_code')}
+              id="overview.quality_gate.conditions_on_new_code"
+              values={{
+                link: <Link to={path}>{translate('overview.quality_gate')}</Link>
+              }}
+            />
+          }>
+          <HelpIcon fill={transparentWhite} size={12} />
+        </HelpTooltip>
+      </div>
+      {level !== undefined && (
+        <h4 className="huge-spacer-top huge">{translate('metric.level', level)}</h4>
+      )}
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/MeasurementLabel.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/MeasurementLabel.tsx
new file mode 100644 (file)
index 0000000..c2ab6e8
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import DrilldownLink from '../../../components/shared/DrilldownLink';
+import { translate } from '../../../helpers/l10n';
+import { formatMeasure, findMeasure } from '../../../helpers/measures';
+import { getLeakValue } from '../../../components/measure/utils';
+import { MEASUREMENTS_MAP, MeasurementType } from '../utils';
+
+interface Props {
+  branchLike?: T.BranchLike;
+  className?: string;
+  component: T.Component;
+  measures: T.Measure[];
+  type: MeasurementType;
+}
+
+export default class MeasurementLabel extends React.Component<Props> {
+  getLabelText = () => {
+    const { branchLike, component, measures, type } = this.props;
+    const { expandedLabelKey, linesMetric, labelKey } = MEASUREMENTS_MAP[type];
+
+    const measure = findMeasure(measures, linesMetric);
+    if (!measure) {
+      return translate(labelKey);
+    } else {
+      return (
+        <FormattedMessage
+          defaultMessage={translate(expandedLabelKey)}
+          id={expandedLabelKey}
+          values={{
+            count: (
+              <DrilldownLink branchLike={branchLike} component={component.key} metric={linesMetric}>
+                {formatMeasure(getLeakValue(measure), 'SHORT_INT')}
+              </DrilldownLink>
+            )
+          }}
+        />
+      );
+    }
+  };
+
+  render() {
+    const { branchLike, className, component, measures, type } = this.props;
+    const { iconClass, metric } = MEASUREMENTS_MAP[type];
+
+    const measure = findMeasure(measures, metric);
+
+    let value;
+    if (measure) {
+      value = getLeakValue(measure);
+    }
+
+    return (
+      <>
+        {value === undefined ? (
+          <span>—</span>
+        ) : (
+          <>
+            <span className="big-spacer-right">
+              {React.createElement(iconClass, { size: 'big', value: Number(value) })}
+            </span>
+            <DrilldownLink
+              branchLike={branchLike}
+              className={className}
+              component={component.key}
+              metric={metric}>
+              {formatMeasure(value, 'PERCENT')}
+            </DrilldownLink>
+          </>
+        )}
+        <span className="big-spacer-left">{this.getLabelText()}</span>
+      </>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/ReviewApp.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/ReviewApp.tsx
new file mode 100644 (file)
index 0000000..a0e47c2
--- /dev/null
@@ -0,0 +1,194 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import AfterMergeEstimate from './AfterMergeEstimate';
+import LargeQualityGateBadge from './LargeQualityGateBadge';
+import IssueLabel from './IssueLabel';
+import IssueRating from './IssueRating';
+import MeasurementLabel from './MeasurementLabel';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import DocTooltip from '../../../components/docs/DocTooltip';
+import QualityGateConditions from '../qualityGate/QualityGateConditions';
+import { getMeasures } from '../../../api/measures';
+import { getQualityGateProjectStatus } from '../../../api/quality-gates';
+import { PR_METRICS, IssueType, MeasurementType } from '../utils';
+import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branches';
+import { translate } from '../../../helpers/l10n';
+import { extractStatusConditionsFromProjectStatus } from '../../../helpers/qualityGates';
+import '../styles.css';
+
+interface Props {
+  branchLike?: T.PullRequest | T.ShortLivingBranch;
+  component: T.Component;
+}
+
+interface State {
+  conditions: T.QualityGateStatusCondition[];
+  loading: boolean;
+  measures: T.Measure[];
+  status?: string;
+}
+
+export default class ReviewApp extends React.Component<Props, State> {
+  mounted = false;
+
+  state: State = {
+    conditions: [],
+    loading: false,
+    measures: []
+  };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchBranchData();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (
+      this.props.component.key !== prevProps.component.key ||
+      !isSameBranchLike(this.props.branchLike, prevProps.branchLike)
+    ) {
+      this.fetchBranchData();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchBranchData = () => {
+    const { branchLike, component } = this.props;
+
+    this.setState({ loading: true });
+
+    const data = { projectKey: component.key, ...getBranchLikeQuery(branchLike) };
+
+    Promise.all([
+      getMeasures({
+        component: component.key,
+        metricKeys: PR_METRICS.join(),
+        ...getBranchLikeQuery(branchLike)
+      }),
+      getQualityGateProjectStatus(data)
+    ]).then(
+      ([measures, status]) => {
+        if (this.mounted && measures && status) {
+          this.setState({
+            conditions: extractStatusConditionsFromProjectStatus(status),
+            loading: false,
+            measures,
+            status: status.projectStatus.status
+          });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  render() {
+    const { branchLike, component } = this.props;
+    const { loading, measures, conditions, status } = this.state;
+    const erroredConditions = conditions.filter(condition => condition.level === 'ERROR');
+
+    return (
+      <div className="page page-limited">
+        {loading ? (
+          <DeferredSpinner />
+        ) : (
+          <div
+            className={classNames('pr-overview', {
+              'has-conditions': erroredConditions.length > 0
+            })}>
+            <div className="pr-overview-quality-gate big-spacer-right">
+              <h3 className="spacer-bottom small">
+                {translate('overview.quality_gate')}
+                <DocTooltip
+                  className="spacer-left"
+                  doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/quality-gates/project-homepage-quality-gate.md')}
+                />
+              </h3>
+              <LargeQualityGateBadge component={component} level={status} />
+            </div>
+
+            {erroredConditions.length > 0 && (
+              <div className="pr-overview-failed-conditions big-spacer-right">
+                <h3 className="spacer-bottom small">{translate('overview.failed_conditions')}</h3>
+                <QualityGateConditions
+                  branchLike={branchLike}
+                  collapsible={true}
+                  component={component}
+                  conditions={erroredConditions}
+                />
+              </div>
+            )}
+
+            <div className="pr-overview-measurements flex-1">
+              <h3 className="spacer-bottom small">{translate('overview.metrics')}</h3>
+
+              {['BUG', 'VULNERABILITY', 'CODE_SMELL'].map((type: IssueType) => (
+                <div className="pr-overview-measurements-row display-flex-row" key={type}>
+                  <div className="pr-overview-measurements-value flex-1 small display-flex-center">
+                    <IssueLabel
+                      branchLike={branchLike}
+                      className="overview-domain-measure-value"
+                      component={component}
+                      measures={measures}
+                      type={type}
+                    />
+                  </div>
+                  <div className="pr-overview-measurements-rating display-flex-center">
+                    <IssueRating
+                      branchLike={branchLike}
+                      component={component}
+                      measures={measures}
+                      type={type}
+                    />
+                  </div>
+                </div>
+              ))}
+
+              {['COVERAGE', 'DUPLICATION'].map((type: MeasurementType) => (
+                <div className="pr-overview-measurements-row display-flex-row" key={type}>
+                  <div className="pr-overview-measurements-value flex-1 small display-flex-center">
+                    <MeasurementLabel
+                      branchLike={branchLike}
+                      className="overview-domain-measure-value"
+                      component={component}
+                      measures={measures}
+                      type={type}
+                    />
+                  </div>
+                  <div className="pr-overview-measurements-estimate display-flex-center">
+                    <AfterMergeEstimate measures={measures} type={type} />
+                  </div>
+                </div>
+              ))}
+            </div>
+          </div>
+        )}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/AfterMergeEstimate-test.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/AfterMergeEstimate-test.tsx
new file mode 100644 (file)
index 0000000..a08dc88
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import AfterMergeEstimate from '../AfterMergeEstimate';
+import { mockMeasure } from '../../../../helpers/testMocks';
+
+it('should render correctly for coverage', () => {
+  expect(shallowRender({ measures: [mockMeasure({ metric: 'coverage' })] })).toMatchSnapshot();
+});
+
+it('should render correctly for duplications', () => {
+  expect(
+    shallowRender({
+      measures: [mockMeasure({ metric: 'duplicated_lines_density' })],
+      type: 'DUPLICATION'
+    })
+  ).toMatchSnapshot();
+});
+
+it('should render correctly with no value', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+function shallowRender(props = {}) {
+  return shallow(<AfterMergeEstimate measures={[mockMeasure()]} type="COVERAGE" {...props} />);
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/IssueLabel-test.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/IssueLabel-test.tsx
new file mode 100644 (file)
index 0000000..ea36141
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import IssueLabel from '../IssueLabel';
+import { mockComponent, mockPullRequest, mockMeasure } from '../../../../helpers/testMocks';
+
+it('should render correctly for bugs', () => {
+  expect(
+    shallowRender({
+      measures: [mockMeasure({ metric: 'new_bugs' })]
+    })
+  ).toMatchSnapshot();
+});
+
+it('should render correctly for code smells', () => {
+  expect(
+    shallowRender({
+      measures: [mockMeasure({ metric: 'new_code_smells' })],
+      type: 'CODE_SMELL'
+    })
+  ).toMatchSnapshot();
+});
+
+it('should render correctly for vulnerabilities', () => {
+  expect(
+    shallowRender({
+      measures: [mockMeasure({ metric: 'new_vulnerabilities' })],
+      type: 'VULNERABILITY'
+    })
+  ).toMatchSnapshot();
+});
+
+it('should render correctly if no values are present', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+function shallowRender(props = {}) {
+  return shallow(
+    <IssueLabel
+      branchLike={mockPullRequest()}
+      component={mockComponent()}
+      measures={[]}
+      type="BUG"
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/IssueRating-test.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/IssueRating-test.tsx
new file mode 100644 (file)
index 0000000..1e95943
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import IssueRating from '../IssueRating';
+import { mockPullRequest, mockComponent, mockMeasure } from '../../../../helpers/testMocks';
+
+it('should render correctly for bugs', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should render correctly for code smells', () => {
+  expect(shallowRender({ type: 'CODE_SMELL' })).toMatchSnapshot();
+});
+
+it('should render correctly for vulnerabilities', () => {
+  expect(shallowRender({ type: 'VULNERABILITY' })).toMatchSnapshot();
+});
+
+it('should render correctly if no values are present', () => {
+  expect(shallowRender({ measures: [mockMeasure({ metric: 'NONE' })] })).toMatchSnapshot();
+});
+
+function shallowRender(props = {}) {
+  return shallow(
+    <IssueRating
+      branchLike={mockPullRequest()}
+      component={mockComponent()}
+      measures={[
+        mockMeasure({ metric: 'new_reliability_rating' }),
+        mockMeasure({ metric: 'new_maintainability_rating' }),
+        mockMeasure({ metric: 'new_security_rating' })
+      ]}
+      type="BUG"
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/LargeQualityGateBadge-test.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/LargeQualityGateBadge-test.tsx
new file mode 100644 (file)
index 0000000..1a53b0a
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import LargeQualityGateBadge from '../LargeQualityGateBadge';
+import { mockComponent } from '../../../../helpers/testMocks';
+import { isSonarCloud } from '../../../../helpers/system';
+
+jest.mock('../../../../helpers/system', () => ({
+  isSonarCloud: jest.fn()
+}));
+
+it('should render correctly for SQ', () => {
+  (isSonarCloud as jest.Mock).mockReturnValue(false);
+
+  expect(shallowRender()).toMatchSnapshot();
+  expect(shallowRender({ level: 'OK' })).toMatchSnapshot();
+});
+
+it('should render the link correctly for SC', () => {
+  (isSonarCloud as jest.Mock).mockReturnValue(true);
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+function shallowRender(props = {}) {
+  return shallow(<LargeQualityGateBadge component={mockComponent()} level="ERROR" {...props} />);
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/MeasurementLabel-test.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/MeasurementLabel-test.tsx
new file mode 100644 (file)
index 0000000..2e3fd26
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import MeasurementLabel from '../MeasurementLabel';
+import { mockShortLivingBranch, mockComponent, mockMeasure } from '../../../../helpers/testMocks';
+
+it('should render correctly for coverage', () => {
+  expect(shallowRender()).toMatchSnapshot();
+  expect(
+    shallowRender({
+      measures: [
+        mockMeasure({ metric: 'new_coverage' }),
+        mockMeasure({ metric: 'new_lines_to_cover' })
+      ]
+    })
+  ).toMatchSnapshot();
+});
+
+it('should render correctly for duplications', () => {
+  expect(
+    shallowRender({
+      measures: [mockMeasure({ metric: 'new_duplicated_lines_density' })],
+      type: 'DUPLICATION'
+    })
+  ).toMatchSnapshot();
+});
+
+it('should render correctly with no value', () => {
+  expect(shallowRender({ measures: [mockMeasure({ metric: 'NONE' })] })).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<MeasurementLabel['props']> = {}) {
+  return shallow(
+    <MeasurementLabel
+      branchLike={mockShortLivingBranch()}
+      component={mockComponent()}
+      measures={[mockMeasure({ metric: 'new_coverage' })]}
+      type="COVERAGE"
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/ReviewApp-test.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/ReviewApp-test.tsx
new file mode 100644 (file)
index 0000000..53e9cbe
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import ReviewApp from '../ReviewApp';
+import { getMeasures } from '../../../../api/measures';
+import { getQualityGateProjectStatus } from '../../../../api/quality-gates';
+import { mockComponent, mockPullRequest } from '../../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/measures', () => {
+  const { mockMeasure } = getMockHelpers();
+  return {
+    getMeasures: jest
+      .fn()
+      .mockResolvedValue([
+        mockMeasure({ metric: 'new_bugs ' }),
+        mockMeasure({ metric: 'new_vulnerabilities' }),
+        mockMeasure({ metric: 'new_code_smells' })
+      ])
+  };
+});
+
+jest.mock('../../../../api/quality-gates', () => ({
+  getQualityGateProjectStatus: jest.fn()
+}));
+
+beforeEach(() => {
+  jest.clearAllMocks();
+});
+
+it('should render correctly for a passed QG', async () => {
+  const { mockQualityGateProjectStatus } = getMockHelpers();
+  (getQualityGateProjectStatus as jest.Mock).mockResolvedValue(mockQualityGateProjectStatus());
+
+  const wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+
+  expect(wrapper.find('QualityGateConditions').exists()).toBe(false);
+
+  expect(getMeasures).toBeCalled();
+  expect(getQualityGateProjectStatus).toBeCalled();
+});
+
+it('should render correctly for a failed QG', async () => {
+  const { mockQualityGateProjectStatus } = getMockHelpers();
+  (getQualityGateProjectStatus as jest.Mock).mockResolvedValue(
+    mockQualityGateProjectStatus({
+      status: 'ERROR',
+      conditions: [
+        {
+          status: 'OK',
+          metricKey: 'new_bugs',
+          comparator: 'GT',
+          periodIndex: 1,
+          errorThreshold: '1.0',
+          actualValue: '0'
+        },
+        {
+          status: 'ERROR',
+          metricKey: 'new_code_smells',
+          comparator: 'GT',
+          periodIndex: 1,
+          errorThreshold: '1.0',
+          actualValue: '10'
+        }
+      ]
+    })
+  );
+
+  const wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+
+  expect(wrapper.find('QualityGateConditions').exists()).toBe(true);
+});
+
+function getMockHelpers() {
+  // We use this little "force-requiring" instead of an import statement in
+  // order to prevent a hoisting race condition while mocking. If we want to use
+  // a mock helper in a Jest mock, we have to require it like this. Otherwise,
+  // we get errors like:
+  //     ReferenceError: testMocks_1 is not defined
+  return require.requireActual('../../../../helpers/testMocks');
+}
+
+function shallowRender(props: Partial<ReviewApp['props']> = {}) {
+  return shallow(
+    <ReviewApp branchLike={mockPullRequest()} component={mockComponent()} {...props} />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/AfterMergeEstimate-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/AfterMergeEstimate-test.tsx.snap
new file mode 100644 (file)
index 0000000..16a7041
--- /dev/null
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly for coverage 1`] = `
+<Fragment>
+  <span
+    className="huge"
+  >
+    1.0%
+  </span>
+  <span
+    className="label flex-1"
+  >
+    component_measures.facet_category.overall_category.estimated
+  </span>
+</Fragment>
+`;
+
+exports[`should render correctly for duplications 1`] = `
+<Fragment>
+  <span
+    className="huge"
+  >
+    1.0%
+  </span>
+  <span
+    className="label flex-1"
+  >
+    component_measures.facet_category.overall_category.estimated
+  </span>
+</Fragment>
+`;
+
+exports[`should render correctly with no value 1`] = `""`;
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/IssueLabel-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/IssueLabel-test.tsx.snap
new file mode 100644 (file)
index 0000000..9ee718a
--- /dev/null
@@ -0,0 +1,93 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly for bugs 1`] = `
+<Fragment>
+  <Link
+    onlyActiveOnIndex={false}
+    style={Object {}}
+    to={
+      Object {
+        "pathname": "/project/issues",
+        "query": Object {
+          "id": "my-project",
+          "pullRequest": "1001",
+          "resolved": "false",
+          "types": "BUG",
+        },
+      }
+    }
+  >
+    1
+  </Link>
+  <BugIcon
+    className="big-spacer-left little-spacer-right"
+  />
+  overview.metric.new_bugs
+</Fragment>
+`;
+
+exports[`should render correctly for code smells 1`] = `
+<Fragment>
+  <Link
+    onlyActiveOnIndex={false}
+    style={Object {}}
+    to={
+      Object {
+        "pathname": "/project/issues",
+        "query": Object {
+          "id": "my-project",
+          "pullRequest": "1001",
+          "resolved": "false",
+          "types": "CODE_SMELL",
+        },
+      }
+    }
+  >
+    1
+  </Link>
+  <CodeSmellIcon
+    className="big-spacer-left little-spacer-right"
+  />
+  overview.metric.new_code_smells
+</Fragment>
+`;
+
+exports[`should render correctly for vulnerabilities 1`] = `
+<Fragment>
+  <Link
+    onlyActiveOnIndex={false}
+    style={Object {}}
+    to={
+      Object {
+        "pathname": "/project/issues",
+        "query": Object {
+          "id": "my-project",
+          "pullRequest": "1001",
+          "resolved": "false",
+          "types": "VULNERABILITY",
+        },
+      }
+    }
+  >
+    1
+  </Link>
+  <VulnerabilityIcon
+    className="big-spacer-left little-spacer-right"
+  />
+  overview.metric.new_vulnerabilities
+</Fragment>
+`;
+
+exports[`should render correctly if no values are present 1`] = `
+<Fragment>
+  <span
+    className="measure-empty"
+  >
+    â€”
+  </span>
+  <BugIcon
+    className="big-spacer-left little-spacer-right"
+  />
+  overview.metric.new_bugs
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/IssueRating-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/IssueRating-test.tsx.snap
new file mode 100644 (file)
index 0000000..d21f078
--- /dev/null
@@ -0,0 +1,105 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly for bugs 1`] = `
+<Fragment>
+  <span
+    className="big-spacer-right flex-1"
+  >
+    metric_domain.Reliability
+  </span>
+  <Tooltip
+    overlay="metric.reliability_rating.tooltip.A"
+  >
+    <span>
+      <DrilldownLink
+        branchLike={
+          Object {
+            "analysisDate": "2018-01-01",
+            "base": "master",
+            "branch": "feature/foo/bar",
+            "key": "1001",
+            "title": "Foo Bar feature",
+          }
+        }
+        className="link-no-underline"
+        component="my-project"
+        metric="new_reliability_rating"
+      >
+        <Rating
+          value="1.0"
+        />
+      </DrilldownLink>
+    </span>
+  </Tooltip>
+</Fragment>
+`;
+
+exports[`should render correctly for code smells 1`] = `
+<Fragment>
+  <span
+    className="big-spacer-right flex-1"
+  >
+    metric_domain.Maintainability
+  </span>
+  <Tooltip
+    overlay="metric.sqale_rating.tooltip.A.0.0%"
+  >
+    <span>
+      <DrilldownLink
+        branchLike={
+          Object {
+            "analysisDate": "2018-01-01",
+            "base": "master",
+            "branch": "feature/foo/bar",
+            "key": "1001",
+            "title": "Foo Bar feature",
+          }
+        }
+        className="link-no-underline"
+        component="my-project"
+        metric="new_maintainability_rating"
+      >
+        <Rating
+          value="1.0"
+        />
+      </DrilldownLink>
+    </span>
+  </Tooltip>
+</Fragment>
+`;
+
+exports[`should render correctly for vulnerabilities 1`] = `
+<Fragment>
+  <span
+    className="big-spacer-right flex-1"
+  >
+    metric_domain.Security
+  </span>
+  <Tooltip
+    overlay="metric.security_rating.tooltip.A"
+  >
+    <span>
+      <DrilldownLink
+        branchLike={
+          Object {
+            "analysisDate": "2018-01-01",
+            "base": "master",
+            "branch": "feature/foo/bar",
+            "key": "1001",
+            "title": "Foo Bar feature",
+          }
+        }
+        className="link-no-underline"
+        component="my-project"
+        metric="new_security_rating"
+      >
+        <Rating
+          value="1.0"
+        />
+      </DrilldownLink>
+    </span>
+  </Tooltip>
+</Fragment>
+`;
+
+exports[`should render correctly if no values are present 1`] = `""`;
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/LargeQualityGateBadge-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/LargeQualityGateBadge-test.tsx.snap
new file mode 100644 (file)
index 0000000..6fdbae1
--- /dev/null
@@ -0,0 +1,145 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly for SQ 1`] = `
+<div
+  className="quality-gate-badge-large small failed"
+>
+  <div
+    className="display-flex-center"
+  >
+    <span>
+      overview.on_new_code_long
+    </span>
+    <HelpTooltip
+      className="little-spacer-left"
+      overlay={
+        <FormattedMessage
+          defaultMessage="overview.quality_gate.conditions_on_new_code"
+          id="overview.quality_gate.conditions_on_new_code"
+          values={
+            Object {
+              "link": <Link
+                onlyActiveOnIndex={false}
+                style={Object {}}
+                to={
+                  Object {
+                    "pathname": "/quality_gates/show/30",
+                  }
+                }
+              >
+                overview.quality_gate
+              </Link>,
+            }
+          }
+        />
+      }
+    >
+      <HelpIcon
+        fill="rgba(255,255,255,0.62)"
+        size={12}
+      />
+    </HelpTooltip>
+  </div>
+  <h4
+    className="huge-spacer-top huge"
+  >
+    metric.level.ERROR
+  </h4>
+</div>
+`;
+
+exports[`should render correctly for SQ 2`] = `
+<div
+  className="quality-gate-badge-large small success"
+>
+  <div
+    className="display-flex-center"
+  >
+    <span>
+      overview.on_new_code_long
+    </span>
+    <HelpTooltip
+      className="little-spacer-left"
+      overlay={
+        <FormattedMessage
+          defaultMessage="overview.quality_gate.conditions_on_new_code"
+          id="overview.quality_gate.conditions_on_new_code"
+          values={
+            Object {
+              "link": <Link
+                onlyActiveOnIndex={false}
+                style={Object {}}
+                to={
+                  Object {
+                    "pathname": "/quality_gates/show/30",
+                  }
+                }
+              >
+                overview.quality_gate
+              </Link>,
+            }
+          }
+        />
+      }
+    >
+      <HelpIcon
+        fill="rgba(255,255,255,0.62)"
+        size={12}
+      />
+    </HelpTooltip>
+  </div>
+  <h4
+    className="huge-spacer-top huge"
+  >
+    metric.level.OK
+  </h4>
+</div>
+`;
+
+exports[`should render the link correctly for SC 1`] = `
+<div
+  className="quality-gate-badge-large small failed"
+>
+  <div
+    className="display-flex-center"
+  >
+    <span>
+      overview.on_new_code_long
+    </span>
+    <HelpTooltip
+      className="little-spacer-left"
+      overlay={
+        <FormattedMessage
+          defaultMessage="overview.quality_gate.conditions_on_new_code"
+          id="overview.quality_gate.conditions_on_new_code"
+          values={
+            Object {
+              "link": <Link
+                onlyActiveOnIndex={false}
+                style={Object {}}
+                to={
+                  Object {
+                    "pathname": "/organizations/foo/quality_gates/show/30",
+                  }
+                }
+              >
+                overview.quality_gate
+              </Link>,
+            }
+          }
+        />
+      }
+    >
+      <HelpIcon
+        fill="rgba(255,255,255,0.62)"
+        size={12}
+      />
+    </HelpTooltip>
+  </div>
+  <h4
+    className="huge-spacer-top huge"
+  >
+    metric.level.ERROR
+  </h4>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/MeasurementLabel-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/MeasurementLabel-test.tsx.snap
new file mode 100644 (file)
index 0000000..4885a7e
--- /dev/null
@@ -0,0 +1,135 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly for coverage 1`] = `
+<Fragment>
+  <span
+    className="big-spacer-right"
+  >
+    <CoverageRating
+      size="big"
+      value={1}
+    />
+  </span>
+  <DrilldownLink
+    branchLike={
+      Object {
+        "analysisDate": "2018-01-01",
+        "isMain": false,
+        "mergeBranch": "master",
+        "name": "release-1.0",
+        "type": "SHORT",
+      }
+    }
+    component="my-project"
+    metric="new_coverage"
+  >
+    1.0%
+  </DrilldownLink>
+  <span
+    className="big-spacer-left"
+  >
+    overview.metric.coverage
+  </span>
+</Fragment>
+`;
+
+exports[`should render correctly for coverage 2`] = `
+<Fragment>
+  <span
+    className="big-spacer-right"
+  >
+    <CoverageRating
+      size="big"
+      value={1}
+    />
+  </span>
+  <DrilldownLink
+    branchLike={
+      Object {
+        "analysisDate": "2018-01-01",
+        "isMain": false,
+        "mergeBranch": "master",
+        "name": "release-1.0",
+        "type": "SHORT",
+      }
+    }
+    component="my-project"
+    metric="new_coverage"
+  >
+    1.0%
+  </DrilldownLink>
+  <span
+    className="big-spacer-left"
+  >
+    <FormattedMessage
+      defaultMessage="overview.coverage_on_X_lines"
+      id="overview.coverage_on_X_lines"
+      values={
+        Object {
+          "count": <DrilldownLink
+            branchLike={
+              Object {
+                "analysisDate": "2018-01-01",
+                "isMain": false,
+                "mergeBranch": "master",
+                "name": "release-1.0",
+                "type": "SHORT",
+              }
+            }
+            component="my-project"
+            metric="new_lines_to_cover"
+          >
+            1
+          </DrilldownLink>,
+        }
+      }
+    />
+  </span>
+</Fragment>
+`;
+
+exports[`should render correctly for duplications 1`] = `
+<Fragment>
+  <span
+    className="big-spacer-right"
+  >
+    <DuplicationsRating
+      size="big"
+      value={1}
+    />
+  </span>
+  <DrilldownLink
+    branchLike={
+      Object {
+        "analysisDate": "2018-01-01",
+        "isMain": false,
+        "mergeBranch": "master",
+        "name": "release-1.0",
+        "type": "SHORT",
+      }
+    }
+    component="my-project"
+    metric="new_duplicated_lines_density"
+  >
+    1.0%
+  </DrilldownLink>
+  <span
+    className="big-spacer-left"
+  >
+    overview.metric.duplications
+  </span>
+</Fragment>
+`;
+
+exports[`should render correctly with no value 1`] = `
+<Fragment>
+  <span>
+    â€”
+  </span>
+  <span
+    className="big-spacer-left"
+  >
+    overview.metric.coverage
+  </span>
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/ReviewApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/__snapshots__/ReviewApp-test.tsx.snap
new file mode 100644 (file)
index 0000000..248765d
--- /dev/null
@@ -0,0 +1,1687 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly for a failed QG 1`] = `
+<div
+  className="page page-limited"
+>
+  <div
+    className="pr-overview has-conditions"
+  >
+    <div
+      className="pr-overview-quality-gate big-spacer-right"
+    >
+      <h3
+        className="spacer-bottom small"
+      >
+        overview.quality_gate
+        <DocTooltip
+          className="spacer-left"
+          doc={Promise {}}
+        />
+      </h3>
+      <LargeQualityGateBadge
+        component={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "my-project",
+            "name": "MyProject",
+            "organization": "foo",
+            "qualifier": "TRK",
+            "qualityGate": Object {
+              "isDefault": true,
+              "key": "30",
+              "name": "Sonar way",
+            },
+            "qualityProfiles": Array [
+              Object {
+                "deleted": false,
+                "key": "my-qp",
+                "language": "ts",
+                "name": "Sonar way",
+              },
+            ],
+            "tags": Array [],
+          }
+        }
+        level="ERROR"
+      />
+    </div>
+    <div
+      className="pr-overview-failed-conditions big-spacer-right"
+    >
+      <h3
+        className="spacer-bottom small"
+      >
+        overview.failed_conditions
+      </h3>
+      <QualityGateConditions
+        branchLike={
+          Object {
+            "analysisDate": "2018-01-01",
+            "base": "master",
+            "branch": "feature/foo/bar",
+            "key": "1001",
+            "title": "Foo Bar feature",
+          }
+        }
+        collapsible={true}
+        component={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "my-project",
+            "name": "MyProject",
+            "organization": "foo",
+            "qualifier": "TRK",
+            "qualityGate": Object {
+              "isDefault": true,
+              "key": "30",
+              "name": "Sonar way",
+            },
+            "qualityProfiles": Array [
+              Object {
+                "deleted": false,
+                "key": "my-qp",
+                "language": "ts",
+                "name": "Sonar way",
+              },
+            ],
+            "tags": Array [],
+          }
+        }
+        conditions={
+          Array [
+            Object {
+              "actual": "10",
+              "error": "1.0",
+              "level": "ERROR",
+              "metric": "new_code_smells",
+              "op": "GT",
+              "period": 1,
+            },
+          ]
+        }
+      />
+    </div>
+    <div
+      className="pr-overview-measurements flex-1"
+    >
+      <h3
+        className="spacer-bottom small"
+      >
+        overview.metrics
+      </h3>
+      <div
+        className="pr-overview-measurements-row display-flex-row"
+        key="BUG"
+      >
+        <div
+          className="pr-overview-measurements-value flex-1 small display-flex-center"
+        >
+          <IssueLabel
+            branchLike={
+              Object {
+                "analysisDate": "2018-01-01",
+                "base": "master",
+                "branch": "feature/foo/bar",
+                "key": "1001",
+                "title": "Foo Bar feature",
+              }
+            }
+            className="overview-domain-measure-value"
+            component={
+              Object {
+                "breadcrumbs": Array [],
+                "key": "my-project",
+                "name": "MyProject",
+                "organization": "foo",
+                "qualifier": "TRK",
+                "qualityGate": Object {
+                  "isDefault": true,
+                  "key": "30",
+                  "name": "Sonar way",
+                },
+                "qualityProfiles": Array [
+                  Object {
+                    "deleted": false,
+                    "key": "my-qp",
+                    "language": "ts",
+                    "name": "Sonar way",
+                  },
+                ],
+                "tags": Array [],
+              }
+            }
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="BUG"
+          />
+        </div>
+        <div
+          className="pr-overview-measurements-rating display-flex-center"
+        >
+          <IssueRating
+            branchLike={
+              Object {
+                "analysisDate": "2018-01-01",
+                "base": "master",
+                "branch": "feature/foo/bar",
+                "key": "1001",
+                "title": "Foo Bar feature",
+              }
+            }
+            component={
+              Object {
+                "breadcrumbs": Array [],
+                "key": "my-project",
+                "name": "MyProject",
+                "organization": "foo",
+                "qualifier": "TRK",
+                "qualityGate": Object {
+                  "isDefault": true,
+                  "key": "30",
+                  "name": "Sonar way",
+                },
+                "qualityProfiles": Array [
+                  Object {
+                    "deleted": false,
+                    "key": "my-qp",
+                    "language": "ts",
+                    "name": "Sonar way",
+                  },
+                ],
+                "tags": Array [],
+              }
+            }
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="BUG"
+          />
+        </div>
+      </div>
+      <div
+        className="pr-overview-measurements-row display-flex-row"
+        key="VULNERABILITY"
+      >
+        <div
+          className="pr-overview-measurements-value flex-1 small display-flex-center"
+        >
+          <IssueLabel
+            branchLike={
+              Object {
+                "analysisDate": "2018-01-01",
+                "base": "master",
+                "branch": "feature/foo/bar",
+                "key": "1001",
+                "title": "Foo Bar feature",
+              }
+            }
+            className="overview-domain-measure-value"
+            component={
+              Object {
+                "breadcrumbs": Array [],
+                "key": "my-project",
+                "name": "MyProject",
+                "organization": "foo",
+                "qualifier": "TRK",
+                "qualityGate": Object {
+                  "isDefault": true,
+                  "key": "30",
+                  "name": "Sonar way",
+                },
+                "qualityProfiles": Array [
+                  Object {
+                    "deleted": false,
+                    "key": "my-qp",
+                    "language": "ts",
+                    "name": "Sonar way",
+                  },
+                ],
+                "tags": Array [],
+              }
+            }
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="VULNERABILITY"
+          />
+        </div>
+        <div
+          className="pr-overview-measurements-rating display-flex-center"
+        >
+          <IssueRating
+            branchLike={
+              Object {
+                "analysisDate": "2018-01-01",
+                "base": "master",
+                "branch": "feature/foo/bar",
+                "key": "1001",
+                "title": "Foo Bar feature",
+              }
+            }
+            component={
+              Object {
+                "breadcrumbs": Array [],
+                "key": "my-project",
+                "name": "MyProject",
+                "organization": "foo",
+                "qualifier": "TRK",
+                "qualityGate": Object {
+                  "isDefault": true,
+                  "key": "30",
+                  "name": "Sonar way",
+                },
+                "qualityProfiles": Array [
+                  Object {
+                    "deleted": false,
+                    "key": "my-qp",
+                    "language": "ts",
+                    "name": "Sonar way",
+                  },
+                ],
+                "tags": Array [],
+              }
+            }
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="VULNERABILITY"
+          />
+        </div>
+      </div>
+      <div
+        className="pr-overview-measurements-row display-flex-row"
+        key="CODE_SMELL"
+      >
+        <div
+          className="pr-overview-measurements-value flex-1 small display-flex-center"
+        >
+          <IssueLabel
+            branchLike={
+              Object {
+                "analysisDate": "2018-01-01",
+                "base": "master",
+                "branch": "feature/foo/bar",
+                "key": "1001",
+                "title": "Foo Bar feature",
+              }
+            }
+            className="overview-domain-measure-value"
+            component={
+              Object {
+                "breadcrumbs": Array [],
+                "key": "my-project",
+                "name": "MyProject",
+                "organization": "foo",
+                "qualifier": "TRK",
+                "qualityGate": Object {
+                  "isDefault": true,
+                  "key": "30",
+                  "name": "Sonar way",
+                },
+                "qualityProfiles": Array [
+                  Object {
+                    "deleted": false,
+                    "key": "my-qp",
+                    "language": "ts",
+                    "name": "Sonar way",
+                  },
+                ],
+                "tags": Array [],
+              }
+            }
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="CODE_SMELL"
+          />
+        </div>
+        <div
+          className="pr-overview-measurements-rating display-flex-center"
+        >
+          <IssueRating
+            branchLike={
+              Object {
+                "analysisDate": "2018-01-01",
+                "base": "master",
+                "branch": "feature/foo/bar",
+                "key": "1001",
+                "title": "Foo Bar feature",
+              }
+            }
+            component={
+              Object {
+                "breadcrumbs": Array [],
+                "key": "my-project",
+                "name": "MyProject",
+                "organization": "foo",
+                "qualifier": "TRK",
+                "qualityGate": Object {
+                  "isDefault": true,
+                  "key": "30",
+                  "name": "Sonar way",
+                },
+                "qualityProfiles": Array [
+                  Object {
+                    "deleted": false,
+                    "key": "my-qp",
+                    "language": "ts",
+                    "name": "Sonar way",
+                  },
+                ],
+                "tags": Array [],
+              }
+            }
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="CODE_SMELL"
+          />
+        </div>
+      </div>
+      <div
+        className="pr-overview-measurements-row display-flex-row"
+        key="COVERAGE"
+      >
+        <div
+          className="pr-overview-measurements-value flex-1 small display-flex-center"
+        >
+          <MeasurementLabel
+            branchLike={
+              Object {
+                "analysisDate": "2018-01-01",
+                "base": "master",
+                "branch": "feature/foo/bar",
+                "key": "1001",
+                "title": "Foo Bar feature",
+              }
+            }
+            className="overview-domain-measure-value"
+            component={
+              Object {
+                "breadcrumbs": Array [],
+                "key": "my-project",
+                "name": "MyProject",
+                "organization": "foo",
+                "qualifier": "TRK",
+                "qualityGate": Object {
+                  "isDefault": true,
+                  "key": "30",
+                  "name": "Sonar way",
+                },
+                "qualityProfiles": Array [
+                  Object {
+                    "deleted": false,
+                    "key": "my-qp",
+                    "language": "ts",
+                    "name": "Sonar way",
+                  },
+                ],
+                "tags": Array [],
+              }
+            }
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="COVERAGE"
+          />
+        </div>
+        <div
+          className="pr-overview-measurements-estimate display-flex-center"
+        >
+          <AfterMergeEstimate
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="COVERAGE"
+          />
+        </div>
+      </div>
+      <div
+        className="pr-overview-measurements-row display-flex-row"
+        key="DUPLICATION"
+      >
+        <div
+          className="pr-overview-measurements-value flex-1 small display-flex-center"
+        >
+          <MeasurementLabel
+            branchLike={
+              Object {
+                "analysisDate": "2018-01-01",
+                "base": "master",
+                "branch": "feature/foo/bar",
+                "key": "1001",
+                "title": "Foo Bar feature",
+              }
+            }
+            className="overview-domain-measure-value"
+            component={
+              Object {
+                "breadcrumbs": Array [],
+                "key": "my-project",
+                "name": "MyProject",
+                "organization": "foo",
+                "qualifier": "TRK",
+                "qualityGate": Object {
+                  "isDefault": true,
+                  "key": "30",
+                  "name": "Sonar way",
+                },
+                "qualityProfiles": Array [
+                  Object {
+                    "deleted": false,
+                    "key": "my-qp",
+                    "language": "ts",
+                    "name": "Sonar way",
+                  },
+                ],
+                "tags": Array [],
+              }
+            }
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="DUPLICATION"
+          />
+        </div>
+        <div
+          className="pr-overview-measurements-estimate display-flex-center"
+        >
+          <AfterMergeEstimate
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="DUPLICATION"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correctly for a passed QG 1`] = `
+<div
+  className="page page-limited"
+>
+  <div
+    className="pr-overview"
+  >
+    <div
+      className="pr-overview-quality-gate big-spacer-right"
+    >
+      <h3
+        className="spacer-bottom small"
+      >
+        overview.quality_gate
+        <DocTooltip
+          className="spacer-left"
+          doc={Promise {}}
+        />
+      </h3>
+      <LargeQualityGateBadge
+        component={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "my-project",
+            "name": "MyProject",
+            "organization": "foo",
+            "qualifier": "TRK",
+            "qualityGate": Object {
+              "isDefault": true,
+              "key": "30",
+              "name": "Sonar way",
+            },
+            "qualityProfiles": Array [
+              Object {
+                "deleted": false,
+                "key": "my-qp",
+                "language": "ts",
+                "name": "Sonar way",
+              },
+            ],
+            "tags": Array [],
+          }
+        }
+        level="OK"
+      />
+    </div>
+    <div
+      className="pr-overview-measurements flex-1"
+    >
+      <h3
+        className="spacer-bottom small"
+      >
+        overview.metrics
+      </h3>
+      <div
+        className="pr-overview-measurements-row display-flex-row"
+        key="BUG"
+      >
+        <div
+          className="pr-overview-measurements-value flex-1 small display-flex-center"
+        >
+          <IssueLabel
+            branchLike={
+              Object {
+                "analysisDate": "2018-01-01",
+                "base": "master",
+                "branch": "feature/foo/bar",
+                "key": "1001",
+                "title": "Foo Bar feature",
+              }
+            }
+            className="overview-domain-measure-value"
+            component={
+              Object {
+                "breadcrumbs": Array [],
+                "key": "my-project",
+                "name": "MyProject",
+                "organization": "foo",
+                "qualifier": "TRK",
+                "qualityGate": Object {
+                  "isDefault": true,
+                  "key": "30",
+                  "name": "Sonar way",
+                },
+                "qualityProfiles": Array [
+                  Object {
+                    "deleted": false,
+                    "key": "my-qp",
+                    "language": "ts",
+                    "name": "Sonar way",
+                  },
+                ],
+                "tags": Array [],
+              }
+            }
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="BUG"
+          />
+        </div>
+        <div
+          className="pr-overview-measurements-rating display-flex-center"
+        >
+          <IssueRating
+            branchLike={
+              Object {
+                "analysisDate": "2018-01-01",
+                "base": "master",
+                "branch": "feature/foo/bar",
+                "key": "1001",
+                "title": "Foo Bar feature",
+              }
+            }
+            component={
+              Object {
+                "breadcrumbs": Array [],
+                "key": "my-project",
+                "name": "MyProject",
+                "organization": "foo",
+                "qualifier": "TRK",
+                "qualityGate": Object {
+                  "isDefault": true,
+                  "key": "30",
+                  "name": "Sonar way",
+                },
+                "qualityProfiles": Array [
+                  Object {
+                    "deleted": false,
+                    "key": "my-qp",
+                    "language": "ts",
+                    "name": "Sonar way",
+                  },
+                ],
+                "tags": Array [],
+              }
+            }
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="BUG"
+          />
+        </div>
+      </div>
+      <div
+        className="pr-overview-measurements-row display-flex-row"
+        key="VULNERABILITY"
+      >
+        <div
+          className="pr-overview-measurements-value flex-1 small display-flex-center"
+        >
+          <IssueLabel
+            branchLike={
+              Object {
+                "analysisDate": "2018-01-01",
+                "base": "master",
+                "branch": "feature/foo/bar",
+                "key": "1001",
+                "title": "Foo Bar feature",
+              }
+            }
+            className="overview-domain-measure-value"
+            component={
+              Object {
+                "breadcrumbs": Array [],
+                "key": "my-project",
+                "name": "MyProject",
+                "organization": "foo",
+                "qualifier": "TRK",
+                "qualityGate": Object {
+                  "isDefault": true,
+                  "key": "30",
+                  "name": "Sonar way",
+                },
+                "qualityProfiles": Array [
+                  Object {
+                    "deleted": false,
+                    "key": "my-qp",
+                    "language": "ts",
+                    "name": "Sonar way",
+                  },
+                ],
+                "tags": Array [],
+              }
+            }
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="VULNERABILITY"
+          />
+        </div>
+        <div
+          className="pr-overview-measurements-rating display-flex-center"
+        >
+          <IssueRating
+            branchLike={
+              Object {
+                "analysisDate": "2018-01-01",
+                "base": "master",
+                "branch": "feature/foo/bar",
+                "key": "1001",
+                "title": "Foo Bar feature",
+              }
+            }
+            component={
+              Object {
+                "breadcrumbs": Array [],
+                "key": "my-project",
+                "name": "MyProject",
+                "organization": "foo",
+                "qualifier": "TRK",
+                "qualityGate": Object {
+                  "isDefault": true,
+                  "key": "30",
+                  "name": "Sonar way",
+                },
+                "qualityProfiles": Array [
+                  Object {
+                    "deleted": false,
+                    "key": "my-qp",
+                    "language": "ts",
+                    "name": "Sonar way",
+                  },
+                ],
+                "tags": Array [],
+              }
+            }
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="VULNERABILITY"
+          />
+        </div>
+      </div>
+      <div
+        className="pr-overview-measurements-row display-flex-row"
+        key="CODE_SMELL"
+      >
+        <div
+          className="pr-overview-measurements-value flex-1 small display-flex-center"
+        >
+          <IssueLabel
+            branchLike={
+              Object {
+                "analysisDate": "2018-01-01",
+                "base": "master",
+                "branch": "feature/foo/bar",
+                "key": "1001",
+                "title": "Foo Bar feature",
+              }
+            }
+            className="overview-domain-measure-value"
+            component={
+              Object {
+                "breadcrumbs": Array [],
+                "key": "my-project",
+                "name": "MyProject",
+                "organization": "foo",
+                "qualifier": "TRK",
+                "qualityGate": Object {
+                  "isDefault": true,
+                  "key": "30",
+                  "name": "Sonar way",
+                },
+                "qualityProfiles": Array [
+                  Object {
+                    "deleted": false,
+                    "key": "my-qp",
+                    "language": "ts",
+                    "name": "Sonar way",
+                  },
+                ],
+                "tags": Array [],
+              }
+            }
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="CODE_SMELL"
+          />
+        </div>
+        <div
+          className="pr-overview-measurements-rating display-flex-center"
+        >
+          <IssueRating
+            branchLike={
+              Object {
+                "analysisDate": "2018-01-01",
+                "base": "master",
+                "branch": "feature/foo/bar",
+                "key": "1001",
+                "title": "Foo Bar feature",
+              }
+            }
+            component={
+              Object {
+                "breadcrumbs": Array [],
+                "key": "my-project",
+                "name": "MyProject",
+                "organization": "foo",
+                "qualifier": "TRK",
+                "qualityGate": Object {
+                  "isDefault": true,
+                  "key": "30",
+                  "name": "Sonar way",
+                },
+                "qualityProfiles": Array [
+                  Object {
+                    "deleted": false,
+                    "key": "my-qp",
+                    "language": "ts",
+                    "name": "Sonar way",
+                  },
+                ],
+                "tags": Array [],
+              }
+            }
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="CODE_SMELL"
+          />
+        </div>
+      </div>
+      <div
+        className="pr-overview-measurements-row display-flex-row"
+        key="COVERAGE"
+      >
+        <div
+          className="pr-overview-measurements-value flex-1 small display-flex-center"
+        >
+          <MeasurementLabel
+            branchLike={
+              Object {
+                "analysisDate": "2018-01-01",
+                "base": "master",
+                "branch": "feature/foo/bar",
+                "key": "1001",
+                "title": "Foo Bar feature",
+              }
+            }
+            className="overview-domain-measure-value"
+            component={
+              Object {
+                "breadcrumbs": Array [],
+                "key": "my-project",
+                "name": "MyProject",
+                "organization": "foo",
+                "qualifier": "TRK",
+                "qualityGate": Object {
+                  "isDefault": true,
+                  "key": "30",
+                  "name": "Sonar way",
+                },
+                "qualityProfiles": Array [
+                  Object {
+                    "deleted": false,
+                    "key": "my-qp",
+                    "language": "ts",
+                    "name": "Sonar way",
+                  },
+                ],
+                "tags": Array [],
+              }
+            }
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="COVERAGE"
+          />
+        </div>
+        <div
+          className="pr-overview-measurements-estimate display-flex-center"
+        >
+          <AfterMergeEstimate
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="COVERAGE"
+          />
+        </div>
+      </div>
+      <div
+        className="pr-overview-measurements-row display-flex-row"
+        key="DUPLICATION"
+      >
+        <div
+          className="pr-overview-measurements-value flex-1 small display-flex-center"
+        >
+          <MeasurementLabel
+            branchLike={
+              Object {
+                "analysisDate": "2018-01-01",
+                "base": "master",
+                "branch": "feature/foo/bar",
+                "key": "1001",
+                "title": "Foo Bar feature",
+              }
+            }
+            className="overview-domain-measure-value"
+            component={
+              Object {
+                "breadcrumbs": Array [],
+                "key": "my-project",
+                "name": "MyProject",
+                "organization": "foo",
+                "qualifier": "TRK",
+                "qualityGate": Object {
+                  "isDefault": true,
+                  "key": "30",
+                  "name": "Sonar way",
+                },
+                "qualityProfiles": Array [
+                  Object {
+                    "deleted": false,
+                    "key": "my-qp",
+                    "language": "ts",
+                    "name": "Sonar way",
+                  },
+                ],
+                "tags": Array [],
+              }
+            }
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="DUPLICATION"
+          />
+        </div>
+        <div
+          className="pr-overview-measurements-estimate display-flex-center"
+        >
+          <AfterMergeEstimate
+            measures={
+              Array [
+                Object {
+                  "bestValue": true,
+                  "metric": "new_bugs ",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_vulnerabilities",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+                Object {
+                  "bestValue": true,
+                  "metric": "new_code_smells",
+                  "periods": Array [
+                    Object {
+                      "bestValue": true,
+                      "index": 1,
+                      "value": "1.0",
+                    },
+                  ],
+                  "value": "1.0",
+                },
+              ]
+            }
+            type="DUPLICATION"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
index b2e35b2a0a817491fd859cf4ce8cd575b57e5863..693d5739e1c7e38929b11e0aea09077fbe6c52d6 100644 (file)
 import * as React from 'react';
 import * as classNames from 'classnames';
 import { Link } from 'react-router';
-import { QualityGateStatusConditionEnhanced } from '../utils';
 import DrilldownLink from '../../../components/shared/DrilldownLink';
 import Measure from '../../../components/measure/Measure';
 import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
 import { getPeriodValue, isDiffMetric, formatMeasure } from '../../../helpers/measures';
 import { translate } from '../../../helpers/l10n';
 import { getComponentIssuesUrl } from '../../../helpers/urls';
-import { getBranchLikeQuery } from '../../../helpers/branches';
+import { getBranchLikeQuery, isPullRequest, isShortLivingBranch } from '../../../helpers/branches';
 
 interface Props {
   branchLike?: T.BranchLike;
   component: Pick<T.Component, 'key'>;
-  condition: QualityGateStatusConditionEnhanced;
+  condition: T.QualityGateStatusConditionEnhanced;
 }
 
 export default class QualityGateCondition extends React.PureComponent<Props> {
@@ -91,7 +90,10 @@ export default class QualityGateCondition extends React.PureComponent<Props> {
     const className = classNames(
       'overview-quality-gate-condition',
       'overview-quality-gate-condition-' + condition.level.toLowerCase(),
-      { 'overview-quality-gate-condition-leak': condition.period != null }
+      {
+        'overview-quality-gate-condition-leak':
+          condition.period != null && !isPullRequest(branchLike) && !isShortLivingBranch(branchLike)
+      }
     );
 
     const metricKey = condition.measure.metric.key;
index 1ac75d2aa4d57f339b616d8b4134c56d75713ecc..5f2d9e5d294009843c017b75300f4e3e18c854d5 100644 (file)
 import * as React from 'react';
 import { sortBy } from 'lodash';
 import QualityGateCondition from './QualityGateCondition';
-import { QualityGateStatusCondition, QualityGateStatusConditionEnhanced } from '../utils';
+import ChevronDownIcon from '../../../components/icons-components/ChevronDownIcon';
+import { ButtonLink } from '../../../components/ui/buttons';
 import { getMeasuresAndMeta } from '../../../api/measures';
 import { enhanceMeasuresWithMetrics } from '../../../helpers/measures';
 import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches';
+import { translateWithParameters } from '../../../helpers/l10n';
 
 const LEVEL_ORDER = ['ERROR', 'WARN'];
 
 interface Props {
   branchLike?: T.BranchLike;
   component: Pick<T.Component, 'key'>;
-  conditions: QualityGateStatusCondition[];
+  collapsible?: boolean;
+  conditions: T.QualityGateStatusCondition[];
 }
 
 interface State {
-  conditions?: QualityGateStatusConditionEnhanced[];
+  collapsed: boolean;
+  conditions?: T.QualityGateStatusConditionEnhanced[];
   loading: boolean;
 }
 
+const MAX_CONDITIONS = 5;
+
 export default class QualityGateConditions extends React.PureComponent<Props, State> {
   mounted = false;
-  state: State = {
-    loading: true
-  };
+  state: State;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      collapsed: Boolean(props.collapsible),
+      loading: true
+    };
+  }
 
   componentDidMount() {
     this.mounted = true;
@@ -63,6 +75,16 @@ export default class QualityGateConditions extends React.PureComponent<Props, St
     this.mounted = false;
   }
 
+  enhanceConditions = (
+    conditions: T.QualityGateStatusCondition[],
+    measures: T.MeasureEnhanced[]
+  ): T.QualityGateStatusConditionEnhanced[] => {
+    return conditions.map(condition => {
+      const measure = measures.find(measure => measure.metric.key === condition.metric)!;
+      return { ...condition, measure };
+    });
+  };
+
   loadFailedMeasures() {
     const { branchLike, component, conditions } = this.props;
     const failedConditions = conditions.filter(c => c.level !== 'OK');
@@ -76,7 +98,7 @@ export default class QualityGateConditions extends React.PureComponent<Props, St
           if (this.mounted) {
             const measures = enhanceMeasuresWithMetrics(component.measures || [], metrics || []);
             this.setState({
-              conditions: enhanceConditions(failedConditions, measures),
+              conditions: this.enhanceConditions(failedConditions, measures),
               loading: false
             });
           }
@@ -92,25 +114,37 @@ export default class QualityGateConditions extends React.PureComponent<Props, St
     }
   }
 
+  handleToggleCollapse = () => {
+    this.setState(state => ({
+      collapsed: !state.collapsed
+    }));
+  };
+
   render() {
     const { branchLike, component } = this.props;
-    const { loading, conditions } = this.state;
+    const { loading, collapsed, conditions } = this.state;
 
     if (loading || !conditions) {
       return null;
     }
 
-    const sortedConditions = sortBy(
-      conditions,
-      condition => LEVEL_ORDER.indexOf(condition.level),
-      condition => condition.measure.metric.name
-    );
+    const sortedConditions = sortBy(conditions, condition => LEVEL_ORDER.indexOf(condition.level));
+
+    let renderConditions;
+    let renderCollapsed;
+    if (collapsed && sortedConditions.length > MAX_CONDITIONS) {
+      renderConditions = sortedConditions.slice(0, MAX_CONDITIONS);
+      renderCollapsed = true;
+    } else {
+      renderConditions = sortedConditions;
+      renderCollapsed = false;
+    }
 
     return (
       <div
         className="overview-quality-gate-conditions-list clearfix"
         id="overview-quality-gate-conditions-list">
-        {sortedConditions.map(condition => (
+        {renderConditions.map(condition => (
           <QualityGateCondition
             branchLike={branchLike}
             component={component}
@@ -118,17 +152,18 @@ export default class QualityGateConditions extends React.PureComponent<Props, St
             key={condition.measure.metric.key}
           />
         ))}
+        {renderCollapsed && (
+          <ButtonLink
+            className="overview-quality-gate-conditions-list-collapse"
+            onClick={this.handleToggleCollapse}>
+            {translateWithParameters(
+              'overview.X_more_failed_conditions',
+              sortedConditions.length - MAX_CONDITIONS
+            )}
+            <ChevronDownIcon className="little-spacer-left" />
+          </ButtonLink>
+        )}
       </div>
     );
   }
 }
-
-function enhanceConditions(
-  conditions: QualityGateStatusCondition[],
-  measures: T.MeasureEnhanced[]
-): QualityGateStatusConditionEnhanced[] {
-  return conditions.map(condition => {
-    const measure = measures.find(measure => measure.metric.key === condition.metric)!;
-    return { ...condition, measure };
-  });
-}
index 2506cf58be5fbd91e05383f4851e4258e31892da..1e8994fe8552100f742b51498ab9919da549d406 100644 (file)
@@ -20,9 +20,8 @@
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import QualityGateCondition from '../QualityGateCondition';
-import { QualityGateStatusConditionEnhanced } from '../../utils';
 
-const mockRatingCondition = (metric: string): QualityGateStatusConditionEnhanced => ({
+const mockRatingCondition = (metric: string): T.QualityGateStatusConditionEnhanced => ({
   error: '1',
   level: 'ERROR',
   measure: {
@@ -41,7 +40,7 @@ const mockRatingCondition = (metric: string): QualityGateStatusConditionEnhanced
 const periods = [{ value: '3', index: 1 }];
 
 it('open_issues', () => {
-  const condition: QualityGateStatusConditionEnhanced = {
+  const condition: T.QualityGateStatusConditionEnhanced = {
     error: '0',
     level: 'ERROR',
     measure: {
@@ -62,7 +61,7 @@ it('open_issues', () => {
 });
 
 it('new_open_issues', () => {
-  const condition: QualityGateStatusConditionEnhanced = {
+  const condition: T.QualityGateStatusConditionEnhanced = {
     error: '0',
     level: 'ERROR',
     measure: {
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/QualityGateConditions-test.tsx b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/QualityGateConditions-test.tsx
new file mode 100644 (file)
index 0000000..4f82a0f
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import QualityGateConditions from '../QualityGateConditions';
+import { getMeasuresAndMeta } from '../../../../api/measures';
+import { mockComponent, mockQualityGateStatusCondition } from '../../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/measures', () => {
+  return {
+    getMeasuresAndMeta: jest.fn().mockResolvedValue({
+      component: { measures: [{ metric: 'foo' }] },
+      metrics: [{ key: 'foo' }]
+    })
+  };
+});
+
+it('should render correctly', async () => {
+  const wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+  expect(getMeasuresAndMeta).toBeCalled();
+  expect(wrapper.find('QualityGateCondition').length).toBe(10);
+});
+
+it('should be collapsible', async () => {
+  const wrapper = shallowRender({ collapsible: true });
+  await waitAndUpdate(wrapper);
+  expect(wrapper.find('QualityGateCondition').length).toBe(5);
+  wrapper.setState({ collapsed: false });
+  await waitAndUpdate(wrapper);
+  expect(wrapper.find('QualityGateCondition').length).toBe(10);
+});
+
+function shallowRender(props: Partial<QualityGateConditions['props']> = {}) {
+  const conditions: T.QualityGateStatusCondition[] = [];
+  for (let i = 10; i > 0; --i) {
+    conditions.push(mockQualityGateStatusCondition());
+  }
+
+  return shallow(
+    <QualityGateConditions component={mockComponent()} conditions={conditions} {...props} />
+  );
+}
index bc8b59a60c7f258e9aa4944c59d69683fe2019bd..4ce5c0b5c709d8774037c2e475540d555f43ed38 100644 (file)
   align-items: flex-start;
 }
 
+.overview-quality-gate-conditions-list-collapse {
+  margin: calc(2 * var(--gridSize)) 0;
+}
+
 .overview-quality-gate-condition {
   display: block;
   margin-top: 15px;
     opacity: 1;
   }
 }
+
+/*
+ * PRs and SLBs
+ */
+.pr-overview {
+  display: flex;
+  max-width: 960px;
+  margin: 0 auto;
+}
+
+.pr-overview.has-conditions {
+  max-width: 1180px;
+}
+
+.pr-overview h3 {
+  text-transform: uppercase;
+}
+
+.pr-overview-failed-conditions {
+  flex: 0 0 240px;
+}
+
+.pr-overview .overview-quality-gate-condition:first-of-type {
+  margin-top: 0;
+}
+
+.pr-overview .overview-quality-gate-condition {
+  margin-right: 0;
+  margin-top: 12px;
+  box-sizing: border-box;
+  width: 100%;
+}
+
+.pr-overview-measurements-row {
+  min-height: 85px;
+  box-sizing: border-box;
+  background: white;
+  border: 1px solid var(--barBorderColor);
+}
+
+.pr-overview-measurements-row + .pr-overview-measurements-row {
+  border-top: 0;
+}
+
+.pr-overview-measurements-value,
+.pr-overview-measurements-rating,
+.pr-overview-measurements-estimate {
+  padding: calc(3 * var(--gridSize));
+}
+
+.pr-overview-measurements-value .measure-empty {
+  margin-top: -4px;
+}
+
+.pr-overview-measurements-rating,
+.pr-overview-measurements-estimate {
+  flex-basis: 270px;
+  text-align: right;
+  box-sizing: border-box;
+}
+
+@media (max-width: 1200px) {
+  .pr-overview-measurements-rating,
+  .pr-overview-measurements-estimate {
+    flex-basis: 220px;
+  }
+}
+
+.pr-overview-measurements-estimate {
+  background: var(--veryLightGreen);
+}
+
+.pr-overview-measurements-estimate .label {
+  margin-left: var(--gridSize);
+  text-align: right;
+}
+
+.quality-gate-badge-large {
+  width: 240px;
+  min-height: 160px;
+  padding: calc(2 * var(--gridSize));
+  color: var(--transparentWhite);
+  box-sizing: border-box;
+}
+
+.quality-gate-badge-large.failed {
+  background: var(--red);
+}
+
+.quality-gate-badge-large.success {
+  background: var(--green);
+}
+
+.quality-gate-badge-large h4 {
+  color: white;
+}
index 72efabf901c5c4df13a39983f3cc4e59512b47b4..9a490f6098aeba9c98d3a3bc5f4ebeb697a93d51 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { translate } from '../../helpers/l10n';
-
-export interface QualityGateStatusCondition {
-  actual?: string;
-  error?: string;
-  level: string;
-  metric: string;
-  op: string;
-  period?: number;
-  warning?: string;
-}
-
-// long name to not mix with Condition from /app/types.ts
-export interface QualityGateStatusConditionEnhanced extends QualityGateStatusCondition {
-  measure: T.MeasureEnhanced;
-}
+import CodeSmellIcon from '../../components/icons-components/CodeSmellIcon';
+import VulnerabilityIcon from '../../components/icons-components/VulnerabilityIcon';
+import BugIcon from '../../components/icons-components/BugIcon';
+import CoverageRating from '../../components/ui/CoverageRating';
+import DuplicationsRating from '../../components/ui/DuplicationsRating';
 
 export const METRICS = [
   // quality gate
@@ -77,6 +67,22 @@ export const METRICS = [
   'new_lines'
 ];
 
+export const PR_METRICS = [
+  'coverage',
+  'new_coverage',
+  'new_lines_to_cover',
+
+  'duplicated_lines_density',
+  'new_duplicated_lines_density',
+  'new_lines',
+  'new_code_smells',
+  'new_maintainability_rating',
+  'new_bugs',
+  'new_reliability_rating',
+  'new_vulnerabilities',
+  'new_security_rating'
+];
+
 export const HISTORY_METRICS_LIST = [
   'sqale_index',
   'duplicated_lines_density',
@@ -84,6 +90,54 @@ export const HISTORY_METRICS_LIST = [
   'coverage'
 ];
 
+export type MeasurementType = 'COVERAGE' | 'DUPLICATION';
+
+export const MEASUREMENTS_MAP = {
+  COVERAGE: {
+    metric: 'new_coverage',
+    linesMetric: 'new_lines_to_cover',
+    afterMergeMetric: 'coverage',
+    labelKey: 'overview.metric.coverage',
+    expandedLabelKey: 'overview.coverage_on_X_lines',
+    iconClass: CoverageRating
+  },
+  DUPLICATION: {
+    metric: 'new_duplicated_lines_density',
+    linesMetric: 'new_lines',
+    afterMergeMetric: 'duplicated_lines_density',
+    labelKey: 'overview.metric.duplications',
+    expandedLabelKey: 'overview.duplications_on_X',
+    iconClass: DuplicationsRating
+  }
+};
+
+export type IssueType = 'CODE_SMELL' | 'VULNERABILITY' | 'BUG';
+
+export const ISSUETYPE_MAP = {
+  CODE_SMELL: {
+    metric: 'new_code_smells',
+    rating: 'new_maintainability_rating',
+    ratingName: 'Maintainability',
+    iconClass: CodeSmellIcon
+  },
+  VULNERABILITY: {
+    metric: 'new_vulnerabilities',
+    rating: 'new_security_rating',
+    ratingName: 'Security',
+    iconClass: VulnerabilityIcon
+  },
+  BUG: {
+    metric: 'new_bugs',
+    rating: 'new_reliability_rating',
+    ratingName: 'Reliability',
+    iconClass: BugIcon
+  }
+};
+
 export function getMetricName(metricKey: string) {
   return translate('overview.metric', metricKey);
 }
+
+export function getRatingName(type: IssueType) {
+  return translate('metric_domain', ISSUETYPE_MAP[type].ratingName);
+}
index 3fe6740c037f600f4b35e37d2f18a72d874a6f7a..94cea5cdf1bbc0c28b5adcc01e1175a1087b6725 100644 (file)
@@ -28,7 +28,6 @@ exports[`should render source file 1`] = `
               "query": Object {
                 "branch": "feature",
                 "id": "project-key",
-                "resolved": "false",
               },
             }
           }
@@ -387,7 +386,6 @@ exports[`should render source file 2`] = `
               "query": Object {
                 "branch": "feature",
                 "id": "project-key",
-                "resolved": "false",
               },
             }
           }
@@ -1352,7 +1350,6 @@ exports[`should render test file 1`] = `
               "query": Object {
                 "branch": "feature",
                 "id": "project-key",
-                "resolved": "false",
               },
             }
           }
diff --git a/server/sonar-web/src/main/js/components/common/BranchStatus.css b/server/sonar-web/src/main/js/components/common/BranchStatus.css
deleted file mode 100644 (file)
index f4e61b4..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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.
- */
-.branch-status {
-  display: inline-flex;
-  justify-content: flex-end;
-  align-items: center;
-  min-width: 64px;
-  line-height: calc(2 * var(--gridSize));
-  text-align: right;
-}
-
-.branch-status .status-indicator {
-  margin: 0;
-}
index 6345b0de276d65b85c468a904fc218bd29ac4726..e0f712a68a7d9e45b352b1fc328e5525e1597ad7 100644 (file)
@@ -19,7 +19,6 @@
  */
 import * as React from 'react';
 import Level from '../ui/Level';
-import './BranchStatus.css';
 
 interface Props {
   branchLike: T.BranchLike;
index 380100fb76e6296257ed955d42dbc8209eb81627..06be324ca14e177568890e20b82ddd68836403d2 100644 (file)
 import { getRatingTooltip as nextGetRatingTooltip, isDiffMetric } from '../../helpers/measures';
 import { getLeakPeriod } from '../../helpers/periods';
 
-const KNOWN_RATINGS = ['sqale_rating', 'reliability_rating', 'security_rating'];
+const KNOWN_RATINGS = [
+  'sqale_rating',
+  'maintainability_rating', // Needed to provide the label for "new_maintainability_rating"
+  'reliability_rating',
+  'security_rating'
+];
 
 export function enhanceMeasure(
   measure: T.Measure,
index e0ec8abbdc49fa1bd41ac64db35c52556d953082..36bdae7ed38438ef85cb0a9e9ea72d4f3dcd796b 100644 (file)
@@ -358,3 +358,7 @@ export function getRatingTooltip(metricKey: string, value: number | string): str
 export function getDisplayMetrics(metrics: T.Metric[]) {
   return metrics.filter(metric => !metric.hidden && !['DATA', 'DISTRIB'].includes(metric.type));
 }
+
+export function findMeasure(measures: T.Measure[], metric: string) {
+  return measures.find(measure => measure.metric === metric);
+}
diff --git a/server/sonar-web/src/main/js/helpers/qualityGates.ts b/server/sonar-web/src/main/js/helpers/qualityGates.ts
new file mode 100644 (file)
index 0000000..ebdb09f
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.
+ */
+export function extractStatusConditionsFromProjectStatus(
+  status: T.QualityGateProjectStatus
+): T.QualityGateStatusCondition[] {
+  const { conditions } = status.projectStatus;
+  return conditions
+    ? conditions.map(c => ({
+        actual: c.actualValue,
+        error: c.errorThreshold,
+        level: c.status,
+        metric: c.metricKey,
+        op: c.comparator,
+        period: c.periodIndex
+      }))
+    : [];
+}
index 30cc2c1f81bf281acb7f17cbe700c43ceb27ded0..04d39de2f9a7b9d19c3423e544f4cae8b75627c9 100644 (file)
@@ -80,6 +80,19 @@ export function mockComponent(overrides: Partial<T.Component> = {}): T.Component
   };
 }
 
+export function mockQualityGateStatusCondition(
+  overrides: Partial<T.QualityGateStatusCondition> = {}
+): T.QualityGateStatusCondition {
+  return {
+    actual: '10',
+    error: '0',
+    level: 'ERROR',
+    metric: 'foo',
+    op: 'GT',
+    ...overrides
+  };
+}
+
 export function mockCurrentUser(overrides: Partial<T.LoggedInUser> = {}): T.LoggedInUser {
   return {
     groups: [],
@@ -159,6 +172,22 @@ export function mockLocation(overrides: Partial<Location> = {}): Location {
   };
 }
 
+export function mockMeasure(overrides: Partial<T.Measure> = {}): T.Measure {
+  return {
+    bestValue: true,
+    metric: 'bugs',
+    periods: [
+      {
+        bestValue: true,
+        index: 1,
+        value: '1.0'
+      }
+    ],
+    value: '1.0',
+    ...overrides
+  };
+}
+
 export function mockOrganization(overrides: Partial<T.Organization> = {}): T.Organization {
   return { key: 'foo', name: 'Foo', ...overrides };
 }
@@ -193,12 +222,6 @@ export function mockPullRequest(overrides: Partial<T.PullRequest> = {}): T.PullR
     analysisDate: '2018-01-01',
     base: 'master',
     branch: 'feature/foo/bar',
-    status: {
-      bugs: 0,
-      codeSmells: 0,
-      qualityGateStatus: 'OK',
-      vulnerabilities: 0
-    },
     key: '1001',
     title: 'Foo Bar feature',
     ...overrides
@@ -224,6 +247,28 @@ export function mockQualityProfile(overrides: Partial<Profile> = {}): Profile {
   };
 }
 
+export function mockQualityGateProjectStatus(
+  overrides: Partial<T.QualityGateProjectStatus['projectStatus']> = {}
+): T.QualityGateProjectStatus {
+  return {
+    projectStatus: {
+      conditions: [
+        {
+          actualValue: '0',
+          comparator: 'GT',
+          errorThreshold: '1.0',
+          metricKey: 'new_bugs',
+          periodIndex: 1,
+          status: 'OK'
+        }
+      ],
+      ignoredConditions: false,
+      status: 'OK',
+      ...overrides
+    }
+  };
+}
+
 export function mockRouter(overrides: { push?: Function; replace?: Function } = {}) {
   return {
     createHref: jest.fn(),
@@ -262,12 +307,6 @@ export function mockShortLivingBranch(
     isMain: false,
     name: 'release-1.0',
     mergeBranch: 'master',
-    status: {
-      bugs: 0,
-      codeSmells: 0,
-      qualityGateStatus: 'OK',
-      vulnerabilities: 0
-    },
     type: 'SHORT',
     ...overrides
   };
@@ -280,9 +319,6 @@ export function mockLongLivingBranch(
     analysisDate: '2018-01-01',
     isMain: false,
     name: 'master',
-    status: {
-      qualityGateStatus: 'OK'
-    },
     type: 'LONG',
     ...overrides
   };
index cf255e6919b24eb43d5ba8d010194ddbbc274b94..72db686a0c9017e4fdac9c4da35a455d945d4f97 100644 (file)
@@ -83,11 +83,11 @@ export function getLongLivingBranchUrl(project: string, branch: string): Locatio
 }
 
 export function getShortLivingBranchUrl(project: string, branch: string): Location {
-  return { pathname: '/dashboard', query: { branch, id: project, resolved: 'false' } };
+  return { pathname: '/dashboard', query: { branch, id: project } };
 }
 
 export function getPullRequestUrl(project: string, pullRequest: string): Location {
-  return { pathname: '/dashboard', query: { id: project, pullRequest, resolved: 'false' } };
+  return { pathname: '/dashboard', query: { id: project, pullRequest } };
 }
 
 /**
index 57ef2a7c71a592723b2d45db336a566f514593c1..fcf8332f2287423e39af57dde8558a368121107d 100644 (file)
@@ -2408,12 +2408,16 @@ system.version_is_availble={version} is available
 # OVERVIEW
 #
 #------------------------------------------------------------------------------
+overview.failed_conditions=Failed conditions
+overview.X_more_failed_conditions={0} more failed conditions
+overview.metrics=Metrics
 overview.quality_gate=Quality Gate
 overview.quality_gate_x=Quality Gate: {0}
 overview.quality_gate_failed_with_x=with {0} errors
 overview.you_should_define_quality_gate=You should define a quality gate on this project.
 overview.quality_gate.ignored_conditions=Some Quality Gate conditions on New Code were ignored because of the small number of New Lines
 overview.quality_gate.ignored_conditions.tooltip=At the start of a new code period, if very few lines have been added or modified, it might be difficult to reach the desired level of code coverage or duplications. To prevent Quality Gate failure when there's little that can be done about it, Quality Gate conditions about duplications in new code and coverage on new code are ignored until the number of new lines is at least 20.
+overview.quality_gate.conditions_on_new_code=Only conditions on new code that are defined in the Quality Gate are checked. See the {link} associated to the project for details.
 overview.quality_profiles=Quality Profiles
 overview.new_code_period_x=New code: {0}
 overview.started_x=started {0}
@@ -2421,6 +2425,7 @@ overview.previous_analysis_x=Previous analysis was {0}
 overview.started_on_x=Started on {0}
 overview.previous_analysis_on_x=Previous analysis on {0}
 overview.on_new_code=On New Code
+overview.on_new_code_long=Conditions on New Code
 overview.about_this_portfolio=About This Portfolio
 overview.about_this_project.APP=About This Application
 overview.about_this_project.TRK=About This Project
@@ -2454,7 +2459,9 @@ overview.metric.new_lines=New Lines
 overview.metric.new_lines_to_cover=New Lines to Cover
 overview.metric.files=Files
 overview.coverage_on=Coverage on
+overview.coverage_on_X_lines=Coverage on {count} New Lines to cover
 overview.duplications_on=Duplications on
+overview.duplications_on_X=Duplications on {count} New Lines
 
 overview.period.previous_version=since {0}
 overview.period.previous_version_only_date=since previous version