]> source.dussan.org Git - sonarqube.git/commitdiff
review source viewer measures overlay in react (#3084)
authorStas Vilchik <stas.vilchik@sonarsource.com>
Wed, 21 Feb 2018 12:32:25 +0000 (13:32 +0100)
committerGitHub <noreply@github.com>
Wed, 21 Feb 2018 12:32:25 +0000 (13:32 +0100)
37 files changed:
server/sonar-web/src/main/js/api/issues.ts
server/sonar-web/src/main/js/api/tests.ts
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/component-measures/components/App.js
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayMeasure.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCase.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCases.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlay-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayCoveredFiles-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayMeasure-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCase-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCases-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayMeasure-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayTestCase-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayTestCases-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/styles.css
server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-all.hbs [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-coverage.hbs [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-duplications.hbs [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-issues.hbs [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-lines.hbs [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-test-cases.hbs [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-tests.hbs [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/views/templates/source-viewer-measures.hbs [deleted file]
server/sonar-web/src/main/js/components/measure/Measure.tsx
server/sonar-web/src/main/js/components/measure/__tests__/Measure-test.tsx
server/sonar-web/src/main/js/components/shared/TestStatusIcon.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/measures.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index de740e4190c8f6dcea10f4c7bfb6fcd811bb8136..9830e627e54658cd99a88ed2b3168e2af51c83b4 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { FacetValue } from '../app/types';
 import { getJSON, post, postJSON, RequestData } from '../helpers/request';
 import { RawIssue } from '../helpers/issues';
 
@@ -30,7 +31,10 @@ export interface IssueResponse {
 interface IssuesResponse {
   components?: { key: string; name: string; uuid: string }[];
   debtTotal?: number;
-  facets: Array<{}>;
+  facets: Array<{
+    property: string;
+    values: { count: number; val: string }[];
+  }>;
   issues: RawIssue[];
   paging: {
     pageIndex: number;
@@ -45,7 +49,13 @@ export function searchIssues(query: RequestData): Promise<IssuesResponse> {
   return getJSON('/api/issues/search', query);
 }
 
-export function getFacets(query: RequestData, facets: string[]): Promise<any> {
+export function getFacets(
+  query: RequestData,
+  facets: string[]
+): Promise<{
+  facets: Array<{ property: string; values: FacetValue[] }>;
+  response: IssuesResponse;
+}> {
   const data = {
     ...query,
     facets: facets.join(),
index e39aa2ff45ef4f914aa4c75125acd5f13a5c6dbf..1ac562186307c5fe544c8922fd32b5f992040334 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import throwGlobalError from '../app/utils/throwGlobalError';
+import { Paging, TestCase, CoveredFile } from '../app/types';
 import { getJSON } from '../helpers/request';
 
-export interface GetTestsParameters {
+export function getTests(parameters: {
   branch?: string;
+  p?: number;
+  ps?: number;
+  sourceFileKey?: string;
+  sourceFileLineNumber?: number;
   testFileKey: string;
-}
-
-export function getTests(parameters: GetTestsParameters) {
+  testId?: string;
+}): Promise<{ paging: Paging; tests: TestCase[] }> {
   return getJSON('/api/tests/list', parameters).catch(throwGlobalError);
 }
 
-export interface GetCoveredFilesParameters {
-  testId: string;
-}
-
-export function getCoveredFiles(parameters: GetCoveredFilesParameters) {
-  return getJSON('/api/tests/covered_files', parameters).catch(throwGlobalError);
+export function getCoveredFiles(data: { testId: string }): Promise<CoveredFile[]> {
+  return getJSON('/api/tests/covered_files', data).then(r => r.files, throwGlobalError);
 }
index 4ec38c6b96193e16686b7fb0b823e0fa6c7169f8..5d552e722a29fe29dcb4e5cfdee6bda4cab87d63 100644 (file)
@@ -331,3 +331,46 @@ export interface PermissionTemplate {
     withProjectCreator?: boolean;
   }>;
 }
+
+export interface TestCase {
+  coveredLines: number;
+  durationInMs: number;
+  fileId: string;
+  fileKey: string;
+  fileName: string;
+  id: string;
+  message?: string;
+  name: string;
+  stacktrace?: string;
+  status: string;
+}
+
+export interface CoveredFile {
+  key: string;
+  longName: string;
+  coveredLines: number;
+}
+
+export interface FacetValue {
+  count: number;
+  val: string;
+}
+
+export interface SourceViewerFile {
+  canMarkAsFavorite?: boolean;
+  key: string;
+  measures: {
+    coverage?: string;
+    duplicationDensity?: string;
+    issues?: string;
+    lines?: string;
+    tests?: string;
+  };
+  path: string;
+  project: string;
+  projectName: string;
+  q: string;
+  subProject?: string;
+  subProjectName?: string;
+  uuid: string;
+}
index dbd68e8f27623347dc7bfe6e5966099a7d6b49a7..4198c6689fd798ad26d047883afacf7c6af478a9 100644 (file)
@@ -28,6 +28,7 @@ import ScreenPositionHelper from '../../../components/common/ScreenPositionHelpe
 import { hasBubbleChart, parseQuery, serializeQuery } from '../utils';
 import { getBranchName } from '../../../helpers/branches';
 import { translate } from '../../../helpers/l10n';
+import { getDisplayMetrics } from '../../../helpers/measures';
 /*:: import type { Component, Query, Period } from '../types'; */
 /*:: import type { RawQuery } from '../../../helpers/query'; */
 /*:: import type { Metric } from '../../../store/metrics/actions'; */
@@ -106,11 +107,9 @@ export default class App extends React.PureComponent {
     }
   }
 
-  fetchMeasures = ({ branch, component, fetchMeasures, metrics, metricsKey } /*: Props */) => {
+  fetchMeasures = ({ branch, component, fetchMeasures, metrics } /*: Props */) => {
     this.setState({ loading: true });
-    const filteredKeys = metricsKey.filter(
-      key => !metrics[key].hidden && !['DATA', 'DISTRIB'].includes(metrics[key].type)
-    );
+    const filteredKeys = getDisplayMetrics(Object.values(metrics)).map(metric => metric.key);
     fetchMeasures(component.key, filteredKeys, getBranchName(branch)).then(
       ({ measures, leakPeriod }) => {
         if (this.mounted) {
index 2022165e43f411d340870370a8d96fe03eb196fe..6f267452fa0723894cf76b20e83fa8be1af22cb6 100644 (file)
@@ -27,7 +27,6 @@ import CoveragePopupView from './popups/coverage-popup';
 import DuplicationPopupView from './popups/duplication-popup';
 import LineActionsPopupView from './popups/line-actions-popup';
 import SCMPopupView from './popups/scm-popup';
-import MeasuresOverlay from './views/measures-overlay';
 import loadIssues from './helpers/loadIssues';
 import getCoverageStatus from './helpers/getCoverageStatus';
 import {
@@ -464,15 +463,6 @@ export default class SourceViewerBase extends React.PureComponent {
     });
   };
 
-  showMeasures = () => {
-    const measuresOverlay = new MeasuresOverlay({
-      branch: this.props.branch,
-      component: this.state.component,
-      large: true
-    });
-    measuresOverlay.render();
-  };
-
   handleCoverageClick = (line /*: SourceLine */, element /*: HTMLElement */) => {
     getTests(this.props.component, line.line, this.props.branch).then(tests => {
       const popup = new CoveragePopupView({
@@ -691,11 +681,7 @@ export default class SourceViewerBase extends React.PureComponent {
 
     return (
       <div className={className} ref={node => (this.node = node)}>
-        <SourceViewerHeader
-          branch={this.props.branch}
-          component={this.state.component}
-          showMeasures={this.showMeasures}
-        />
+        <SourceViewerHeader branch={this.props.branch} sourceViewerFile={this.state.component} />
         {sourceRemoved && (
           <div className="alert alert-warning spacer-top">
             {translate('code_viewer.no_source_code_displayed_due_to_source_removed')}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js
deleted file mode 100644 (file)
index 253f8db..0000000
+++ /dev/null
@@ -1,222 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import { Link } from 'react-router';
-import QualifierIcon from '../shared/QualifierIcon';
-import FavoriteContainer from '../controls/FavoriteContainer';
-import { getPathUrlAsString, getProjectUrl, getComponentIssuesUrl } from '../../helpers/urls';
-import { collapsedDirFromPath, fileFromPath } from '../../helpers/path';
-import { translate } from '../../helpers/l10n';
-import { formatMeasure } from '../../helpers/measures';
-
-export default class SourceViewerHeader extends React.PureComponent {
-  /*:: props: {
-    branch?: string,
-    component: {
-      canMarkAsFavorite: boolean,
-      key: string,
-      measures: {
-        coverage?: string,
-        duplicationDensity?: string,
-        issues?: string,
-        lines?: string,
-        tests?: string
-      },
-      path: string,
-      project: string,
-      projectName: string,
-      q: string,
-      subProject?: string,
-      subProjectName?: string,
-      uuid: string
-    },
-    showMeasures: () => void
-  };
-*/
-
-  showMeasures = (e /*: SyntheticInputEvent */) => {
-    e.preventDefault();
-    this.props.showMeasures();
-  };
-
-  openInWorkspace = (e /*: SyntheticInputEvent */) => {
-    e.preventDefault();
-    const { key } = this.props.component;
-    const Workspace = require('../workspace/main').default;
-    Workspace.openComponent({ key, branch: this.props.branch });
-  };
-
-  render() {
-    const {
-      key,
-      measures,
-      path,
-      project,
-      projectName,
-      q,
-      subProject,
-      subProjectName,
-      uuid
-    } = this.props.component;
-    const isUnitTest = q === 'UTS';
-    const workspace = false;
-    let rawSourcesLink =
-      window.baseUrl + `/api/sources/raw?key=${encodeURIComponent(this.props.component.key)}`;
-    if (this.props.branch) {
-      rawSourcesLink += `&branch=${encodeURIComponent(this.props.branch)}`;
-    }
-
-    // TODO favorite
-    return (
-      <div className="source-viewer-header">
-        <div className="source-viewer-header-component">
-          <div className="component-name">
-            <div className="component-name-parent">
-              <a
-                href={getPathUrlAsString(getProjectUrl(project, this.props.branch))}
-                className="link-with-icon">
-                <QualifierIcon qualifier="TRK" /> <span>{projectName}</span>
-              </a>
-            </div>
-
-            {subProject != null && (
-              <div className="component-name-parent">
-                <a
-                  href={getPathUrlAsString(getProjectUrl(subProject, this.props.branch))}
-                  className="link-with-icon">
-                  <QualifierIcon qualifier="BRC" /> <span>{subProjectName}</span>
-                </a>
-              </div>
-            )}
-
-            <div className="component-name-path">
-              <QualifierIcon qualifier={q} /> <span>{collapsedDirFromPath(path)}</span>
-              <span className="component-name-file">{fileFromPath(path)}</span>
-              {this.props.component.canMarkAsFavorite && (
-                <FavoriteContainer className="component-name-favorite" componentKey={key} />
-              )}
-            </div>
-          </div>
-        </div>
-
-        <div className="dropdown source-viewer-header-actions">
-          <a
-            className="js-actions icon-list dropdown-toggle"
-            data-toggle="dropdown"
-            title={translate('component_viewer.more_actions')}
-          />
-          <ul className="dropdown-menu dropdown-menu-right">
-            <li>
-              <a className="js-measures" href="#" onClick={this.showMeasures}>
-                {translate('component_viewer.show_details')}
-              </a>
-            </li>
-            <li>
-              <a
-                className="js-new-window"
-                target="_blank"
-                href={getPathUrlAsString({
-                  pathname: '/component',
-                  query: { branch: this.props.branch, id: this.props.component.key }
-                })}>
-                {translate('component_viewer.new_window')}
-              </a>
-            </li>
-            {!workspace && (
-              <li>
-                <a className="js-workspace" href="#" onClick={this.openInWorkspace}>
-                  {translate('component_viewer.open_in_workspace')}
-                </a>
-              </li>
-            )}
-            <li>
-              <a className="js-raw-source" href={rawSourcesLink} target="_blank">
-                {translate('component_viewer.show_raw_source')}
-              </a>
-            </li>
-          </ul>
-        </div>
-
-        <div className="source-viewer-header-measures">
-          {isUnitTest && (
-            <div className="source-viewer-header-measure">
-              <span className="source-viewer-header-measure-value">
-                {formatMeasure(measures.tests, 'SHORT_INT')}
-              </span>
-              <span className="source-viewer-header-measure-label">
-                {translate('metric.tests.name')}
-              </span>
-            </div>
-          )}
-
-          {!isUnitTest && (
-            <div className="source-viewer-header-measure">
-              <span className="source-viewer-header-measure-value">
-                {formatMeasure(measures.lines, 'SHORT_INT')}
-              </span>
-              <span className="source-viewer-header-measure-label">
-                {translate('metric.lines.name')}
-              </span>
-            </div>
-          )}
-
-          <div className="source-viewer-header-measure">
-            <span className="source-viewer-header-measure-value">
-              <Link
-                to={getComponentIssuesUrl(project, {
-                  resolved: 'false',
-                  fileUuids: uuid,
-                  branch: this.props.branch
-                })}>
-                {measures.issues != null ? formatMeasure(measures.issues, 'SHORT_INT') : 0}
-              </Link>
-            </span>
-            <span className="source-viewer-header-measure-label">
-              {translate('metric.violations.name')}
-            </span>
-          </div>
-
-          {measures.coverage != null && (
-            <div className="source-viewer-header-measure">
-              <span className="source-viewer-header-measure-value">
-                {formatMeasure(measures.coverage, 'PERCENT')}
-              </span>
-              <span className="source-viewer-header-measure-label">
-                {translate('metric.coverage.name')}
-              </span>
-            </div>
-          )}
-
-          {measures.duplicationDensity != null && (
-            <div className="source-viewer-header-measure">
-              <span className="source-viewer-header-measure-value">
-                {formatMeasure(measures.duplicationDensity, 'PERCENT')}
-              </span>
-              <span className="source-viewer-header-measure-label">
-                {translate('duplications')}
-              </span>
-            </div>
-          )}
-        </div>
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx
new file mode 100644 (file)
index 0000000..a29348b
--- /dev/null
@@ -0,0 +1,226 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 MeasuresOverlay from './components/MeasuresOverlay';
+import { SourceViewerFile } from '../../app/types';
+import QualifierIcon from '../shared/QualifierIcon';
+import FavoriteContainer from '../controls/FavoriteContainer';
+import {
+  getPathUrlAsString,
+  getProjectUrl,
+  getComponentIssuesUrl,
+  getBaseUrl
+} from '../../helpers/urls';
+import { collapsedDirFromPath, fileFromPath } from '../../helpers/path';
+import { translate } from '../../helpers/l10n';
+import { formatMeasure } from '../../helpers/measures';
+
+interface Props {
+  branch: string | undefined;
+  sourceViewerFile: SourceViewerFile;
+}
+
+interface State {
+  measuresOverlay: boolean;
+}
+
+export default class SourceViewerHeader extends React.PureComponent<Props, State> {
+  state: State = { measuresOverlay: false };
+
+  handleShowMeasuresClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    this.setState({ measuresOverlay: true });
+  };
+
+  handleMeasuresOverlayClose = () => {
+    this.setState({ measuresOverlay: false });
+  };
+
+  openInWorkspace = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    const { key } = this.props.sourceViewerFile;
+    const Workspace = require('../workspace/main').default;
+    Workspace.openComponent({ key, branch: this.props.branch });
+  };
+
+  render() {
+    const {
+      key,
+      measures,
+      path,
+      project,
+      projectName,
+      q,
+      subProject,
+      subProjectName,
+      uuid
+    } = this.props.sourceViewerFile;
+    const isUnitTest = q === 'UTS';
+    const workspace = false;
+    let rawSourcesLink =
+      getBaseUrl() + `/api/sources/raw?key=${encodeURIComponent(this.props.sourceViewerFile.key)}`;
+    if (this.props.branch) {
+      rawSourcesLink += `&branch=${encodeURIComponent(this.props.branch)}`;
+    }
+
+    // TODO favorite
+    return (
+      <div className="source-viewer-header">
+        <div className="source-viewer-header-component">
+          <div className="component-name">
+            <div className="component-name-parent">
+              <a
+                className="link-with-icon"
+                href={getPathUrlAsString(getProjectUrl(project, this.props.branch))}>
+                <QualifierIcon qualifier="TRK" /> <span>{projectName}</span>
+              </a>
+            </div>
+
+            {subProject != null && (
+              <div className="component-name-parent">
+                <a
+                  className="link-with-icon"
+                  href={getPathUrlAsString(getProjectUrl(subProject, this.props.branch))}>
+                  <QualifierIcon qualifier="BRC" /> <span>{subProjectName}</span>
+                </a>
+              </div>
+            )}
+
+            <div className="component-name-path">
+              <QualifierIcon qualifier={q} /> <span>{collapsedDirFromPath(path)}</span>
+              <span className="component-name-file">{fileFromPath(path)}</span>
+              {this.props.sourceViewerFile.canMarkAsFavorite && (
+                <FavoriteContainer className="component-name-favorite" componentKey={key} />
+              )}
+            </div>
+          </div>
+        </div>
+
+        <div className="dropdown source-viewer-header-actions">
+          <a
+            className="js-actions icon-list dropdown-toggle"
+            data-toggle="dropdown"
+            title={translate('component_viewer.more_actions')}
+          />
+          <ul className="dropdown-menu dropdown-menu-right">
+            <li>
+              <a className="js-measures" href="#" onClick={this.handleShowMeasuresClick}>
+                {translate('component_viewer.show_details')}
+              </a>
+              {this.state.measuresOverlay && (
+                <MeasuresOverlay
+                  branch={this.props.branch}
+                  onClose={this.handleMeasuresOverlayClose}
+                  sourceViewerFile={this.props.sourceViewerFile}
+                />
+              )}
+            </li>
+            <li>
+              <a
+                className="js-new-window"
+                href={getPathUrlAsString({
+                  pathname: '/component',
+                  query: { branch: this.props.branch, id: this.props.sourceViewerFile.key }
+                })}
+                target="_blank">
+                {translate('component_viewer.new_window')}
+              </a>
+            </li>
+            {!workspace && (
+              <li>
+                <a className="js-workspace" href="#" onClick={this.openInWorkspace}>
+                  {translate('component_viewer.open_in_workspace')}
+                </a>
+              </li>
+            )}
+            <li>
+              <a className="js-raw-source" href={rawSourcesLink} target="_blank">
+                {translate('component_viewer.show_raw_source')}
+              </a>
+            </li>
+          </ul>
+        </div>
+
+        <div className="source-viewer-header-measures">
+          {isUnitTest && (
+            <div className="source-viewer-header-measure">
+              <span className="source-viewer-header-measure-value">
+                {formatMeasure(measures.tests, 'SHORT_INT')}
+              </span>
+              <span className="source-viewer-header-measure-label">
+                {translate('metric.tests.name')}
+              </span>
+            </div>
+          )}
+
+          {!isUnitTest && (
+            <div className="source-viewer-header-measure">
+              <span className="source-viewer-header-measure-value">
+                {formatMeasure(measures.lines, 'SHORT_INT')}
+              </span>
+              <span className="source-viewer-header-measure-label">
+                {translate('metric.lines.name')}
+              </span>
+            </div>
+          )}
+
+          <div className="source-viewer-header-measure">
+            <span className="source-viewer-header-measure-value">
+              <Link
+                to={getComponentIssuesUrl(project, {
+                  resolved: 'false',
+                  fileUuids: uuid,
+                  branch: this.props.branch
+                })}>
+                {measures.issues != null ? formatMeasure(measures.issues, 'SHORT_INT') : 0}
+              </Link>
+            </span>
+            <span className="source-viewer-header-measure-label">
+              {translate('metric.violations.name')}
+            </span>
+          </div>
+
+          {measures.coverage != null && (
+            <div className="source-viewer-header-measure">
+              <span className="source-viewer-header-measure-value">
+                {formatMeasure(measures.coverage, 'PERCENT')}
+              </span>
+              <span className="source-viewer-header-measure-label">
+                {translate('metric.coverage.name')}
+              </span>
+            </div>
+          )}
+
+          {measures.duplicationDensity != null && (
+            <div className="source-viewer-header-measure">
+              <span className="source-viewer-header-measure-value">
+                {formatMeasure(measures.duplicationDensity, 'PERCENT')}
+              </span>
+              <span className="source-viewer-header-measure-label">
+                {translate('duplications')}
+              </span>
+            </div>
+          )}
+        </div>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx
new file mode 100644 (file)
index 0000000..131c58b
--- /dev/null
@@ -0,0 +1,459 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { keyBy, sortBy, groupBy } from 'lodash';
+import MeasuresOverlayMeasure from './MeasuresOverlayMeasure';
+import MeasuresOverlayTestCases from './MeasuresOverlayTestCases';
+import { getFacets } from '../../../api/issues';
+import { getMeasures } from '../../../api/measures';
+import { getAllMetrics } from '../../../api/metrics';
+import { FacetValue, SourceViewerFile } from '../../../app/types';
+import Modal from '../../controls/Modal';
+import Measure from '../../measure/Measure';
+import QualifierIcon from '../../shared/QualifierIcon';
+import SeverityHelper from '../../shared/SeverityHelper';
+import CoverageRating from '../../ui/CoverageRating';
+import DuplicationsRating from '../../ui/DuplicationsRating';
+import IssueTypeIcon from '../../ui/IssueTypeIcon';
+import { SEVERITIES, TYPES } from '../../../helpers/constants';
+import { translate, getLocalizedMetricName } from '../../../helpers/l10n';
+import {
+  formatMeasure,
+  MeasureEnhanced,
+  getDisplayMetrics,
+  enhanceMeasuresWithMetrics
+} from '../../../helpers/measures';
+import { getProjectUrl } from '../../../helpers/urls';
+
+interface Props {
+  branch: string | undefined;
+  onClose: () => void;
+  sourceViewerFile: SourceViewerFile;
+}
+
+interface Measures {
+  [metricKey: string]: MeasureEnhanced;
+}
+
+interface State {
+  loading: boolean;
+  measures: Measures;
+  severitiesFacet?: FacetValue[];
+  showAllMeasures: boolean;
+  tagsFacet?: FacetValue[];
+  typesFacet?: FacetValue[];
+}
+
+export default class MeasuresOverlay extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { loading: true, measures: {}, showAllMeasures: false };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchData();
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchData = () => {
+    Promise.all([this.fetchMeasures(), this.fetchIssues()]).then(
+      ([measures, facets]) => {
+        if (this.mounted) {
+          this.setState({ loading: false, measures, ...facets });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  fetchMeasures = () => {
+    return getAllMetrics().then(metrics => {
+      const metricKeys = getDisplayMetrics(metrics).map(metric => metric.key);
+
+      // eslint-disable-next-line promise/no-nesting
+      return getMeasures(this.props.sourceViewerFile.key, metricKeys, this.props.branch).then(
+        measures => {
+          const withMetrics = enhanceMeasuresWithMetrics(measures, metrics).filter(
+            measure => measure.metric
+          );
+          return keyBy(withMetrics, measure => measure.metric.key);
+        }
+      );
+    });
+  };
+
+  fetchIssues = () => {
+    return getFacets(
+      {
+        branch: this.props.branch,
+        componentKeys: this.props.sourceViewerFile.key,
+        resolved: 'false'
+      },
+      ['types', 'severities', 'tags']
+    ).then(({ facets }) => {
+      const severitiesFacet = facets.find(f => f.property === 'severities');
+      const tagsFacet = facets.find(f => f.property === 'tags');
+      const typesFacet = facets.find(f => f.property === 'types');
+      return {
+        severitiesFacet: severitiesFacet && severitiesFacet.values,
+        tagsFacet: tagsFacet && tagsFacet.values,
+        typesFacet: typesFacet && typesFacet.values
+      };
+    });
+  };
+
+  handleCloseClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.props.onClose();
+  };
+
+  handleAllMeasuresClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.setState({ showAllMeasures: true });
+  };
+
+  renderMeasure = (measure: MeasureEnhanced | undefined) => {
+    return measure ? <MeasuresOverlayMeasure key={measure.metric.key} measure={measure} /> : null;
+  };
+
+  renderLines = () => {
+    const { measures } = this.state;
+
+    return (
+      <div className="source-viewer-measures-section">
+        <div className="source-viewer-measures-card">
+          <div className="measures">
+            <div className="measures-list">
+              {this.renderMeasure(measures.lines)}
+              {this.renderMeasure(measures.ncloc)}
+              {this.renderMeasure(measures.comment_lines)}
+              {this.renderMeasure(measures.comment_lines_density)}
+            </div>
+          </div>
+
+          <div className="measures">
+            <div className="measures-list">
+              {this.renderMeasure(measures.cognitive_complexity)}
+              {this.renderMeasure(measures.complexity)}
+              {this.renderMeasure(measures.function_complexity)}
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  };
+
+  renderBigMeasure = (measure: MeasureEnhanced | undefined) => {
+    return measure ? (
+      <div className="measure measure-big" data-metric={measure.metric.key}>
+        <span className="measure-value">
+          <Measure
+            metricKey={measure.metric.key}
+            metricType={measure.metric.type}
+            value={measure.value}
+          />
+        </span>
+        <span className="measure-name">{getLocalizedMetricName(measure.metric, true)}</span>
+      </div>
+    ) : null;
+  };
+
+  renderIssues = () => {
+    const { measures, severitiesFacet, tagsFacet, typesFacet } = this.state;
+    return (
+      <div className="source-viewer-measures-section">
+        <div className="source-viewer-measures-card">
+          <div className="measures">
+            {this.renderBigMeasure(measures.violations)}
+            {this.renderBigMeasure(measures.sqale_index)}
+          </div>
+          {measures.violations &&
+            !measures.violations.value && (
+              <>
+                {typesFacet && (
+                  <div className="measures">
+                    <div className="measures-list">
+                      {sortBy(typesFacet, f => TYPES.indexOf(f.val)).map(f => (
+                        <div className="measure measure-one-line" key={f.val}>
+                          <span className="measure-name">
+                            <IssueTypeIcon className="little-spacer-right" query={f.val} />
+                            {translate('issue.type', f.val)}
+                          </span>
+                          <span className="measure-value">
+                            {formatMeasure(f.count, 'SHORT_INT')}
+                          </span>
+                        </div>
+                      ))}
+                    </div>
+                  </div>
+                )}
+                {severitiesFacet && (
+                  <div className="measures">
+                    <div className="measures-list">
+                      {sortBy(severitiesFacet, f => SEVERITIES.indexOf(f.val)).map(f => (
+                        <div className="measure measure-one-line" key={f.val}>
+                          <span className="measure-name">
+                            <SeverityHelper severity={f.val} />
+                          </span>
+                          <span className="measure-value">
+                            {formatMeasure(f.count, 'SHORT_INT')}
+                          </span>
+                        </div>
+                      ))}
+                    </div>
+                  </div>
+                )}
+                {tagsFacet && (
+                  <div className="measures">
+                    <div className="measures-list">
+                      {tagsFacet.map(f => (
+                        <div className="measure measure-one-line" key={f.val}>
+                          <span className="measure-name">
+                            <i className="icon-tags little-spacer-right" />
+                            {f.val}
+                          </span>
+                          <span className="measure-value">
+                            {formatMeasure(f.count, 'SHORT_INT')}
+                          </span>
+                        </div>
+                      ))}
+                    </div>
+                  </div>
+                )}
+              </>
+            )}
+        </div>
+      </div>
+    );
+  };
+
+  renderCoverage = () => {
+    const { coverage } = this.state.measures;
+    if (!coverage) {
+      return null;
+    }
+    return (
+      <div className="source-viewer-measures-section">
+        <div className="source-viewer-measures-card">
+          <div className="measures">
+            <div className="measures-chart">
+              <CoverageRating size="big" value={coverage.value} />
+            </div>
+            <div className="measure measure-big" data-metric={coverage.metric.key}>
+              <span className="measure-value">
+                <Measure
+                  metricKey={coverage.metric.key}
+                  metricType={coverage.metric.type}
+                  value={coverage.value}
+                />
+              </span>
+              <span className="measure-name">{getLocalizedMetricName(coverage.metric)}</span>
+            </div>
+          </div>
+
+          <div className="measures">
+            <div className="measures-list">
+              {this.renderMeasure(this.state.measures.uncovered_lines)}
+              {this.renderMeasure(this.state.measures.lines_to_cover)}
+              {this.renderMeasure(this.state.measures.uncovered_conditions)}
+              {this.renderMeasure(this.state.measures.conditions_to_cover)}
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  };
+
+  renderDuplications = () => {
+    const { duplicated_lines_density: duplications } = this.state.measures;
+    if (!duplications) {
+      return null;
+    }
+    return (
+      <div className="source-viewer-measures-section">
+        <div className="source-viewer-measures-card">
+          <div className="measures">
+            <div className="measures-chart">
+              <DuplicationsRating
+                muted={duplications.value === undefined}
+                size="big"
+                value={Number(duplications.value || 0)}
+              />
+            </div>
+            <div className="measure measure-big" data-metric={duplications.metric.key}>
+              <span className="measure-value">
+                <Measure
+                  metricKey={duplications.metric.key}
+                  metricType={duplications.metric.type}
+                  value={duplications.value}
+                />
+              </span>
+              <span className="measure-name">
+                {getLocalizedMetricName(duplications.metric, true)}
+              </span>
+            </div>
+          </div>
+
+          <div className="measures">
+            <div className="measures-list">
+              {this.renderMeasure(this.state.measures.duplicated_blocks)}
+              {this.renderMeasure(this.state.measures.duplicated_lines)}
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  };
+
+  renderTests = () => {
+    const { measures } = this.state;
+    return (
+      <div className="source-viewer-measures">
+        <div className="source-viewer-measures-section">
+          <div className="source-viewer-measures-card">
+            <div className="measures">
+              <div className="measures-list">
+                {this.renderMeasure(measures.tests)}
+                {this.renderMeasure(measures.test_success_density)}
+                {this.renderMeasure(measures.test_failures)}
+                {this.renderMeasure(measures.test_errors)}
+                {this.renderMeasure(measures.skipped_tests)}
+                {this.renderMeasure(measures.test_execution_time)}
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  };
+
+  renderDomain = (domain: string, measures: MeasureEnhanced[]) => {
+    return (
+      <div className="source-viewer-measures-card" key={domain}>
+        <div className="measures">
+          <div className="measures-list">
+            <div className="measure measure-one-line measure-big">
+              <span className="measure-name">{domain}</span>
+            </div>
+            {sortBy(measures.filter(measure => measure.value !== undefined), measure =>
+              getLocalizedMetricName(measure.metric)
+            ).map(measure => this.renderMeasure(measure))}
+          </div>
+        </div>
+      </div>
+    );
+  };
+
+  renderAllMeasures = () => {
+    const domains = groupBy(Object.values(this.state.measures), measure => measure.metric.domain);
+    const domainKeys = Object.keys(domains);
+    const odd = domainKeys.filter((_, index) => index % 2 === 1);
+    const even = domainKeys.filter((_, index) => index % 2 === 0);
+    return (
+      <div className="source-viewer-measures source-viewer-measures-secondary js-all-measures">
+        <div className="source-viewer-measures-section source-viewer-measures-section-big">
+          {odd.map(domain => this.renderDomain(domain, domains[domain]))}
+        </div>
+        <div className="source-viewer-measures-section source-viewer-measures-section-big">
+          {even.map(domain => this.renderDomain(domain, domains[domain]))}
+        </div>
+      </div>
+    );
+  };
+
+  render() {
+    const { branch, sourceViewerFile } = this.props;
+    const { loading } = this.state;
+
+    return (
+      <Modal contentLabel="" large={true} onRequestClose={this.props.onClose}>
+        <div className="modal-container source-viewer-measures-modal">
+          <div className="source-viewer-header-component source-viewer-measures-component">
+            <div className="source-viewer-header-component-project">
+              <QualifierIcon className="little-spacer-right" qualifier="TRK" />
+              <Link to={getProjectUrl(sourceViewerFile.project, branch)}>
+                {sourceViewerFile.projectName}
+              </Link>
+
+              {sourceViewerFile.subProject && (
+                <>
+                  <QualifierIcon className="big-spacer-left little-spacer-right" qualifier="BRC" />
+                  <Link to={getProjectUrl(sourceViewerFile.subProject, branch)}>
+                    {sourceViewerFile.subProjectName}
+                  </Link>
+                </>
+              )}
+            </div>
+
+            <div className="source-viewer-header-component-name">
+              <QualifierIcon className="little-spacer-right" qualifier={sourceViewerFile.q} />
+              {sourceViewerFile.path}
+            </div>
+          </div>
+
+          {loading ? (
+            <i className="spinner" />
+          ) : (
+            <>
+              {sourceViewerFile.q === 'UTS' ? (
+                <>
+                  {this.renderTests()}
+                  <MeasuresOverlayTestCases branch={branch} componentKey={sourceViewerFile.key} />
+                </>
+              ) : (
+                <div className="source-viewer-measures">
+                  {this.renderLines()}
+                  {this.renderIssues()}
+                  {this.renderCoverage()}
+                  {this.renderDuplications()}
+                </div>
+              )}
+            </>
+          )}
+
+          <div className="spacer-top">
+            {this.state.showAllMeasures ? (
+              this.renderAllMeasures()
+            ) : (
+              <a className="js-show-all-measures" href="#" onClick={this.handleAllMeasuresClick}>
+                {translate('component_viewer.show_all_measures')}
+              </a>
+            )}
+          </div>
+        </div>
+
+        <footer className="modal-foot">
+          <button className="button-link" onClick={this.handleCloseClick} type="button">
+            {translate('close')}
+          </button>
+        </footer>
+      </Modal>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx
new file mode 100644 (file)
index 0000000..f89a9aa
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { getCoveredFiles } from '../../../api/tests';
+import { TestCase, CoveredFile } from '../../../app/types';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { getProjectUrl } from '../../../helpers/urls';
+import DeferredSpinner from '../../common/DeferredSpinner';
+
+interface Props {
+  testCase: TestCase;
+}
+
+interface State {
+  coveredFiles?: CoveredFile[];
+  loading: boolean;
+}
+
+export default class MeasuresOverlayCoveredFiles extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { loading: true };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchCoveredFiles();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (this.props.testCase.id !== prevProps.testCase.id) {
+      this.fetchCoveredFiles();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchCoveredFiles = () => {
+    this.setState({ loading: true });
+    getCoveredFiles({ testId: this.props.testCase.id }).then(
+      coveredFiles => {
+        if (this.mounted) {
+          this.setState({ coveredFiles, loading: false });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  render() {
+    const { testCase } = this.props;
+    const { loading, coveredFiles } = this.state;
+
+    return (
+      <div className="source-viewer-measures-section source-viewer-measures-section-big js-selected-test">
+        <DeferredSpinner loading={loading}>
+          <div className="source-viewer-measures-card source-viewer-measures-card-fixed-height">
+            {testCase.status !== 'ERROR' &&
+              testCase.status !== 'FAILURE' &&
+              coveredFiles !== undefined && (
+                <>
+                  <div className="bubble-popup-title">
+                    {translate('component_viewer.transition.covers')}
+                  </div>
+                  {coveredFiles.length > 0
+                    ? coveredFiles.map(coveredFile => (
+                        <div className="bubble-popup-section" key={coveredFile.key}>
+                          <Link to={getProjectUrl(coveredFile.key)}>{coveredFile.longName}</Link>
+                          <span className="note spacer-left">
+                            {translateWithParameters(
+                              'component_viewer.x_lines_are_covered',
+                              coveredFile.coveredLines
+                            )}
+                          </span>
+                        </div>
+                      ))
+                    : translate('none')}
+                </>
+              )}
+
+            {testCase.status !== 'OK' && (
+              <>
+                <div className="bubble-popup-title">{translate('component_viewer.details')}</div>
+                {testCase.message && <pre>{testCase.message}</pre>}
+                <pre>{testCase.stacktrace}</pre>
+              </>
+            )}
+          </div>
+        </DeferredSpinner>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayMeasure.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayMeasure.tsx
new file mode 100644 (file)
index 0000000..7a41a9e
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { Metric } from '../../../app/types';
+import Measure from '../../measure/Measure';
+import IssueTypeIcon from '../../ui/IssueTypeIcon';
+import { getLocalizedMetricName } from '../../../helpers/l10n';
+
+export interface MeasureWithMetric {
+  metric: Metric;
+  value?: string;
+}
+
+interface Props {
+  measure: MeasureWithMetric;
+}
+
+export default function MeasuresOverlayMeasure({ measure }: Props) {
+  return (
+    <div
+      className="measure measure-one-line"
+      data-metric={measure.metric.key}
+      key={measure.metric.key}>
+      <span className="measure-name">
+        {['bugs', 'vulnerabilities', 'code_smells'].includes(measure.metric.key) && (
+          <IssueTypeIcon className="little-spacer-right" query={measure.metric.key} />
+        )}
+        {getLocalizedMetricName(measure.metric)}
+      </span>
+      <span className="measure-value">
+        <Measure
+          metricKey={measure.metric.key}
+          metricType={measure.metric.type}
+          small={true}
+          value={measure.value}
+        />
+      </span>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCase.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCase.tsx
new file mode 100644 (file)
index 0000000..17b3e45
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { TestCase } from '../../../app/types';
+import TestStatusIcon from '../../shared/TestStatusIcon';
+
+interface Props {
+  onClick: (testId: string) => void;
+  testCase: TestCase;
+}
+
+export default class MeasuresOverlayTestCase extends React.PureComponent<Props> {
+  handleTestCaseClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.props.onClick(this.props.testCase.id);
+  };
+
+  render() {
+    const { testCase } = this.props;
+    const { status } = testCase;
+    const hasAdditionalData = status !== 'OK' || (status === 'OK' && testCase.coveredLines);
+
+    return (
+      <tr>
+        <td className="source-viewer-test-status">
+          <TestStatusIcon status={status} />
+        </td>
+        <td className="source-viewer-test-duration note">
+          {status !== 'SKIPPED' && `${testCase.durationInMs}ms`}
+        </td>
+        <td className="source-viewer-test-name">
+          {hasAdditionalData ? (
+            <a className="js-show-test" href="#" onClick={this.handleTestCaseClick}>
+              {testCase.name}
+            </a>
+          ) : (
+            testCase.name
+          )}
+        </td>
+        <td className="source-viewer-test-covered-lines note">{testCase.coveredLines}</td>
+      </tr>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCases.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCases.tsx
new file mode 100644 (file)
index 0000000..64dca78
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { orderBy } from 'lodash';
+import MeasuresOverlayCoveredFiles from './MeasuresOverlayCoveredFiles';
+import MeasuresOverlayTestCase from './MeasuresOverlayTestCase';
+import { getTests } from '../../../api/tests';
+import { TestCase } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  branch: string | undefined;
+  componentKey: string;
+}
+
+interface State {
+  loading: boolean;
+  selectedTestId?: string;
+  sort?: string;
+  sortAsc?: boolean;
+  testCases?: TestCase[];
+}
+
+export default class MeasuresOverlayTestCases extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { loading: true };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchTests();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (
+      prevProps.branch !== this.props.branch ||
+      prevProps.componentKey !== this.props.componentKey
+    ) {
+      this.fetchTests();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchTests = () => {
+    // TODO implement pagination one day...
+    this.setState({ loading: true });
+    getTests({ branch: this.props.branch, ps: 500, testFileKey: this.props.componentKey }).then(
+      ({ tests: testCases }) => {
+        if (this.mounted) {
+          this.setState({ loading: false, testCases });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  handleTestCasesSortClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    const { sort } = event.currentTarget.dataset;
+    if (sort) {
+      this.setState((state: State) => ({
+        sort,
+        sortAsc: sort === state.sort ? !state.sortAsc : true
+      }));
+    }
+  };
+
+  handleTestCaseClick = (selectedTestId: string) => {
+    this.setState({ selectedTestId });
+  };
+
+  render() {
+    const { selectedTestId, sort = 'name', sortAsc = true, testCases } = this.state;
+
+    if (!testCases) {
+      return null;
+    }
+
+    const selectedTest = testCases.find(test => test.id === selectedTestId);
+
+    return (
+      <div className="source-viewer-measures">
+        <div className="source-viewer-measures-section source-viewer-measures-section-big">
+          <div className="source-viewer-measures-card source-viewer-measures-card-fixed-height js-test-list">
+            <div className="measures">
+              <table className="source-viewer-tests-list">
+                <tbody>
+                  <tr>
+                    <td className="source-viewer-test-status note" colSpan={3}>
+                      {translate('component_viewer.measure_section.unit_tests')}
+                      <br />
+                      <span className="spacer-right">
+                        {translate('component_viewer.tests.ordered_by')}
+                      </span>
+                      <a
+                        className={classNames('js-sort-tests-by-duration', {
+                          'active-link': sort === 'duration'
+                        })}
+                        data-sort="duration"
+                        href="#"
+                        onClick={this.handleTestCasesSortClick}>
+                        {translate('component_viewer.tests.duration')}
+                      </a>
+                      <span className="slash-separator" />
+                      <a
+                        className={classNames('js-sort-tests-by-name', {
+                          'active-link': sort === 'name'
+                        })}
+                        data-sort="name"
+                        href="#"
+                        onClick={this.handleTestCasesSortClick}>
+                        {translate('component_viewer.tests.test_name')}
+                      </a>
+                      <span className="slash-separator" />
+                      <a
+                        className={classNames('js-sort-tests-by-status', {
+                          'active-link': sort === 'status'
+                        })}
+                        data-sort="status"
+                        href="#"
+                        onClick={this.handleTestCasesSortClick}>
+                        {translate('component_viewer.tests.status')}
+                      </a>
+                    </td>
+                    <td className="source-viewer-test-covered-lines note">
+                      {translate('component_viewer.covered_lines')}
+                    </td>
+                  </tr>
+                  {sortTestCases(testCases, sort, sortAsc).map(testCase => (
+                    <MeasuresOverlayTestCase
+                      key={testCase.id}
+                      onClick={this.handleTestCaseClick}
+                      testCase={testCase}
+                    />
+                  ))}
+                </tbody>
+              </table>
+            </div>
+          </div>
+        </div>
+        {selectedTest && <MeasuresOverlayCoveredFiles testCase={selectedTest} />}
+      </div>
+    );
+  }
+}
+
+function sortTestCases(testCases: TestCase[], sort: string, sortAsc: boolean) {
+  const mainOrder = sortAsc ? 'asc' : 'desc';
+  if (sort === 'duration') {
+    return orderBy(testCases, ['durationInMs', 'name'], [mainOrder, 'asc']);
+  } else if (sort === 'status') {
+    return orderBy(testCases, ['status', 'name'], [mainOrder, 'asc']);
+  } else {
+    return orderBy(testCases, ['name'], [mainOrder]);
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlay-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlay-test.tsx
new file mode 100644 (file)
index 0000000..1f94f13
--- /dev/null
@@ -0,0 +1,172 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 MeasuresOverlay from '../MeasuresOverlay';
+import { SourceViewerFile } from '../../../../app/types';
+import { waitAndUpdate, click } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/issues', () => ({
+  getFacets: () =>
+    Promise.resolve({
+      facets: [
+        {
+          property: 'types',
+          values: [
+            { val: 'CODE_SMELL', count: 2 },
+            { val: 'BUG', count: 1 },
+            { val: 'VULNERABILITY', count: 0 }
+          ]
+        },
+        {
+          property: 'severities',
+          values: [
+            { val: 'MAJOR', count: 1 },
+            { val: 'INFO', count: 2 },
+            { val: 'MINOR', count: 3 },
+            { val: 'CRITICAL', count: 4 },
+            { val: 'BLOCKER', count: 5 }
+          ]
+        },
+        {
+          property: 'tags',
+          values: [
+            { val: 'bad-practice', count: 1 },
+            { val: 'cert', count: 3 },
+            { val: 'design', count: 1 }
+          ]
+        }
+      ]
+    })
+}));
+
+jest.mock('../../../../api/measures', () => ({
+  getMeasures: () =>
+    Promise.resolve([
+      { metric: 'vulnerabilities', value: '0' },
+      { metric: 'complexity', value: '1' },
+      { metric: 'test_errors', value: '1' },
+      { metric: 'comment_lines_density', value: '20.0' },
+      { metric: 'wont_fix_issues', value: '0' },
+      { metric: 'uncovered_lines', value: '1' },
+      { metric: 'functions', value: '1' },
+      { metric: 'duplicated_files', value: '1' },
+      { metric: 'duplicated_blocks', value: '3' },
+      { metric: 'line_coverage', value: '75.0' },
+      { metric: 'duplicated_lines_density', value: '0.0' },
+      { metric: 'comment_lines', value: '2' },
+      { metric: 'ncloc', value: '8' },
+      { metric: 'reliability_rating', value: '1.0' },
+      { metric: 'false_positive_issues', value: '0' },
+      { metric: 'reliability_remediation_effort', value: '0' },
+      { metric: 'code_smells', value: '2' },
+      { metric: 'security_rating', value: '1.0' },
+      { metric: 'test_success_density', value: '100.0' },
+      { metric: 'cognitive_complexity', value: '0' },
+      { metric: 'files', value: '1' },
+      { metric: 'duplicated_lines', value: '0' },
+      { metric: 'lines', value: '18' },
+      { metric: 'classes', value: '1' },
+      { metric: 'bugs', value: '0' },
+      { metric: 'lines_to_cover', value: '4' },
+      { metric: 'sqale_index', value: '40' },
+      { metric: 'sqale_debt_ratio', value: '16.7' },
+      { metric: 'coverage', value: '75.0' },
+      { metric: 'security_remediation_effort', value: '0' },
+      { metric: 'statements', value: '3' },
+      { metric: 'skipped_tests', value: '0' },
+      { metric: 'test_failures', value: '0' }
+    ])
+}));
+
+jest.mock('../../../../api/metrics', () => ({
+  getAllMetrics: () =>
+    Promise.resolve([
+      { key: 'vulnerabilities', type: 'INT', domain: 'Security' },
+      { key: 'complexity', type: 'INT', domain: 'Complexity' },
+      { key: 'test_errors', type: 'INT', domain: 'Tests' },
+      { key: 'comment_lines_density', type: 'PERCENT', domain: 'Size' },
+      { key: 'wont_fix_issues', type: 'INT', domain: 'Issues' },
+      { key: 'uncovered_lines', type: 'INT', domain: 'Coverage' },
+      { key: 'functions', type: 'INT', domain: 'Size' },
+      { key: 'duplicated_files', type: 'INT', domain: 'Duplications' },
+      { key: 'duplicated_blocks', type: 'INT', domain: 'Duplications' },
+      { key: 'line_coverage', type: 'PERCENT', domain: 'Coverage' },
+      { key: 'duplicated_lines_density', type: 'PERCENT', domain: 'Duplications' },
+      { key: 'comment_lines', type: 'INT', domain: 'Size' },
+      { key: 'ncloc', type: 'INT', domain: 'Size' },
+      { key: 'reliability_rating', type: 'RATING', domain: 'Reliability' },
+      { key: 'false_positive_issues', type: 'INT', domain: 'Issues' },
+      { key: 'code_smells', type: 'INT', domain: 'Maintainability' },
+      { key: 'security_rating', type: 'RATING', domain: 'Security' },
+      { key: 'test_success_density', type: 'PERCENT', domain: 'Tests' },
+      { key: 'cognitive_complexity', type: 'INT', domain: 'Complexity' },
+      { key: 'files', type: 'INT', domain: 'Size' },
+      { key: 'duplicated_lines', type: 'INT', domain: 'Duplications' },
+      { key: 'lines', type: 'INT', domain: 'Size' },
+      { key: 'classes', type: 'INT', domain: 'Size' },
+      { key: 'bugs', type: 'INT', domain: 'Reliability' },
+      { key: 'lines_to_cover', type: 'INT', domain: 'Coverage' },
+      { key: 'sqale_index', type: 'WORK_DUR', domain: 'Maintainability' },
+      { key: 'sqale_debt_ratio', type: 'PERCENT', domain: 'Maintainability' },
+      { key: 'coverage', type: 'PERCENT', domain: 'Coverage' },
+      { key: 'statements', type: 'INT', domain: 'Size' },
+      { key: 'skipped_tests', type: 'INT', domain: 'Tests' },
+      { key: 'test_failures', type: 'INT', domain: 'Tests' },
+      // next two must be filtered out
+      { key: 'data', type: 'DATA' },
+      { key: 'hidden', hidden: true }
+    ])
+}));
+
+const sourceViewerFile: SourceViewerFile = {
+  key: 'component-key',
+  measures: {},
+  path: 'src/file.js',
+  project: 'project-key',
+  projectName: 'Project Name',
+  q: 'FIL',
+  subProject: 'sub-project-key',
+  subProjectName: 'Sub-Project Name',
+  uuid: 'abcd123'
+};
+
+it('should render source file', async () => {
+  const wrapper = shallow(
+    <MeasuresOverlay branch="branch" onClose={jest.fn()} sourceViewerFile={sourceViewerFile} />
+  );
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+
+  click(wrapper.find('.js-show-all-measures'));
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render test file', async () => {
+  const wrapper = shallow(
+    <MeasuresOverlay
+      branch="branch"
+      onClose={jest.fn()}
+      sourceViewerFile={{ ...sourceViewerFile, q: 'UTS' }}
+    />
+  );
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayCoveredFiles-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayCoveredFiles-test.tsx
new file mode 100644 (file)
index 0000000..4be9b29
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 MeasuresOverlayCoveredFiles from '../MeasuresOverlayCoveredFiles';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/tests', () => ({
+  getCoveredFiles: () =>
+    Promise.resolve([{ key: 'project:src/file.js', longName: 'src/file.js', coveredLines: 3 }])
+}));
+
+const testCase = {
+  coveredLines: 3,
+  durationInMs: 1,
+  fileId: 'abcd',
+  fileKey: 'project:test.js',
+  fileName: 'test.js',
+  id: 'test-abcd',
+  name: 'should work',
+  status: 'OK'
+};
+
+it('should render OK test', async () => {
+  const wrapper = shallow(<MeasuresOverlayCoveredFiles testCase={testCase} />);
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render ERROR test', async () => {
+  const wrapper = shallow(
+    <MeasuresOverlayCoveredFiles
+      testCase={{ ...testCase, status: 'ERROR', message: 'Something failed' }}
+    />
+  );
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayMeasure-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayMeasure-test.tsx
new file mode 100644 (file)
index 0000000..c16c841
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 MeasuresOverlayMeasure from '../MeasuresOverlayMeasure';
+
+it('should render', () => {
+  expect(
+    shallow(
+      <MeasuresOverlayMeasure
+        measure={{
+          metric: { id: '1', key: 'coverage', name: 'Coverage', type: 'PERCENT' },
+          value: '72'
+        }}
+      />
+    )
+  ).toMatchSnapshot();
+});
+
+it('should render issues icon', () => {
+  expect(
+    shallow(
+      <MeasuresOverlayMeasure
+        measure={{
+          metric: { id: '1', key: 'bugs', name: 'Bugs', type: 'INT' },
+          value: '2'
+        }}
+      />
+    )
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCase-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCase-test.tsx
new file mode 100644 (file)
index 0000000..6c51c7c
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 MeasuresOverlayTestCase from '../MeasuresOverlayTestCase';
+import { click } from '../../../../helpers/testUtils';
+
+const testCase = {
+  coveredLines: 3,
+  durationInMs: 1,
+  fileId: 'abcd',
+  fileKey: 'project:test.js',
+  fileName: 'test.js',
+  id: 'test-abcd',
+  name: 'should work',
+  status: 'OK'
+};
+
+it('should render', () => {
+  const onClick = jest.fn();
+  const wrapper = shallow(<MeasuresOverlayTestCase onClick={onClick} testCase={testCase} />);
+  expect(wrapper).toMatchSnapshot();
+  click(wrapper.find('a'));
+  expect(onClick).toBeCalledWith('test-abcd');
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCases-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCases-test.tsx
new file mode 100644 (file)
index 0000000..7222670
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 MeasuresOverlayTestCases from '../MeasuresOverlayTestCases';
+import { waitAndUpdate, click } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/tests', () => ({
+  getTests: () =>
+    Promise.resolve({
+      tests: [
+        {
+          id: 'AWGub2mGGZxsAttCZwQy',
+          name: 'testAdd_WhichFails',
+          fileKey: 'test:fake-project-for-tests:src/test/java/bar/SimplestTest.java',
+          fileName: 'src/test/java/bar/SimplestTest.java',
+          status: 'FAILURE',
+          durationInMs: 6,
+          coveredLines: 3,
+          message: 'expected:<9> but was:<2>',
+          stacktrace:
+            'java.lang.AssertionError: expected:<9> but was:<2>\n\tat org.junit.Assert.fail(Assert.java:93)\n\tat org.junit.Assert.failNotEquals(Assert.java:647)'
+        },
+        {
+          id: 'AWGub2mGGZxsAttCZwQz',
+          name: 'testAdd_InError',
+          fileKey: 'test:fake-project-for-tests:src/test/java/bar/SimplestTest.java',
+          fileName: 'src/test/java/bar/SimplestTest.java',
+          status: 'ERROR',
+          durationInMs: 2,
+          coveredLines: 3
+        },
+        {
+          id: 'AWGub2mFGZxsAttCZwQx',
+          name: 'testAdd',
+          fileKey: 'test:fake-project-for-tests:src/test/java/bar/SimplestTest.java',
+          fileName: 'src/test/java/bar/SimplestTest.java',
+          status: 'OK',
+          durationInMs: 8,
+          coveredLines: 3
+        }
+      ]
+    })
+}));
+
+it('should render', async () => {
+  const wrapper = shallow(
+    <MeasuresOverlayTestCases branch="branch" componentKey="component-key" />
+  );
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+
+  click(wrapper.find('.js-sort-tests-by-duration'), {
+    currentTarget: { blur() {}, dataset: { sort: 'duration' } }
+  });
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap
new file mode 100644 (file)
index 0000000..bd7eb4c
--- /dev/null
@@ -0,0 +1,1535 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render source file 1`] = `
+<Modal
+  contentLabel=""
+  large={true}
+  onRequestClose={[MockFunction]}
+>
+  <div
+    className="modal-container source-viewer-measures-modal"
+  >
+    <div
+      className="source-viewer-header-component source-viewer-measures-component"
+    >
+      <div
+        className="source-viewer-header-component-project"
+      >
+        <QualifierIcon
+          className="little-spacer-right"
+          qualifier="TRK"
+        />
+        <Link
+          onlyActiveOnIndex={false}
+          style={Object {}}
+          to={
+            Object {
+              "pathname": "/dashboard",
+              "query": Object {
+                "branch": "branch",
+                "id": "project-key",
+              },
+            }
+          }
+        >
+          Project Name
+        </Link>
+        <React.Fragment>
+          <QualifierIcon
+            className="big-spacer-left little-spacer-right"
+            qualifier="BRC"
+          />
+          <Link
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to={
+              Object {
+                "pathname": "/dashboard",
+                "query": Object {
+                  "branch": "branch",
+                  "id": "sub-project-key",
+                },
+              }
+            }
+          >
+            Sub-Project Name
+          </Link>
+        </React.Fragment>
+      </div>
+      <div
+        className="source-viewer-header-component-name"
+      >
+        <QualifierIcon
+          className="little-spacer-right"
+          qualifier="FIL"
+        />
+        src/file.js
+      </div>
+    </div>
+    <React.Fragment>
+      <div
+        className="source-viewer-measures"
+      >
+        <div
+          className="source-viewer-measures-section"
+        >
+          <div
+            className="source-viewer-measures-card"
+          >
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-list"
+              >
+                <MeasuresOverlayMeasure
+                  key="lines"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Size",
+                        "key": "lines",
+                        "type": "INT",
+                      },
+                      "value": "18",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="ncloc"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Size",
+                        "key": "ncloc",
+                        "type": "INT",
+                      },
+                      "value": "8",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="comment_lines"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Size",
+                        "key": "comment_lines",
+                        "type": "INT",
+                      },
+                      "value": "2",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="comment_lines_density"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Size",
+                        "key": "comment_lines_density",
+                        "type": "PERCENT",
+                      },
+                      "value": "20.0",
+                    }
+                  }
+                />
+              </div>
+            </div>
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-list"
+              >
+                <MeasuresOverlayMeasure
+                  key="cognitive_complexity"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Complexity",
+                        "key": "cognitive_complexity",
+                        "type": "INT",
+                      },
+                      "value": "0",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="complexity"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Complexity",
+                        "key": "complexity",
+                        "type": "INT",
+                      },
+                      "value": "1",
+                    }
+                  }
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+        <div
+          className="source-viewer-measures-section"
+        >
+          <div
+            className="source-viewer-measures-card"
+          >
+            <div
+              className="measures"
+            >
+              <div
+                className="measure measure-big"
+                data-metric="sqale_index"
+              >
+                <span
+                  className="measure-value"
+                >
+                  <Measure
+                    metricKey="sqale_index"
+                    metricType="WORK_DUR"
+                    value="40"
+                  />
+                </span>
+                <span
+                  className="measure-name"
+                >
+                  sqale_index
+                </span>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div
+          className="source-viewer-measures-section"
+        >
+          <div
+            className="source-viewer-measures-card"
+          >
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-chart"
+              >
+                <CoverageRating
+                  size="big"
+                  value="75.0"
+                />
+              </div>
+              <div
+                className="measure measure-big"
+                data-metric="coverage"
+              >
+                <span
+                  className="measure-value"
+                >
+                  <Measure
+                    metricKey="coverage"
+                    metricType="PERCENT"
+                    value="75.0"
+                  />
+                </span>
+                <span
+                  className="measure-name"
+                >
+                  coverage
+                </span>
+              </div>
+            </div>
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-list"
+              >
+                <MeasuresOverlayMeasure
+                  key="uncovered_lines"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Coverage",
+                        "key": "uncovered_lines",
+                        "type": "INT",
+                      },
+                      "value": "1",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="lines_to_cover"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Coverage",
+                        "key": "lines_to_cover",
+                        "type": "INT",
+                      },
+                      "value": "4",
+                    }
+                  }
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+        <div
+          className="source-viewer-measures-section"
+        >
+          <div
+            className="source-viewer-measures-card"
+          >
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-chart"
+              >
+                <DuplicationsRating
+                  muted={false}
+                  size="big"
+                  value={0}
+                />
+              </div>
+              <div
+                className="measure measure-big"
+                data-metric="duplicated_lines_density"
+              >
+                <span
+                  className="measure-value"
+                >
+                  <Measure
+                    metricKey="duplicated_lines_density"
+                    metricType="PERCENT"
+                    value="0.0"
+                  />
+                </span>
+                <span
+                  className="measure-name"
+                >
+                  duplicated_lines_density
+                </span>
+              </div>
+            </div>
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-list"
+              >
+                <MeasuresOverlayMeasure
+                  key="duplicated_blocks"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Duplications",
+                        "key": "duplicated_blocks",
+                        "type": "INT",
+                      },
+                      "value": "3",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="duplicated_lines"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Duplications",
+                        "key": "duplicated_lines",
+                        "type": "INT",
+                      },
+                      "value": "0",
+                    }
+                  }
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </React.Fragment>
+    <div
+      className="spacer-top"
+    >
+      <a
+        className="js-show-all-measures"
+        href="#"
+        onClick={[Function]}
+      >
+        component_viewer.show_all_measures
+      </a>
+    </div>
+  </div>
+  <footer
+    className="modal-foot"
+  >
+    <button
+      className="button-link"
+      onClick={[Function]}
+      type="button"
+    >
+      close
+    </button>
+  </footer>
+</Modal>
+`;
+
+exports[`should render source file 2`] = `
+<Modal
+  contentLabel=""
+  large={true}
+  onRequestClose={[MockFunction]}
+>
+  <div
+    className="modal-container source-viewer-measures-modal"
+  >
+    <div
+      className="source-viewer-header-component source-viewer-measures-component"
+    >
+      <div
+        className="source-viewer-header-component-project"
+      >
+        <QualifierIcon
+          className="little-spacer-right"
+          qualifier="TRK"
+        />
+        <Link
+          onlyActiveOnIndex={false}
+          style={Object {}}
+          to={
+            Object {
+              "pathname": "/dashboard",
+              "query": Object {
+                "branch": "branch",
+                "id": "project-key",
+              },
+            }
+          }
+        >
+          Project Name
+        </Link>
+        <React.Fragment>
+          <QualifierIcon
+            className="big-spacer-left little-spacer-right"
+            qualifier="BRC"
+          />
+          <Link
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to={
+              Object {
+                "pathname": "/dashboard",
+                "query": Object {
+                  "branch": "branch",
+                  "id": "sub-project-key",
+                },
+              }
+            }
+          >
+            Sub-Project Name
+          </Link>
+        </React.Fragment>
+      </div>
+      <div
+        className="source-viewer-header-component-name"
+      >
+        <QualifierIcon
+          className="little-spacer-right"
+          qualifier="FIL"
+        />
+        src/file.js
+      </div>
+    </div>
+    <React.Fragment>
+      <div
+        className="source-viewer-measures"
+      >
+        <div
+          className="source-viewer-measures-section"
+        >
+          <div
+            className="source-viewer-measures-card"
+          >
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-list"
+              >
+                <MeasuresOverlayMeasure
+                  key="lines"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Size",
+                        "key": "lines",
+                        "type": "INT",
+                      },
+                      "value": "18",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="ncloc"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Size",
+                        "key": "ncloc",
+                        "type": "INT",
+                      },
+                      "value": "8",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="comment_lines"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Size",
+                        "key": "comment_lines",
+                        "type": "INT",
+                      },
+                      "value": "2",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="comment_lines_density"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Size",
+                        "key": "comment_lines_density",
+                        "type": "PERCENT",
+                      },
+                      "value": "20.0",
+                    }
+                  }
+                />
+              </div>
+            </div>
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-list"
+              >
+                <MeasuresOverlayMeasure
+                  key="cognitive_complexity"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Complexity",
+                        "key": "cognitive_complexity",
+                        "type": "INT",
+                      },
+                      "value": "0",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="complexity"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Complexity",
+                        "key": "complexity",
+                        "type": "INT",
+                      },
+                      "value": "1",
+                    }
+                  }
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+        <div
+          className="source-viewer-measures-section"
+        >
+          <div
+            className="source-viewer-measures-card"
+          >
+            <div
+              className="measures"
+            >
+              <div
+                className="measure measure-big"
+                data-metric="sqale_index"
+              >
+                <span
+                  className="measure-value"
+                >
+                  <Measure
+                    metricKey="sqale_index"
+                    metricType="WORK_DUR"
+                    value="40"
+                  />
+                </span>
+                <span
+                  className="measure-name"
+                >
+                  sqale_index
+                </span>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div
+          className="source-viewer-measures-section"
+        >
+          <div
+            className="source-viewer-measures-card"
+          >
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-chart"
+              >
+                <CoverageRating
+                  size="big"
+                  value="75.0"
+                />
+              </div>
+              <div
+                className="measure measure-big"
+                data-metric="coverage"
+              >
+                <span
+                  className="measure-value"
+                >
+                  <Measure
+                    metricKey="coverage"
+                    metricType="PERCENT"
+                    value="75.0"
+                  />
+                </span>
+                <span
+                  className="measure-name"
+                >
+                  coverage
+                </span>
+              </div>
+            </div>
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-list"
+              >
+                <MeasuresOverlayMeasure
+                  key="uncovered_lines"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Coverage",
+                        "key": "uncovered_lines",
+                        "type": "INT",
+                      },
+                      "value": "1",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="lines_to_cover"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Coverage",
+                        "key": "lines_to_cover",
+                        "type": "INT",
+                      },
+                      "value": "4",
+                    }
+                  }
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+        <div
+          className="source-viewer-measures-section"
+        >
+          <div
+            className="source-viewer-measures-card"
+          >
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-chart"
+              >
+                <DuplicationsRating
+                  muted={false}
+                  size="big"
+                  value={0}
+                />
+              </div>
+              <div
+                className="measure measure-big"
+                data-metric="duplicated_lines_density"
+              >
+                <span
+                  className="measure-value"
+                >
+                  <Measure
+                    metricKey="duplicated_lines_density"
+                    metricType="PERCENT"
+                    value="0.0"
+                  />
+                </span>
+                <span
+                  className="measure-name"
+                >
+                  duplicated_lines_density
+                </span>
+              </div>
+            </div>
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-list"
+              >
+                <MeasuresOverlayMeasure
+                  key="duplicated_blocks"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Duplications",
+                        "key": "duplicated_blocks",
+                        "type": "INT",
+                      },
+                      "value": "3",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="duplicated_lines"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Duplications",
+                        "key": "duplicated_lines",
+                        "type": "INT",
+                      },
+                      "value": "0",
+                    }
+                  }
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </React.Fragment>
+    <div
+      className="spacer-top"
+    >
+      <div
+        className="source-viewer-measures source-viewer-measures-secondary js-all-measures"
+      >
+        <div
+          className="source-viewer-measures-section source-viewer-measures-section-big"
+        >
+          <div
+            className="source-viewer-measures-card"
+            key="Complexity"
+          >
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-list"
+              >
+                <div
+                  className="measure measure-one-line measure-big"
+                >
+                  <span
+                    className="measure-name"
+                  >
+                    Complexity
+                  </span>
+                </div>
+                <MeasuresOverlayMeasure
+                  key="cognitive_complexity"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Complexity",
+                        "key": "cognitive_complexity",
+                        "type": "INT",
+                      },
+                      "value": "0",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="complexity"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Complexity",
+                        "key": "complexity",
+                        "type": "INT",
+                      },
+                      "value": "1",
+                    }
+                  }
+                />
+              </div>
+            </div>
+          </div>
+          <div
+            className="source-viewer-measures-card"
+            key="Size"
+          >
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-list"
+              >
+                <div
+                  className="measure measure-one-line measure-big"
+                >
+                  <span
+                    className="measure-name"
+                  >
+                    Size
+                  </span>
+                </div>
+                <MeasuresOverlayMeasure
+                  key="classes"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Size",
+                        "key": "classes",
+                        "type": "INT",
+                      },
+                      "value": "1",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="comment_lines"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Size",
+                        "key": "comment_lines",
+                        "type": "INT",
+                      },
+                      "value": "2",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="comment_lines_density"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Size",
+                        "key": "comment_lines_density",
+                        "type": "PERCENT",
+                      },
+                      "value": "20.0",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="files"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Size",
+                        "key": "files",
+                        "type": "INT",
+                      },
+                      "value": "1",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="functions"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Size",
+                        "key": "functions",
+                        "type": "INT",
+                      },
+                      "value": "1",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="lines"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Size",
+                        "key": "lines",
+                        "type": "INT",
+                      },
+                      "value": "18",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="ncloc"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Size",
+                        "key": "ncloc",
+                        "type": "INT",
+                      },
+                      "value": "8",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="statements"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Size",
+                        "key": "statements",
+                        "type": "INT",
+                      },
+                      "value": "3",
+                    }
+                  }
+                />
+              </div>
+            </div>
+          </div>
+          <div
+            className="source-viewer-measures-card"
+            key="Coverage"
+          >
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-list"
+              >
+                <div
+                  className="measure measure-one-line measure-big"
+                >
+                  <span
+                    className="measure-name"
+                  >
+                    Coverage
+                  </span>
+                </div>
+                <MeasuresOverlayMeasure
+                  key="coverage"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Coverage",
+                        "key": "coverage",
+                        "type": "PERCENT",
+                      },
+                      "value": "75.0",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="line_coverage"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Coverage",
+                        "key": "line_coverage",
+                        "type": "PERCENT",
+                      },
+                      "value": "75.0",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="lines_to_cover"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Coverage",
+                        "key": "lines_to_cover",
+                        "type": "INT",
+                      },
+                      "value": "4",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="uncovered_lines"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Coverage",
+                        "key": "uncovered_lines",
+                        "type": "INT",
+                      },
+                      "value": "1",
+                    }
+                  }
+                />
+              </div>
+            </div>
+          </div>
+          <div
+            className="source-viewer-measures-card"
+            key="Reliability"
+          >
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-list"
+              >
+                <div
+                  className="measure measure-one-line measure-big"
+                >
+                  <span
+                    className="measure-name"
+                  >
+                    Reliability
+                  </span>
+                </div>
+                <MeasuresOverlayMeasure
+                  key="bugs"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Reliability",
+                        "key": "bugs",
+                        "type": "INT",
+                      },
+                      "value": "0",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="reliability_rating"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Reliability",
+                        "key": "reliability_rating",
+                        "type": "RATING",
+                      },
+                      "value": "1.0",
+                    }
+                  }
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+        <div
+          className="source-viewer-measures-section source-viewer-measures-section-big"
+        >
+          <div
+            className="source-viewer-measures-card"
+            key="Security"
+          >
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-list"
+              >
+                <div
+                  className="measure measure-one-line measure-big"
+                >
+                  <span
+                    className="measure-name"
+                  >
+                    Security
+                  </span>
+                </div>
+                <MeasuresOverlayMeasure
+                  key="security_rating"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Security",
+                        "key": "security_rating",
+                        "type": "RATING",
+                      },
+                      "value": "1.0",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="vulnerabilities"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Security",
+                        "key": "vulnerabilities",
+                        "type": "INT",
+                      },
+                      "value": "0",
+                    }
+                  }
+                />
+              </div>
+            </div>
+          </div>
+          <div
+            className="source-viewer-measures-card"
+            key="Tests"
+          >
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-list"
+              >
+                <div
+                  className="measure measure-one-line measure-big"
+                >
+                  <span
+                    className="measure-name"
+                  >
+                    Tests
+                  </span>
+                </div>
+                <MeasuresOverlayMeasure
+                  key="skipped_tests"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Tests",
+                        "key": "skipped_tests",
+                        "type": "INT",
+                      },
+                      "value": "0",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="test_errors"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Tests",
+                        "key": "test_errors",
+                        "type": "INT",
+                      },
+                      "value": "1",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="test_failures"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Tests",
+                        "key": "test_failures",
+                        "type": "INT",
+                      },
+                      "value": "0",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="test_success_density"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Tests",
+                        "key": "test_success_density",
+                        "type": "PERCENT",
+                      },
+                      "value": "100.0",
+                    }
+                  }
+                />
+              </div>
+            </div>
+          </div>
+          <div
+            className="source-viewer-measures-card"
+            key="Issues"
+          >
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-list"
+              >
+                <div
+                  className="measure measure-one-line measure-big"
+                >
+                  <span
+                    className="measure-name"
+                  >
+                    Issues
+                  </span>
+                </div>
+                <MeasuresOverlayMeasure
+                  key="false_positive_issues"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Issues",
+                        "key": "false_positive_issues",
+                        "type": "INT",
+                      },
+                      "value": "0",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="wont_fix_issues"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Issues",
+                        "key": "wont_fix_issues",
+                        "type": "INT",
+                      },
+                      "value": "0",
+                    }
+                  }
+                />
+              </div>
+            </div>
+          </div>
+          <div
+            className="source-viewer-measures-card"
+            key="Duplications"
+          >
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-list"
+              >
+                <div
+                  className="measure measure-one-line measure-big"
+                >
+                  <span
+                    className="measure-name"
+                  >
+                    Duplications
+                  </span>
+                </div>
+                <MeasuresOverlayMeasure
+                  key="duplicated_blocks"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Duplications",
+                        "key": "duplicated_blocks",
+                        "type": "INT",
+                      },
+                      "value": "3",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="duplicated_files"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Duplications",
+                        "key": "duplicated_files",
+                        "type": "INT",
+                      },
+                      "value": "1",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="duplicated_lines"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Duplications",
+                        "key": "duplicated_lines",
+                        "type": "INT",
+                      },
+                      "value": "0",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="duplicated_lines_density"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Duplications",
+                        "key": "duplicated_lines_density",
+                        "type": "PERCENT",
+                      },
+                      "value": "0.0",
+                    }
+                  }
+                />
+              </div>
+            </div>
+          </div>
+          <div
+            className="source-viewer-measures-card"
+            key="Maintainability"
+          >
+            <div
+              className="measures"
+            >
+              <div
+                className="measures-list"
+              >
+                <div
+                  className="measure measure-one-line measure-big"
+                >
+                  <span
+                    className="measure-name"
+                  >
+                    Maintainability
+                  </span>
+                </div>
+                <MeasuresOverlayMeasure
+                  key="code_smells"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Maintainability",
+                        "key": "code_smells",
+                        "type": "INT",
+                      },
+                      "value": "2",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="sqale_debt_ratio"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Maintainability",
+                        "key": "sqale_debt_ratio",
+                        "type": "PERCENT",
+                      },
+                      "value": "16.7",
+                    }
+                  }
+                />
+                <MeasuresOverlayMeasure
+                  key="sqale_index"
+                  measure={
+                    Object {
+                      "metric": Object {
+                        "domain": "Maintainability",
+                        "key": "sqale_index",
+                        "type": "WORK_DUR",
+                      },
+                      "value": "40",
+                    }
+                  }
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <footer
+    className="modal-foot"
+  >
+    <button
+      className="button-link"
+      onClick={[Function]}
+      type="button"
+    >
+      close
+    </button>
+  </footer>
+</Modal>
+`;
+
+exports[`should render test file 1`] = `
+<Modal
+  contentLabel=""
+  large={true}
+  onRequestClose={[MockFunction]}
+>
+  <div
+    className="modal-container source-viewer-measures-modal"
+  >
+    <div
+      className="source-viewer-header-component source-viewer-measures-component"
+    >
+      <div
+        className="source-viewer-header-component-project"
+      >
+        <QualifierIcon
+          className="little-spacer-right"
+          qualifier="TRK"
+        />
+        <Link
+          onlyActiveOnIndex={false}
+          style={Object {}}
+          to={
+            Object {
+              "pathname": "/dashboard",
+              "query": Object {
+                "branch": "branch",
+                "id": "project-key",
+              },
+            }
+          }
+        >
+          Project Name
+        </Link>
+        <React.Fragment>
+          <QualifierIcon
+            className="big-spacer-left little-spacer-right"
+            qualifier="BRC"
+          />
+          <Link
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to={
+              Object {
+                "pathname": "/dashboard",
+                "query": Object {
+                  "branch": "branch",
+                  "id": "sub-project-key",
+                },
+              }
+            }
+          >
+            Sub-Project Name
+          </Link>
+        </React.Fragment>
+      </div>
+      <div
+        className="source-viewer-header-component-name"
+      >
+        <QualifierIcon
+          className="little-spacer-right"
+          qualifier="UTS"
+        />
+        src/file.js
+      </div>
+    </div>
+    <React.Fragment>
+      <React.Fragment>
+        <div
+          className="source-viewer-measures"
+        >
+          <div
+            className="source-viewer-measures-section"
+          >
+            <div
+              className="source-viewer-measures-card"
+            >
+              <div
+                className="measures"
+              >
+                <div
+                  className="measures-list"
+                >
+                  <MeasuresOverlayMeasure
+                    key="test_success_density"
+                    measure={
+                      Object {
+                        "metric": Object {
+                          "domain": "Tests",
+                          "key": "test_success_density",
+                          "type": "PERCENT",
+                        },
+                        "value": "100.0",
+                      }
+                    }
+                  />
+                  <MeasuresOverlayMeasure
+                    key="test_failures"
+                    measure={
+                      Object {
+                        "metric": Object {
+                          "domain": "Tests",
+                          "key": "test_failures",
+                          "type": "INT",
+                        },
+                        "value": "0",
+                      }
+                    }
+                  />
+                  <MeasuresOverlayMeasure
+                    key="test_errors"
+                    measure={
+                      Object {
+                        "metric": Object {
+                          "domain": "Tests",
+                          "key": "test_errors",
+                          "type": "INT",
+                        },
+                        "value": "1",
+                      }
+                    }
+                  />
+                  <MeasuresOverlayMeasure
+                    key="skipped_tests"
+                    measure={
+                      Object {
+                        "metric": Object {
+                          "domain": "Tests",
+                          "key": "skipped_tests",
+                          "type": "INT",
+                        },
+                        "value": "0",
+                      }
+                    }
+                  />
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+        <MeasuresOverlayTestCases
+          branch="branch"
+          componentKey="component-key"
+        />
+      </React.Fragment>
+    </React.Fragment>
+    <div
+      className="spacer-top"
+    >
+      <a
+        className="js-show-all-measures"
+        href="#"
+        onClick={[Function]}
+      >
+        component_viewer.show_all_measures
+      </a>
+    </div>
+  </div>
+  <footer
+    className="modal-foot"
+  >
+    <button
+      className="button-link"
+      onClick={[Function]}
+      type="button"
+    >
+      close
+    </button>
+  </footer>
+</Modal>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap
new file mode 100644 (file)
index 0000000..c578435
--- /dev/null
@@ -0,0 +1,76 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render ERROR test 1`] = `
+<div
+  className="source-viewer-measures-section source-viewer-measures-section-big js-selected-test"
+>
+  <DeferredSpinner
+    loading={false}
+    timeout={100}
+  >
+    <div
+      className="source-viewer-measures-card source-viewer-measures-card-fixed-height"
+    >
+      <React.Fragment>
+        <div
+          className="bubble-popup-title"
+        >
+          component_viewer.details
+        </div>
+        <pre>
+          Something failed
+        </pre>
+        <pre />
+      </React.Fragment>
+    </div>
+  </DeferredSpinner>
+</div>
+`;
+
+exports[`should render OK test 1`] = `
+<div
+  className="source-viewer-measures-section source-viewer-measures-section-big js-selected-test"
+>
+  <DeferredSpinner
+    loading={false}
+    timeout={100}
+  >
+    <div
+      className="source-viewer-measures-card source-viewer-measures-card-fixed-height"
+    >
+      <React.Fragment>
+        <div
+          className="bubble-popup-title"
+        >
+          component_viewer.transition.covers
+        </div>
+        <div
+          className="bubble-popup-section"
+          key="project:src/file.js"
+        >
+          <Link
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to={
+              Object {
+                "pathname": "/dashboard",
+                "query": Object {
+                  "branch": undefined,
+                  "id": "project:src/file.js",
+                },
+              }
+            }
+          >
+            src/file.js
+          </Link>
+          <span
+            className="note spacer-left"
+          >
+            component_viewer.x_lines_are_covered.3
+          </span>
+        </div>
+      </React.Fragment>
+    </div>
+  </DeferredSpinner>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayMeasure-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayMeasure-test.tsx.snap
new file mode 100644 (file)
index 0000000..4612ee4
--- /dev/null
@@ -0,0 +1,53 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<div
+  className="measure measure-one-line"
+  data-metric="coverage"
+  key="coverage"
+>
+  <span
+    className="measure-name"
+  >
+    Coverage
+  </span>
+  <span
+    className="measure-value"
+  >
+    <Measure
+      metricKey="coverage"
+      metricType="PERCENT"
+      small={true}
+      value="72"
+    />
+  </span>
+</div>
+`;
+
+exports[`should render issues icon 1`] = `
+<div
+  className="measure measure-one-line"
+  data-metric="bugs"
+  key="bugs"
+>
+  <span
+    className="measure-name"
+  >
+    <IssueTypeIcon
+      className="little-spacer-right"
+      query="bugs"
+    />
+    Bugs
+  </span>
+  <span
+    className="measure-value"
+  >
+    <Measure
+      metricKey="bugs"
+      metricType="INT"
+      small={true}
+      value="2"
+    />
+  </span>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayTestCase-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayTestCase-test.tsx.snap
new file mode 100644 (file)
index 0000000..02553e9
--- /dev/null
@@ -0,0 +1,34 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<tr>
+  <td
+    className="source-viewer-test-status"
+  >
+    <TestStatusIcon
+      status="OK"
+    />
+  </td>
+  <td
+    className="source-viewer-test-duration note"
+  >
+    1ms
+  </td>
+  <td
+    className="source-viewer-test-name"
+  >
+    <a
+      className="js-show-test"
+      href="#"
+      onClick={[Function]}
+    >
+      should work
+    </a>
+  </td>
+  <td
+    className="source-viewer-test-covered-lines note"
+  >
+    3
+  </td>
+</tr>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayTestCases-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayTestCases-test.tsx.snap
new file mode 100644 (file)
index 0000000..78316c3
--- /dev/null
@@ -0,0 +1,247 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<div
+  className="source-viewer-measures"
+>
+  <div
+    className="source-viewer-measures-section source-viewer-measures-section-big"
+  >
+    <div
+      className="source-viewer-measures-card source-viewer-measures-card-fixed-height js-test-list"
+    >
+      <div
+        className="measures"
+      >
+        <table
+          className="source-viewer-tests-list"
+        >
+          <tbody>
+            <tr>
+              <td
+                className="source-viewer-test-status note"
+                colSpan={3}
+              >
+                component_viewer.measure_section.unit_tests
+                <br />
+                <span
+                  className="spacer-right"
+                >
+                  component_viewer.tests.ordered_by
+                </span>
+                <a
+                  className="js-sort-tests-by-duration"
+                  data-sort="duration"
+                  href="#"
+                  onClick={[Function]}
+                >
+                  component_viewer.tests.duration
+                </a>
+                <span
+                  className="slash-separator"
+                />
+                <a
+                  className="js-sort-tests-by-name active-link"
+                  data-sort="name"
+                  href="#"
+                  onClick={[Function]}
+                >
+                  component_viewer.tests.test_name
+                </a>
+                <span
+                  className="slash-separator"
+                />
+                <a
+                  className="js-sort-tests-by-status"
+                  data-sort="status"
+                  href="#"
+                  onClick={[Function]}
+                >
+                  component_viewer.tests.status
+                </a>
+              </td>
+              <td
+                className="source-viewer-test-covered-lines note"
+              >
+                component_viewer.covered_lines
+              </td>
+            </tr>
+            <MeasuresOverlayTestCase
+              key="AWGub2mFGZxsAttCZwQx"
+              onClick={[Function]}
+              testCase={
+                Object {
+                  "coveredLines": 3,
+                  "durationInMs": 8,
+                  "fileKey": "test:fake-project-for-tests:src/test/java/bar/SimplestTest.java",
+                  "fileName": "src/test/java/bar/SimplestTest.java",
+                  "id": "AWGub2mFGZxsAttCZwQx",
+                  "name": "testAdd",
+                  "status": "OK",
+                }
+              }
+            />
+            <MeasuresOverlayTestCase
+              key="AWGub2mGGZxsAttCZwQz"
+              onClick={[Function]}
+              testCase={
+                Object {
+                  "coveredLines": 3,
+                  "durationInMs": 2,
+                  "fileKey": "test:fake-project-for-tests:src/test/java/bar/SimplestTest.java",
+                  "fileName": "src/test/java/bar/SimplestTest.java",
+                  "id": "AWGub2mGGZxsAttCZwQz",
+                  "name": "testAdd_InError",
+                  "status": "ERROR",
+                }
+              }
+            />
+            <MeasuresOverlayTestCase
+              key="AWGub2mGGZxsAttCZwQy"
+              onClick={[Function]}
+              testCase={
+                Object {
+                  "coveredLines": 3,
+                  "durationInMs": 6,
+                  "fileKey": "test:fake-project-for-tests:src/test/java/bar/SimplestTest.java",
+                  "fileName": "src/test/java/bar/SimplestTest.java",
+                  "id": "AWGub2mGGZxsAttCZwQy",
+                  "message": "expected:<9> but was:<2>",
+                  "name": "testAdd_WhichFails",
+                  "stacktrace": "java.lang.AssertionError: expected:<9> but was:<2>
+       at org.junit.Assert.fail(Assert.java:93)
+       at org.junit.Assert.failNotEquals(Assert.java:647)",
+                  "status": "FAILURE",
+                }
+              }
+            />
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render 2`] = `
+<div
+  className="source-viewer-measures"
+>
+  <div
+    className="source-viewer-measures-section source-viewer-measures-section-big"
+  >
+    <div
+      className="source-viewer-measures-card source-viewer-measures-card-fixed-height js-test-list"
+    >
+      <div
+        className="measures"
+      >
+        <table
+          className="source-viewer-tests-list"
+        >
+          <tbody>
+            <tr>
+              <td
+                className="source-viewer-test-status note"
+                colSpan={3}
+              >
+                component_viewer.measure_section.unit_tests
+                <br />
+                <span
+                  className="spacer-right"
+                >
+                  component_viewer.tests.ordered_by
+                </span>
+                <a
+                  className="js-sort-tests-by-duration active-link"
+                  data-sort="duration"
+                  href="#"
+                  onClick={[Function]}
+                >
+                  component_viewer.tests.duration
+                </a>
+                <span
+                  className="slash-separator"
+                />
+                <a
+                  className="js-sort-tests-by-name"
+                  data-sort="name"
+                  href="#"
+                  onClick={[Function]}
+                >
+                  component_viewer.tests.test_name
+                </a>
+                <span
+                  className="slash-separator"
+                />
+                <a
+                  className="js-sort-tests-by-status"
+                  data-sort="status"
+                  href="#"
+                  onClick={[Function]}
+                >
+                  component_viewer.tests.status
+                </a>
+              </td>
+              <td
+                className="source-viewer-test-covered-lines note"
+              >
+                component_viewer.covered_lines
+              </td>
+            </tr>
+            <MeasuresOverlayTestCase
+              key="AWGub2mGGZxsAttCZwQz"
+              onClick={[Function]}
+              testCase={
+                Object {
+                  "coveredLines": 3,
+                  "durationInMs": 2,
+                  "fileKey": "test:fake-project-for-tests:src/test/java/bar/SimplestTest.java",
+                  "fileName": "src/test/java/bar/SimplestTest.java",
+                  "id": "AWGub2mGGZxsAttCZwQz",
+                  "name": "testAdd_InError",
+                  "status": "ERROR",
+                }
+              }
+            />
+            <MeasuresOverlayTestCase
+              key="AWGub2mGGZxsAttCZwQy"
+              onClick={[Function]}
+              testCase={
+                Object {
+                  "coveredLines": 3,
+                  "durationInMs": 6,
+                  "fileKey": "test:fake-project-for-tests:src/test/java/bar/SimplestTest.java",
+                  "fileName": "src/test/java/bar/SimplestTest.java",
+                  "id": "AWGub2mGGZxsAttCZwQy",
+                  "message": "expected:<9> but was:<2>",
+                  "name": "testAdd_WhichFails",
+                  "stacktrace": "java.lang.AssertionError: expected:<9> but was:<2>
+       at org.junit.Assert.fail(Assert.java:93)
+       at org.junit.Assert.failNotEquals(Assert.java:647)",
+                  "status": "FAILURE",
+                }
+              }
+            />
+            <MeasuresOverlayTestCase
+              key="AWGub2mFGZxsAttCZwQx"
+              onClick={[Function]}
+              testCase={
+                Object {
+                  "coveredLines": 3,
+                  "durationInMs": 8,
+                  "fileKey": "test:fake-project-for-tests:src/test/java/bar/SimplestTest.java",
+                  "fileName": "src/test/java/bar/SimplestTest.java",
+                  "id": "AWGub2mFGZxsAttCZwQx",
+                  "name": "testAdd",
+                  "status": "OK",
+                }
+              }
+            />
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </div>
+</div>
+`;
index 2c98827855f18cbcf3700502986822804d2dd59e..3bcdcdf4415227e481f4a7fa42c59620fbb2eb6e 100644 (file)
 
 .measure-big .measure-value {
   font-size: 22px;
-  font-weight: 300;
 }
 
 .measure-big .rating {
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js b/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js
deleted file mode 100644 (file)
index 925e533..0000000
+++ /dev/null
@@ -1,282 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 $ from 'jquery';
-import { select } from 'd3-selection';
-import { arc as d3Arc, pie as d3Pie } from 'd3-shape';
-import { groupBy, sortBy, toPairs } from 'lodash';
-import Template from './templates/source-viewer-measures.hbs';
-import ModalView from '../../common/modals';
-import { searchIssues } from '../../../api/issues';
-import { getMeasures } from '../../../api/measures';
-import { getAllMetrics } from '../../../api/metrics';
-import { getTests, getCoveredFiles } from '../../../api/tests';
-import * as theme from '../../../app/theme';
-import { getLocalizedMetricName, getLocalizedMetricDomain } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
-
-const severityComparator = severity => {
-  const SEVERITIES_ORDER = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'];
-  return SEVERITIES_ORDER.indexOf(severity);
-};
-
-export default ModalView.extend({
-  template: Template,
-  testsOrder: ['ERROR', 'FAILURE', 'OK', 'SKIPPED'],
-
-  initialize() {
-    this.testsScroll = 0;
-    const requests = [this.requestMeasures(), this.requestIssues()];
-    if (this.options.component.q === 'UTS') {
-      requests.push(this.requestTests());
-    }
-    Promise.all(requests).then(() => this.render());
-  },
-
-  events() {
-    return {
-      ...ModalView.prototype.events.apply(this, arguments),
-      'click .js-sort-tests-by-duration': 'sortTestsByDuration',
-      'click .js-sort-tests-by-name': 'sortTestsByName',
-      'click .js-sort-tests-by-status': 'sortTestsByStatus',
-      'click .js-show-test': 'showTest',
-      'click .js-show-all-measures': 'showAllMeasures'
-    };
-  },
-
-  initPieChart() {
-    const trans = function(left, top) {
-      return `translate(${left}, ${top})`;
-    };
-
-    const defaults = {
-      size: 40,
-      thickness: 8,
-      color: '#1f77b4',
-      baseColor: theme.barBorderColor
-    };
-
-    this.$('.js-pie-chart').each(function() {
-      const data = [$(this).data('value'), $(this).data('max') - $(this).data('value')];
-      const options = { ...defaults, ...$(this).data() };
-      const radius = options.size / 2;
-
-      const container = select(this);
-      const svg = container
-        .append('svg')
-        .attr('width', options.size)
-        .attr('height', options.size);
-      const plot = svg.append('g').attr('transform', trans(radius, radius));
-      const arc = d3Arc()
-        .innerRadius(radius - options.thickness)
-        .outerRadius(radius);
-      const pie = d3Pie()
-        .sort(null)
-        .value(d => d);
-      const colors = function(i) {
-        return i === 0 ? options.color : options.baseColor;
-      };
-      const sectors = plot.selectAll('path').data(pie(data));
-
-      sectors
-        .enter()
-        .append('path')
-        .style('fill', (d, i) => colors(i))
-        .attr('d', arc);
-    });
-  },
-
-  onRender() {
-    ModalView.prototype.onRender.apply(this, arguments);
-    this.initPieChart();
-    this.$('.js-test-list').scrollTop(this.testsScroll);
-  },
-
-  calcAdditionalMeasures(measures) {
-    measures.issuesRemediationEffort =
-      (Number(measures.sqale_index_raw) || 0) +
-      (Number(measures.reliability_remediation_effort_raw) || 0) +
-      (Number(measures.security_remediation_effort_raw) || 0);
-
-    if (measures.lines_to_cover && measures.uncovered_lines) {
-      measures.covered_lines = measures.lines_to_cover_raw - measures.uncovered_lines_raw;
-    }
-    if (measures.conditions_to_cover && measures.uncovered_conditions) {
-      measures.covered_conditions = measures.conditions_to_cover - measures.uncovered_conditions;
-    }
-    return measures;
-  },
-
-  prepareMetrics(metrics) {
-    metrics = metrics
-      .filter(metric => metric.value != null)
-      .map(metric => ({ ...metric, name: getLocalizedMetricName(metric) }));
-    return sortBy(
-      toPairs(groupBy(metrics, 'domain')).map(domain => {
-        return {
-          name: getLocalizedMetricDomain(domain[0]),
-          metrics: domain[1]
-        };
-      }),
-      'name'
-    );
-  },
-
-  requestMeasures() {
-    return getAllMetrics().then(metrics => {
-      const metricsToRequest = metrics
-        .filter(metric => metric.type !== 'DATA' && !metric.hidden)
-        .map(metric => metric.key);
-
-      return getMeasures(this.options.component.key, metricsToRequest, this.options.branch).then(
-        measures => {
-          let nextMeasures = this.options.component.measures || {};
-          measures.forEach(measure => {
-            const metric = metrics.find(metric => metric.key === measure.metric);
-            nextMeasures[metric.key] = formatMeasure(measure.value, metric.type);
-            nextMeasures[metric.key + '_raw'] = measure.value;
-            metric.value = nextMeasures[metric.key];
-          });
-          nextMeasures = this.calcAdditionalMeasures(nextMeasures);
-          this.measures = nextMeasures;
-          this.measuresToDisplay = this.prepareMetrics(metrics);
-        },
-        () => {}
-      );
-    });
-  },
-
-  requestIssues() {
-    const options = {
-      branch: this.options.branch,
-      componentKeys: this.options.component.key,
-      resolved: false,
-      ps: 1,
-      facets: 'types,severities,tags'
-    };
-
-    return searchIssues(options).then(
-      data => {
-        const typesFacet = data.facets.find(facet => facet.property === 'types').values;
-        const typesOrder = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
-        const sortedTypesFacet = sortBy(typesFacet, v => typesOrder.indexOf(v.val));
-
-        const severitiesFacet = data.facets.find(facet => facet.property === 'severities').values;
-        const sortedSeveritiesFacet = sortBy(severitiesFacet, facet =>
-          severityComparator(facet.val)
-        );
-
-        const tagsFacet = data.facets.find(facet => facet.property === 'tags').values;
-
-        this.tagsFacet = tagsFacet;
-        this.typesFacet = sortedTypesFacet;
-        this.severitiesFacet = sortedSeveritiesFacet;
-        this.issuesCount = data.total;
-      },
-      () => {}
-    );
-  },
-
-  requestTests() {
-    return getTests({ branch: this.options.branch, testFileKey: this.options.component.key }).then(
-      data => {
-        this.tests = data.tests;
-        this.testSorting = 'status';
-        this.testAsc = true;
-        this.sortTests(test => `${this.testsOrder.indexOf(test.status)}_______${test.name}`);
-      },
-      () => {}
-    );
-  },
-
-  sortTests(condition) {
-    let tests = this.tests;
-    if (Array.isArray(tests)) {
-      tests = sortBy(tests, condition);
-      if (!this.testAsc) {
-        tests.reverse();
-      }
-      this.tests = tests;
-    }
-  },
-
-  sortTestsByDuration() {
-    if (this.testSorting === 'duration') {
-      this.testAsc = !this.testAsc;
-    }
-    this.sortTests('durationInMs');
-    this.testSorting = 'duration';
-    this.render();
-  },
-
-  sortTestsByName() {
-    if (this.testSorting === 'name') {
-      this.testAsc = !this.testAsc;
-    }
-    this.sortTests('name');
-    this.testSorting = 'name';
-    this.render();
-  },
-
-  sortTestsByStatus() {
-    if (this.testSorting === 'status') {
-      this.testAsc = !this.testAsc;
-    }
-    this.sortTests(test => `${this.testsOrder.indexOf(test.status)}_______${test.name}`);
-    this.testSorting = 'status';
-    this.render();
-  },
-
-  showTest(e) {
-    const testId = $(e.currentTarget).data('id');
-    this.testsScroll = $(e.currentTarget)
-      .scrollParent()
-      .scrollTop();
-    getCoveredFiles({ testId }).then(
-      data => {
-        this.coveredFiles = data.files;
-        this.selectedTest = this.tests.find(test => test.id === testId);
-        this.render();
-      },
-      () => {}
-    );
-  },
-
-  showAllMeasures() {
-    this.$('.js-all-measures').removeClass('hidden');
-    this.$('.js-show-all-measures').remove();
-  },
-
-  serializeData() {
-    return {
-      ...ModalView.prototype.serializeData.apply(this, arguments),
-      ...this.options.component,
-      measures: this.measures,
-      measuresToDisplay: this.measuresToDisplay,
-      tests: this.tests,
-      tagsFacet: this.tagsFacet,
-      typesFacet: this.typesFacet,
-      severitiesFacet: this.severitiesFacet,
-      issuesCount: this.issuesCount,
-      testSorting: this.testSorting,
-      selectedTest: this.selectedTest,
-      coveredFiles: this.coveredFiles || []
-    };
-  }
-});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-all.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-all.hbs
deleted file mode 100644 (file)
index cac99fe..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-{{#notEmpty measuresToDisplay}}
-  <div class="source-viewer-measures-section source-viewer-measures-section-big">
-    {{#eachEven measuresToDisplay}}
-      <div class="source-viewer-measures-card">
-        <div class="measures">
-          <div class="measures-list">
-            <div class="measure measure-one-line measure-big">
-              <span class="measure-name">{{name}}</span>
-            </div>
-            {{#each metrics}}
-                <div class="measure measure-one-line" data-metric="{{key}}">
-                  <span class="measure-name">{{#eq key 'bugs'}}{{issueTypeIcon 'BUG'}} {{/eq}}{{#eq key 'vulnerabilities'}}{{issueTypeIcon 'VULNERABILITY'}} {{/eq}}{{#eq key 'code_smells'}}{{issueTypeIcon 'CODE_SMELL'}} {{/eq}}{{name}}</span>
-                  <span class="measure-value">&nbsp;{{value}}</span>
-                </div>
-            {{/each}}
-          </div>
-        </div>
-      </div>
-    {{/eachEven}}
-  </div>
-
-  <div class="source-viewer-measures-section source-viewer-measures-section-big">
-    {{#eachOdd measuresToDisplay}}
-      <div class="source-viewer-measures-card">
-        <div class="measures">
-          <div class="measures-list">
-            <div class="measure measure-one-line measure-big">
-              <span class="measure-name">{{name}}</span>
-            </div>
-            {{#each metrics}}
-              <div class="measure measure-one-line" data-metric="{{key}}">
-                <span class="measure-name">{{#eq key 'bugs'}}{{issueTypeIcon 'BUG'}} {{/eq}}{{#eq key 'vulnerabilities'}}{{issueTypeIcon 'VULNERABILITY'}} {{/eq}}{{#eq key 'code_smells'}}{{issueTypeIcon 'CODE_SMELL'}} {{/eq}}{{name}}</span>
-                <span class="measure-value">&nbsp;{{value}}</span>
-              </div>
-            {{/each}}
-          </div>
-        </div>
-      </div>
-    {{/eachOdd}}
-  </div>
-{{/notEmpty}}
-
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-coverage.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-coverage.hbs
deleted file mode 100644 (file)
index 2598e96..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-{{#if measures.coverage}}
-  <div class="measures">
-    <div class="measures-chart">
-      <span class="js-pie-chart"
-            data-value="{{measures.coverage_raw}}"
-            data-max="100"
-            data-color="#00aa00"
-            data-base-color="#d4333f"
-            data-size="47"></span>
-    </div>
-    <div class="measure measure-big" data-metric="coverage">
-      <span class="measure-value">{{measures.coverage}}</span>
-      <span class="measure-name">{{t 'metric.coverage.name'}}</span>
-    </div>
-  </div>
-
-  {{#any measures.covered_lines measures.lines_to_cover measures.covered_conditions measures.conditions_to_cover}}
-    <div class="measures">
-      <div class="measures-list">
-        <div class="measure measure-one-line">
-          <span class="measure-name">Covered by Tests</span>
-        </div>
-        <div class="measure measure-one-line" data-metric="lines_to_cover">
-          <span class="measure-name">Lines</span>
-          <span class="measure-value">{{formatMeasure measures.covered_lines 'INT'}}/{{measures.lines_to_cover}}</span>
-        </div>
-        {{#if measures.conditions_to_cover}}
-          <div class="measure measure-one-line" data-metric="conditions_to_cover">
-            <span class="measure-name">Conditions</span>
-            <span class="measure-value">{{default measures.covered_conditions 0}}/{{measures.conditions_to_cover}}</span>
-          </div>
-        {{/if}}
-      </div>
-    </div>
-  {{/any}}
-{{/if}}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-duplications.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-duplications.hbs
deleted file mode 100644 (file)
index 86ddc9e..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-{{#notNull measures.duplicated_lines_density}}
-  <div class="source-viewer-measures-card">
-    <div class="measures">
-      <div class="measures-chart">
-          <span class="js-pie-chart"
-                data-value="{{measures.duplicated_lines_density_raw}}"
-                data-max="100"
-                data-size="50"
-                data-color="#797979"></span>
-      </div>
-      <div class="measure measure-big" data-metric="duplicated_lines_density">
-        <span class="measure-value">{{measures.duplicated_lines_density}}</span>
-        <span class="measure-name">{{t 'metric.duplicated_lines_density.short_name'}}</span>
-      </div>
-    </div>
-
-    <div class="measures">
-      <div class="measures-list">
-        <div class="measure measure-one-line" data-metric="duplicated_blocks">
-          <span class="measure-name">{{t 'metric.duplicated_blocks.name'}}</span>
-          <span class="measure-value">{{measures.duplicated_blocks}}</span>
-        </div>
-        <div class="measure measure-one-line" data-metric="duplicated_lines">
-          <span class="measure-name">{{t 'metric.duplicated_lines.name'}}</span>
-          <span class="measure-value">{{measures.duplicated_lines}}</span>
-        </div>
-      </div>
-    </div>
-  </div>
-{{/notNull}}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-issues.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-issues.hbs
deleted file mode 100644 (file)
index 30a3914..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-<div class="source-viewer-measures-card">
-  <div class="measures">
-    <div class="measure measure-big" data-metric="violations">
-      <span class="measure-value">{{default issuesCount 0}}</span>
-      <span class="measure-name">{{t 'metric.violations.name'}}</span>
-    </div>
-    <div class="measure measure-big" data-metric="sqale_index">
-      <span class="measure-value">{{formatMeasure measures.issuesRemediationEffort 'SHORT_WORK_DUR'}}</span>
-      <span class="measure-name">{{t 'metric.sqale_index.short_name'}}</span>
-    </div>
-  </div>
-
-  {{#if issuesCount}}
-    <div class="measures">
-      <div class="measures-list">
-        {{#each typesFacet}}
-          <div class="measure measure-one-line">
-            <span class="measure-name">{{issueTypeIcon val}} {{issueType val}}</span>
-            <span class="measure-value">{{formatMeasure count 'SHORT_INT'}}</span>
-          </div>
-        {{/each}}
-      </div>
-    </div>
-
-    <div class="measures">
-      <div class="measures-list">
-        {{#each severitiesFacet}}
-          <div class="measure measure-one-line">
-            <span class="measure-name">{{severityHelper val}}</span>
-            <span class="measure-value">{{formatMeasure count 'SHORT_INT'}}</span>
-          </div>
-        {{/each}}
-      </div>
-    </div>
-
-    <div class="measures">
-      <div class="measures-list">
-        {{#each tagsFacet}}
-          <div class="measure measure-one-line">
-            <span class="measure-name"><i class="icon-tags"></i>&nbsp;{{val}}</span>
-            <span class="measure-value">{{formatMeasure count 'SHORT_INT'}}</span>
-          </div>
-        {{/each}}
-      </div>
-    </div>
-  {{/if}}
-</div>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-lines.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-lines.hbs
deleted file mode 100644 (file)
index 2846426..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-<div class="measures">
-  <div class="measures-list">
-    <div class="measure measure-one-line" data-metric="lines">
-      <span class="measure-name">{{t 'metric.lines.name'}}</span>
-      <span class="measure-value">{{measures.lines}}</span>
-    </div>
-    <div class="measure measure-one-line" data-metric="ncloc">
-      <span class="measure-name">{{t 'metric.ncloc.name'}}</span>
-      <span class="measure-value">{{measures.ncloc}}</span>
-    </div>
-    <div class="measure measure-one-line" data-metric="comment_lines">
-      <span class="measure-name">{{t 'metric.comment_lines_density.short_name'}}</span>
-      <span class="measure-value">{{measures.comment_lines_density}} / {{measures.comment_lines}}</span>
-    </div>
-  </div>
-</div>
-
-<div class="measures">
-  <div class="measures-list">
-    <div class="measure measure-one-line" data-metric="complexity">
-      <span class="measure-name">{{t 'metric.complexity.name'}}</span>
-      <span class="measure-value">{{measures.complexity}}</span>
-    </div>
-    <div class="measure measure-one-line" data-metric="function_complexity">
-      <span class="measure-name">{{t 'metric.function_complexity.name'}}</span>
-      <span class="measure-value">{{measures.function_complexity}}</span>
-    </div>
-  </div>
-</div>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-test-cases.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-test-cases.hbs
deleted file mode 100644 (file)
index 894ab99..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-<div class="source-viewer-measures-section source-viewer-measures-section-big">
-  <div class="source-viewer-measures-card source-viewer-measures-card-fixed-height js-test-list">
-    <div class="measures">
-      <table class="source-viewer-tests-list">
-        <tr>
-          <td class="source-viewer-test-status note" colspan="3">
-            {{t 'component_viewer.measure_section.unit_tests'}}<br>
-            {{t 'component_viewer.tests.ordered_by'}}
-            <a class="js-sort-tests-by-duration {{#eq testSorting 'duration'}}active-link{{/eq}}">
-              {{t 'component_viewer.tests.duration'}}</a>
-            /
-            <a class="js-sort-tests-by-name {{#eq testSorting 'name'}}active-link{{/eq}}">
-              {{t 'component_viewer.tests.test_name'}}</a>
-            /
-            <a class="js-sort-tests-by-status {{#eq testSorting 'status'}}active-link{{/eq}}">
-              {{t 'component_viewer.tests.status'}}</a>
-          </td>
-          <td class="source-viewer-test-covered-lines note">{{t 'component_viewer.covered_lines'}}</td>
-        </tr>
-        {{#each tests}}
-          <tr>
-            {{#eq status 'SKIPPED'}}
-              <td class="source-viewer-test-status note">{{testStatusIcon status}}</td>
-              <td class="source-viewer-test-duration note"></td>
-              <td class="source-viewer-test-name">{{name}}</td>
-              <td class="source-viewer-test-covered-lines note"></td>
-            {{else}}
-              {{#ifTestData this}}
-                <td class="source-viewer-test-status note">{{testStatusIcon status}}</td>
-                <td class="source-viewer-test-duration note">{{durationInMs}}ms</td>
-                <td class="source-viewer-test-name"><a class="js-show-test" data-id="{{id}}">{{name}}</a></td>
-                <td class="source-viewer-test-covered-lines note">{{coveredLines}}</td>
-              {{else}}
-                <td class="source-viewer-test-status note">{{testStatusIcon status}}</td>
-                <td class="source-viewer-test-duration note">{{durationInMs}}ms</td>
-                <td class="source-viewer-test-name">{{name}}</td>
-              {{/ifTestData}}
-            {{/eq}}
-          </tr>
-        {{/each}}
-      </table>
-    </div>
-  </div>
-</div>
-
-{{#if selectedTest}}
-  <div class="source-viewer-measures-section source-viewer-measures-section-big js-selected-test">
-    <div class="source-viewer-measures-card source-viewer-measures-card-fixed-height">
-      {{#notEq selectedTest.status 'ERROR'}}
-        {{#notEq selectedTest.status 'FAILURE'}}
-          <div class="bubble-popup-title">{{t 'component_viewer.transition.covers'}}</div>
-          {{#each coveredFiles}}
-            <div class="bubble-popup-section">
-              <a target="_blank" href="{{dashboardUrl key}}" title="{{longName}}">{{longName}}</a>
-              <span class="note">{{tp 'component_viewer.x_lines_are_covered' coveredLines}}</span>
-            </div>
-          {{else}}
-            {{t 'none'}}
-          {{/each}}
-        {{/notEq}}
-      {{/notEq}}
-
-      {{#notEq selectedTest.status 'OK'}}
-        <div class="bubble-popup-title">{{t 'component_viewer.details'}}</div>
-        {{#if selectedTest.message}}
-          <pre>{{selectedTest.message}}</pre>
-        {{/if}}
-        <pre>{{selectedTest.stacktrace}}</pre>
-      {{/notEq}}
-    </div>
-  </div>
-{{/if}}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-tests.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-tests.hbs
deleted file mode 100644 (file)
index c9b33c3..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-<div class="source-viewer-measures-card">
-  <div class="measures">
-    <div class="measures-list">
-      <div class="measure measure-big" data-metric="tests">
-        <span class="measure-name">{{t 'metric.tests.name'}}</span>
-        <span class="measure-value">{{measures.tests}}</span>
-      </div>
-      {{#notNull measures.test_success_density}}
-        <div class="measure measure-one-line" data-metric="test_success_density">
-          <span class="measure-name">{{t 'metric.test_success_density.name'}}</span>
-          <span class="measure-value">{{measures.test_success_density}}</span>
-        </div>
-      {{/notNull}}
-      {{#notNull measures.test_failures}}
-        <div class="measure measure-one-line" data-metric="test_failures">
-          <span class="measure-name">{{t 'metric.test_failures.name'}}</span>
-          <span class="measure-value">{{measures.test_failures}}</span>
-        </div>
-      {{/notNull}}
-      {{#notNull measures.test_errors}}
-        <div class="measure measure-one-line" data-metric="test_errors">
-          <span class="measure-name">{{t 'metric.test_errors.name'}}</span>
-          <span class="measure-value">{{measures.test_errors}}</span>
-        </div>
-      {{/notNull}}
-      {{#notNull measures.skipped_tests}}
-        <div class="measure measure-one-line" data-metric="skipped_tests">
-          <span class="measure-name">{{t 'metric.skipped_tests.name'}}</span>
-          <span class="measure-value">{{measures.skipped_tests}}</span>
-        </div>
-      {{/notNull}}
-      {{#notNull measures.test_execution_time}}
-        <div class="measure measure-one-line" data-metric="test_execution_time">
-          <span class="measure-name">{{t 'metric.test_execution_time.name'}}</span>
-          <span class="measure-value">{{measures.test_execution_time}}</span>
-        </div>
-      {{/notNull}}
-    </div>
-  </div>
-</div>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/source-viewer-measures.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/source-viewer-measures.hbs
deleted file mode 100644 (file)
index ebcc2e7..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-<div class="modal-container source-viewer-measures-modal">
-  <div class="source-viewer-header-component source-viewer-measures-component">
-    {{#unless removed}}
-      {{#if projectName}}
-        <div class="source-viewer-header-component-project">
-          {{qualifierIcon 'TRK'}}&nbsp;<a href="{{dashboardUrl project}}">{{projectName}}</a>
-          {{#if subProjectName}}
-            &nbsp;&nbsp;&nbsp;
-            {{qualifierIcon 'TRK'}}&nbsp;<a href="{{dashboardUrl subProject}}">{{subProjectName}}</a>
-          {{/if}}
-        </div>
-      {{/if}}
-
-      <div class="source-viewer-header-component-name">
-        {{qualifierIcon q}} {{default path longName}}
-      </div>
-    {{else}}
-      <div class="source-viewer-header-component-project removed">{{removedMessage}}</div>
-    {{/unless}}
-  </div>
-
-  {{#eq q 'UTS'}}
-    <div class="source-viewer-measures">
-      <div class="source-viewer-measures-section">
-        {{> '_source-viewer-measures-tests'}}
-      </div>
-    </div>
-    <div class="source-viewer-measures">
-      {{> '_source-viewer-measures-test-cases'}}
-    </div>
-  {{else}}
-    <div class="source-viewer-measures">
-      <div class="source-viewer-measures-section">
-        <div class="source-viewer-measures-card">
-          {{> '_source-viewer-measures-lines'}}
-        </div>
-      </div>
-
-      <div class="source-viewer-measures-section">
-        {{> '_source-viewer-measures-issues'}}
-      </div>
-
-      {{#if measures.coverage}}
-        <div class="source-viewer-measures-section">
-          <div class="source-viewer-measures-card">
-            {{> '_source-viewer-measures-coverage'}}
-          </div>
-        </div>
-      {{/if}}
-
-      <div class="source-viewer-measures-section">
-        {{> '_source-viewer-measures-duplications'}}
-      </div>
-    </div>
-  {{/eq}}
-
-
-  <div class="spacer-bottom">&nbsp;</div>
-  <a class="js-show-all-measures">{{t 'component_viewer.show_all_measures'}}</a>
-
-  <div class="source-viewer-measures source-viewer-measures-secondary js-all-measures hidden">
-    {{> '_source-viewer-measures-all'}}
-  </div>
-</div>
-
-<div class="modal-foot">
-  <a class="js-modal-close" href="#">{{t 'close'}}</a>
-</div>
index 3437fa9616f1e0f30513fd13852b2fef1d86fb13..8cccb3cd5d64308aeaea51d0e0655f54fbebff28 100644 (file)
@@ -27,18 +27,26 @@ import { formatMeasure } from '../../helpers/measures';
 interface Props {
   className?: string;
   decimals?: number | null;
-  value?: string;
   metricKey: string;
   metricType: string;
+  small?: boolean;
+  value: string | undefined;
 }
 
-export default function Measure({ className, decimals, metricKey, metricType, value }: Props) {
+export default function Measure({
+  className,
+  decimals,
+  metricKey,
+  metricType,
+  small,
+  value
+}: Props) {
   if (value === undefined) {
     return <span>{'–'}</span>;
   }
 
   if (metricType === 'LEVEL') {
-    return <Level className={className} level={value} />;
+    return <Level className={className} level={value} small={small} />;
   }
 
   if (metricType !== 'RATING') {
@@ -47,7 +55,7 @@ export default function Measure({ className, decimals, metricKey, metricType, va
   }
 
   const tooltip = getRatingTooltip(metricKey, Number(value));
-  const rating = <Rating value={value} />;
+  const rating = <Rating small={small} value={value} />;
   if (tooltip) {
     return (
       <Tooltips overlay={tooltip}>
index 84aaf15fdb3d835d44a7b8afe6f600c8269f620c..cbfe52cb4bf0fb51a7e3c5a5bcdc67f3ba5ccadd 100644 (file)
@@ -58,5 +58,7 @@ it('renders unknown RATING', () => {
 });
 
 it('renders undefined measure', () => {
-  expect(shallow(<Measure metricKey="foo" metricType="PERCENT" />)).toMatchSnapshot();
+  expect(
+    shallow(<Measure metricKey="foo" metricType="PERCENT" value={undefined} />)
+  ).toMatchSnapshot();
 });
diff --git a/server/sonar-web/src/main/js/components/shared/TestStatusIcon.tsx b/server/sonar-web/src/main/js/components/shared/TestStatusIcon.tsx
new file mode 100644 (file)
index 0000000..c54fca5
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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';
+
+interface Props {
+  className?: string;
+  status: string;
+}
+
+export default function TestStatusIcon({ className, status }: Props) {
+  return <i className={classNames('icon-test-status-' + status.toLowerCase(), className)} />;
+}
index da64a3d66540c1d5db81cd9b60193b1ffcdf2b9e..928fc79a103b5a2ac08f5c14b7a38793fbc43b0d 100644 (file)
@@ -365,3 +365,7 @@ export function getRatingTooltip(metricKey: string, value: number | string): str
     ? getMaintainabilityRatingTooltip(Number(value))
     : translate('metric', finalMetricKey, 'tooltip', ratingLetter);
 }
+
+export function getDisplayMetrics(metrics: Metric[]) {
+  return metrics.filter(metric => !metric.hidden && !['DATA', 'DISTRIB'].includes(metric.type));
+}
index 780c65b1206aed6e75e40fdae23681ffd74c47bc..8144c5c5190198d2ab9eb18a73534a4e165cb827 100644 (file)
@@ -1915,6 +1915,7 @@ metric.usability.description=Usability
 metric.usability.name=Usability
 metric.violations.description=Issues
 metric.violations.name=Issues
+metric.violations.short_name=Issues
 metric.vulnerabilities.description=Vulnerabilities
 metric.vulnerabilities.name=Vulnerabilities
 metric.wont_fix_issues.description=Won't fix issues