]> source.dussan.org Git - sonarqube.git/commitdiff
Polish new source viewer (#1755)
authorStas Vilchik <stas-vilchik@users.noreply.github.com>
Wed, 8 Mar 2017 08:30:24 +0000 (09:30 +0100)
committerGitHub <noreply@github.com>
Wed, 8 Mar 2017 08:30:24 +0000 (09:30 +0100)
91 files changed:
server/sonar-web/src/main/js/apps/code/components/App.js
server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js
server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js
server/sonar-web/src/main/js/apps/component/components/App.js
server/sonar-web/src/main/js/apps/overview/components/App.js
server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssueLocations.js
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssuesIndicator.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewer.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewerBase.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/Line.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicatorContainer.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js
server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js
server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js
server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js
server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/popups/scm-popup.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-coverage-popup.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-duplication-popup.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-line-options-popup.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-scm-popup.hbs [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 [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-all.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-coverage.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-duplications.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-issues.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-lines.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-test-cases.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-tests.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/views/templates/source-viewer-measures.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/components/__tests__/source-viewer-test.js [deleted file]
server/sonar-web/src/main/js/components/issue/issue-view.js
server/sonar-web/src/main/js/components/source-viewer/header.js [deleted file]
server/sonar-web/src/main/js/components/source-viewer/helpers/code-with-issue-locations-helper.js [deleted file]
server/sonar-web/src/main/js/components/source-viewer/main.js [deleted file]
server/sonar-web/src/main/js/components/source-viewer/measures-overlay.js [deleted file]
server/sonar-web/src/main/js/components/source-viewer/more-actions.js [deleted file]
server/sonar-web/src/main/js/components/source-viewer/popups/coverage-popup.js [deleted file]
server/sonar-web/src/main/js/components/source-viewer/popups/duplication-popup.js [deleted file]
server/sonar-web/src/main/js/components/source-viewer/popups/line-actions-popup.js [deleted file]
server/sonar-web/src/main/js/components/source-viewer/popups/scm-popup.js [deleted file]
server/sonar-web/src/main/js/components/source-viewer/source.js [deleted file]
server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-all.hbs [deleted file]
server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-coverage.hbs [deleted file]
server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-duplications.hbs [deleted file]
server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-issues.hbs [deleted file]
server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-lines.hbs [deleted file]
server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-test-cases.hbs [deleted file]
server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-tests.hbs [deleted file]
server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-coverage-popup.hbs [deleted file]
server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-duplication-popup.hbs [deleted file]
server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-header.hbs [deleted file]
server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-issue-location.hbs [deleted file]
server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-line-options-popup.hbs [deleted file]
server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-measures.hbs [deleted file]
server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-more-actions.hbs [deleted file]
server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-scm-popup.hbs [deleted file]
server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer.hbs [deleted file]
server/sonar-web/src/main/js/components/workspace/views/viewer-view.js

index 4208e8d409b87a6ff6bb4d31453120bb41c48ffd..8e75483c1da418b30a6a68344759998946fad645 100644 (file)
@@ -22,7 +22,7 @@ import React from 'react';
 import { connect } from 'react-redux';
 import Components from './Components';
 import Breadcrumbs from './Breadcrumbs';
-import SourceViewer from './../../../components/SourceViewer/StandaloneSourceViewer';
+import SourceViewer from './../../../components/SourceViewer/SourceViewer';
 import Search from './Search';
 import ListFooter from '../../../components/controls/ListFooter';
 import { retrieveComponentChildren, retrieveComponent, loadMoreChildren, parseError } from '../utils';
index bbfc0ae32bf6d3dd09bf1d7c8fb03cb09c8b56e5..6f52f65713e79dbdc1d874d843811c6be07d6309 100644 (file)
@@ -23,7 +23,7 @@ import moment from 'moment';
 import ComponentsList from './ComponentsList';
 import ListHeader from './ListHeader';
 import Spinner from '../../components/Spinner';
-import SourceViewer from '../../../../components/SourceViewer/StandaloneSourceViewer';
+import SourceViewer from '../../../../components/SourceViewer/SourceViewer';
 import ListFooter from '../../../../components/controls/ListFooter';
 
 export default class ListView extends React.Component {
index fb0bb744bd04b10f5ee3a3d3d064f46236f15309..4c6e64c94e4f18864a51353672e8e1fe97046df1 100644 (file)
@@ -22,7 +22,7 @@ import moment from 'moment';
 import ComponentsList from './ComponentsList';
 import ListHeader from './ListHeader';
 import Spinner from '../../components/Spinner';
-import SourceViewer from '../../../../components/SourceViewer/StandaloneSourceViewer';
+import SourceViewer from '../../../../components/SourceViewer/SourceViewer';
 import ListFooter from '../../../../components/controls/ListFooter';
 
 export default class TreeView extends React.Component {
index 041625d8243770b114f547635a2877ad1d431c6d..735e89a21d15882f032b618e469200fa6fa9ef59 100644 (file)
@@ -19,7 +19,7 @@
  */
 // @flow
 import React from 'react';
-import SourceViewer from '../../../components/SourceViewer/StandaloneSourceViewer';
+import SourceViewer from '../../../components/SourceViewer/SourceViewer';
 
 export default class App extends React.Component {
   props: {
index 91e636eb52ab014ce41b94dc0f0dcf15aaeb2fc1..c30c80f2630cbc29b2b9840136ef1f35d320c865 100644 (file)
@@ -23,6 +23,7 @@ import shallowCompare from 'react-addons-shallow-compare';
 import { withRouter } from 'react-router';
 import OverviewApp from './OverviewApp';
 import EmptyOverview from './EmptyOverview';
+import SourceViewer from '../../../components/SourceViewer/SourceViewer';
 
 type Props = {
   component: {
@@ -54,7 +55,6 @@ class App extends React.Component {
     const { component } = this.props;
 
     if (['FIL', 'UTS'].includes(component.qualifier)) {
-      const SourceViewer = require('../../../components/SourceViewer/StandaloneSourceViewer').default;
       return (
           <div className="page">
             <SourceViewer component={component.key}/>
index 2c6e5b594a6050c9cca776b9956315c8ced1b318..e7b999e323171ee4af85e59d7f30058948b4f6b8 100644 (file)
@@ -25,18 +25,19 @@ import { receiveIssues } from '../../store/issues/duck';
 
 const mapStateToProps = null;
 
-const onReceiveComponent = (component: { key: string, canMarkAsFavorite: boolean, fav: boolean }) => dispatch => {
-  if (component.canMarkAsFavorite) {
-    const favorites = [];
-    const notFavorites = [];
-    if (component.fav) {
-      favorites.push({ key: component.key });
-    } else {
-      notFavorites.push({ key: component.key });
+const onReceiveComponent = (component: { key: string, canMarkAsFavorite: boolean, fav: boolean }) =>
+  dispatch => {
+    if (component.canMarkAsFavorite) {
+      const favorites = [];
+      const notFavorites = [];
+      if (component.fav) {
+        favorites.push({ key: component.key });
+      } else {
+        notFavorites.push({ key: component.key });
+      }
+      dispatch(receiveFavorites(favorites, notFavorites));
     }
-    dispatch(receiveFavorites(favorites, notFavorites));
-  }
-};
+  };
 
 const onReceiveIssues = (issues: Array<*>) => dispatch => {
   dispatch(receiveIssues(issues));
index 4a011e163c3ad903d0d59a610e71f89f0731704c..96e7641e52598b58d02dea818a11b204261b4555 100644 (file)
@@ -24,12 +24,11 @@ import uniqBy from 'lodash/uniqBy';
 import SourceViewerHeader from './SourceViewerHeader';
 import SourceViewerCode from './SourceViewerCode';
 import SourceViewerIssueLocations from './SourceViewerIssueLocations';
-import CoveragePopupView from '../source-viewer/popups/coverage-popup';
-import DuplicationPopupView from '../source-viewer/popups/duplication-popup';
-import LineActionsPopupView from '../source-viewer/popups/line-actions-popup';
-import SCMPopupView from '../source-viewer/popups/scm-popup';
-import MeasuresOverlay from '../source-viewer/measures-overlay';
-import Source from '../source-viewer/source';
+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 {
@@ -47,7 +46,12 @@ import type {
   IndexedIssueLocationsByIssueAndLine,
   IndexedIssueLocationMessagesByIssueAndLine
 } from './helpers/indexing';
-import { getComponentForSourceViewer, getSources, getDuplications, getTests } from '../../api/components';
+import {
+  getComponentForSourceViewer,
+  getSources,
+  getDuplications,
+  getTests
+} from '../../api/components';
 import { translate } from '../../helpers/l10n';
 import { scrollToElement } from '../../helpers/scrolling';
 import type { SourceLine } from './types';
@@ -66,11 +70,11 @@ type Props = {
   loadIssues: (string, number, number) => Promise<*>,
   loadSources: (string, number, number) => Promise<*>,
   onLoaded?: (component: Object, sources: Array<*>, issues: Array<*>) => void,
-  onIssueSelect: (string) => void,
-  onIssueUnselect: () => void,
+  onIssueSelect?: (string) => void,
+  onIssueUnselect?: () => void,
   onReceiveComponent: ({ canMarkAsFavorite: boolean, fav: boolean, key: string }) => void,
   onReceiveIssues: (issues: Array<*>) => void,
-  selectedIssue: string | null,
+  selectedIssue?: string
 };
 
 type State = {
@@ -99,6 +103,8 @@ type State = {
   locationsPanelHeight: number,
   notAccessible: boolean,
   notExist: boolean,
+  openIssuesByLine: { [number]: boolean },
+  selectedIssue?: string,
   selectedIssueLocation: IndexedIssueLocation | null,
   sources?: Array<SourceLine>,
   symbolsByLine: { [number]: Array<string> }
@@ -125,8 +131,6 @@ export default class SourceViewerBase extends React.Component {
 
   static defaultProps = {
     displayAllIssues: false,
-    onIssueSelect: () => { },
-    onIssueUnselect: () => { },
     loadComponent,
     loadIssues,
     loadSources
@@ -150,7 +154,8 @@ export default class SourceViewerBase extends React.Component {
       locationsPanelHeight: this.getInitialLocationsPanelHeight(),
       notAccessible: false,
       notExist: false,
-      selectedIssue: props.defaultSelectedIssue || null,
+      openIssuesByLine: {},
+      selectedIssue: props.selectedIssue,
       selectedIssueLocation: null,
       symbolsByLine: {}
     };
@@ -161,16 +166,27 @@ export default class SourceViewerBase extends React.Component {
     this.fetchComponent();
   }
 
+  componentWillReceiveProps (nextProps: Props) {
+    if (nextProps.onIssueSelect != null && nextProps.selectedIssue !== this.props.selectedIssue) {
+      this.setState({ selectedIssue: nextProps.selectedIssue, selectedIssueLocation: null });
+    }
+  }
+
   componentDidUpdate (prevProps: Props, prevState: State) {
     if (prevProps.component !== this.props.component) {
       this.fetchComponent();
-    } else if (this.props.aroundLine != null && prevProps.aroundLine !== this.props.aroundLine &&
-      this.isLineOutsideOfRange(this.props.aroundLine)) {
+    } else if (
+      this.props.aroundLine != null &&
+      prevProps.aroundLine !== this.props.aroundLine &&
+      this.isLineOutsideOfRange(this.props.aroundLine)
+    ) {
       this.fetchSources();
     }
 
-    if (prevState.selectedIssueLocation !== this.state.selectedIssueLocation &&
-      this.state.selectedIssueLocation != null) {
+    if (
+      prevState.selectedIssueLocation !== this.state.selectedIssueLocation &&
+      this.state.selectedIssueLocation != null
+    ) {
       this.scrollToLine(this.state.selectedIssueLocation.line);
     }
   }
@@ -211,22 +227,25 @@ export default class SourceViewerBase extends React.Component {
         this.props.onReceiveIssues(issues);
         if (this.mounted) {
           const finalSources = sources.slice(0, LINES);
-          this.setState({
-            component,
-            issues,
-            issuesByLine: issuesByLine(issues),
-            issueLocationsByLine: locationsByLine(issues),
-            issueSecondaryLocationsByIssueByLine: locationsByIssueAndLine(issues),
-            issueSecondaryLocationMessagesByIssueByLine: locationMessagesByIssueAndLine(issues),
-            loading: false,
-            hasSourcesAfter: sources.length > LINES,
-            sources: this.computeCoverageStatus(finalSources),
-            symbolsByLine: symbolsByLine(sources.slice(0, LINES))
-          }, () => {
-            if (this.props.onLoaded) {
-              this.props.onLoaded(component, finalSources, issues);
+          this.setState(
+            {
+              component,
+              issues,
+              issuesByLine: issuesByLine(issues),
+              issueLocationsByLine: locationsByLine(issues),
+              issueSecondaryLocationsByIssueByLine: locationsByIssueAndLine(issues),
+              issueSecondaryLocationMessagesByIssueByLine: locationMessagesByIssueAndLine(issues),
+              loading: false,
+              hasSourcesAfter: sources.length > LINES,
+              sources: this.computeCoverageStatus(finalSources),
+              symbolsByLine: symbolsByLine(sources.slice(0, LINES))
+            },
+            () => {
+              if (this.props.onLoaded) {
+                this.props.onLoaded(component, finalSources, issues);
+              }
             }
-          });
+          );
         }
       });
     };
@@ -262,15 +281,18 @@ export default class SourceViewerBase extends React.Component {
     this.loadSources().then(sources => {
       if (this.mounted) {
         const finalSources = sources.slice(0, LINES);
-        this.setState({
-          sources: sources.slice(0, LINES),
-          hasSourcesAfter: sources.length > LINES
-        }, () => {
-          if (this.props.onLoaded) {
-            // $FlowFixMe
-            this.props.onLoaded(this.state.component, finalSources, this.state.issues);
+        this.setState(
+          {
+            sources: sources.slice(0, LINES),
+            hasSourcesAfter: sources.length > LINES
+          },
+          () => {
+            if (this.props.onLoaded) {
+              // $FlowFixMe
+              this.props.onLoaded(this.state.component, finalSources, this.state.issues);
+            }
           }
-        });
+        );
       }
     });
   }
@@ -292,10 +314,9 @@ export default class SourceViewerBase extends React.Component {
       // request one additional line to define `hasSourcesAfter`
       const to = this.props.aroundLine ? this.props.aroundLine + LINES / 2 + 1 : LINES + 1;
 
-      return this.props.loadSources(this.props.component, from, to).then(
-        sources => resolve(sources),
-        onFailLoadSources
-      );
+      return this.props
+          .loadSources(this.props.component, from, to)
+          .then(sources => resolve(sources), onFailLoadSources);
     });
   }
 
@@ -349,17 +370,20 @@ export default class SourceViewerBase extends React.Component {
   loadDuplications = (line: SourceLine, element: HTMLElement) => {
     getDuplications(this.props.component).then(r => {
       if (this.mounted) {
-        this.setState({
-          displayDuplications: true,
-          duplications: r.duplications,
-          duplicationsByLine: duplicationsByLine(r.duplications),
-          duplicatedFiles: r.files
-        }, () => {
-          // immediately show dropdown popup if there is only one duplicated block
-          if (r.duplications.length === 1) {
-            this.handleDuplicationClick(0, line.line, element);
+        this.setState(
+          {
+            displayDuplications: true,
+            duplications: r.duplications,
+            duplicationsByLine: duplicationsByLine(r.duplications),
+            duplicatedFiles: r.files
+          },
+          () => {
+            // immediately show dropdown popup if there is only one duplicated block
+            if (r.duplications.length === 1) {
+              this.handleDuplicationClick(0, line.line, element);
+            }
           }
-        });
+        );
       }
     });
   };
@@ -394,9 +418,8 @@ export default class SourceViewerBase extends React.Component {
   };
 
   showMeasures = () => {
-    const model = new Source(this.state.component);
-    const measuresOvervlay = new MeasuresOverlay({ model, large: true });
-    measuresOvervlay.render();
+    const measuresOverlay = new MeasuresOverlay({ component: this.state.component, large: true });
+    measuresOverlay.render();
   };
 
   handleCoverageClick = (line: SourceLine, element: HTMLElement) => {
@@ -416,14 +439,16 @@ export default class SourceViewerBase extends React.Component {
       const currentFile = b._ref === '1';
       const shouldDisplayForCurrentFile = outOfBounds || foundOne;
       const shouldDisplay = !currentFile || shouldDisplayForCurrentFile;
-      const isOk = (b._ref != null) && shouldDisplay;
+      const isOk = b._ref != null && shouldDisplay;
       if (b._ref === '1' && !outOfBounds) {
         foundOne = true;
       }
       return isOk;
     });
 
-    const element = this.node.querySelector(`.source-line-duplications-extra[data-line-number="${line}"]`);
+    const element = this.node.querySelector(
+      `.source-line-duplications-extra[data-line-number="${line}"]`
+    );
     if (element) {
       const popup = new DuplicationPopupView({
         blocks,
@@ -445,11 +470,11 @@ export default class SourceViewerBase extends React.Component {
     popup.render();
   }
 
-  handleLineClick = (line: number, element: HTMLElement) => {
+  handleLineClick = (line: SourceLine, element: HTMLElement) => {
     this.setState(prevState => ({
-      highlightedLine: prevState.highlightedLine === line ? null : line
+      highlightedLine: prevState.highlightedLine === line.line ? null : line
     }));
-    this.displayLinePopup(line, element);
+    this.displayLinePopup(line.line, element);
   };
 
   handleSymbolClick = (symbol: string) => {
@@ -479,6 +504,34 @@ export default class SourceViewerBase extends React.Component {
     this.storeLocationsPanelHeight(height);
   };
 
+  handleIssueSelect = (issue: string) => {
+    if (this.props.onIssueSelect) {
+      this.props.onIssueSelect(issue);
+    } else {
+      this.setState({ selectedIssue: issue, selectedIssueLocation: null });
+    }
+  };
+
+  handleIssueUnselect = () => {
+    if (this.props.onIssueUnselect) {
+      this.props.onIssueUnselect();
+    } else {
+      this.setState({ selectedIssue: undefined, selectedIssueLocation: null });
+    }
+  };
+
+  handleOpenIssues = (line: SourceLine) => {
+    this.setState(state => ({
+      openIssuesByLine: { ...state.openIssuesByLine, [line.line]: true }
+    }));
+  };
+
+  handleCloseIssues = (line: SourceLine) => {
+    this.setState(state => ({
+      openIssuesByLine: { ...state.openIssuesByLine, [line.line]: false }
+    }));
+  };
+
   renderCode (sources: Array<SourceLine>) {
     const hasSourcesBefore = sources.length > 0 && sources[0].line > 1;
     return (
@@ -496,7 +549,9 @@ export default class SourceViewerBase extends React.Component {
         issuesByLine={this.state.issuesByLine}
         issueLocationsByLine={this.state.issueLocationsByLine}
         issueSecondaryLocationsByIssueByLine={this.state.issueSecondaryLocationsByIssueByLine}
-        issueSecondaryLocationMessagesByIssueByLine={this.state.issueSecondaryLocationMessagesByIssueByLine}
+        issueSecondaryLocationMessagesByIssueByLine={
+          this.state.issueSecondaryLocationMessagesByIssueByLine
+        }
         loadDuplications={this.loadDuplications}
         loadSourcesAfter={this.loadSourcesAfter}
         loadSourcesBefore={this.loadSourcesBefore}
@@ -504,13 +559,16 @@ export default class SourceViewerBase extends React.Component {
         loadingSourcesBefore={this.state.loadingSourcesBefore}
         onCoverageClick={this.handleCoverageClick}
         onDuplicationClick={this.handleDuplicationClick}
-        onIssueSelect={this.props.onIssueSelect}
-        onIssueUnselect={this.props.onIssueUnselect}
+        onIssueSelect={this.handleIssueSelect}
+        onIssueUnselect={this.handleIssueUnselect}
+        onIssuesOpen={this.handleOpenIssues}
+        onIssuesClose={this.handleCloseIssues}
         onLineClick={this.handleLineClick}
         onSCMClick={this.handleSCMClick}
-        onSelectLocation={this.handleSelectIssueLocation}
+        onLocationSelect={this.handleSelectIssueLocation}
         onSymbolClick={this.handleSymbolClick}
-        selectedIssue={this.props.selectedIssue}
+        openIssuesByLine={this.state.openIssuesByLine}
+        selectedIssue={this.state.selectedIssue}
         selectedIssueLocation={this.state.selectedIssueLocation}
         sources={sources}
         symbolsByLine={this.state.symbolsByLine}/>
@@ -526,7 +584,9 @@ export default class SourceViewerBase extends React.Component {
 
     if (this.state.notExist) {
       return (
-        <div className="alert alert-warning spacer-top">{translate('component_viewer.no_component')}</div>
+        <div className="alert alert-warning spacer-top">
+          {translate('component_viewer.no_component')}
+        </div>
       );
     }
 
@@ -534,11 +594,13 @@ export default class SourceViewerBase extends React.Component {
       return null;
     }
 
-    const className = classNames('source-viewer', { 'source-duplications-expanded': this.state.displayDuplications });
+    const className = classNames('source-viewer', {
+      'source-duplications-expanded': this.state.displayDuplications
+    });
 
-    const selectedIssueObj = this.props.selectedIssue && this.state.issues != null ?
-      this.state.issues.find(issue => issue.key === this.props.selectedIssue) :
-      null;
+    const selectedIssueObj = this.state.selectedIssue && this.state.issues != null
+      ? this.state.issues.find(issue => issue.key === this.state.selectedIssue)
+      null;
 
     return (
       <div className={className} ref={node => this.node = node}>
@@ -546,20 +608,19 @@ export default class SourceViewerBase extends React.Component {
           component={this.state.component}
           openNewWindow={this.openNewWindow}
           showMeasures={this.showMeasures}/>
-        {this.state.notAccessible && (
+        {this.state.notAccessible &&
           <div className="alert alert-warning spacer-top">
             {translate('code_viewer.no_source_code_displayed_due_to_security')}
-          </div>
-        )}
+          </div>}
         {this.state.sources != null && this.renderCode(this.state.sources)}
-        {selectedIssueObj != null && selectedIssueObj.flows.length > 0 && (
+        {selectedIssueObj != null &&
+          selectedIssueObj.flows.length > 0 &&
           <SourceViewerIssueLocations
             height={this.state.locationsPanelHeight}
             issue={selectedIssueObj}
             onResize={this.handleLocationsPanelResize}
             onSelectLocation={this.handleSelectIssueLocation}
-            selectedLocation={this.state.selectedIssueLocation}/>
-        )}
+            selectedLocation={this.state.selectedIssueLocation}/>}
       </div>
     );
   }
index fb98c06ab157182e300f76731f4ee89eec236e56..d0b5eff25b07828b6ebc7cf6a93045f1d27d833f 100644 (file)
@@ -19,8 +19,7 @@
  */
 // @flow
 import React from 'react';
-import SourceViewerLine from './SourceViewerLine';
-import { TooltipsContainer } from '../mixins/tooltips-mixin';
+import Line from './components/Line';
 import { translate } from '../../helpers/l10n';
 import type { Duplication, SourceLine } from './types';
 import type { Issue } from '../issue/types';
@@ -64,24 +63,19 @@ export default class SourceViewerCode extends React.PureComponent {
     onDuplicationClick: (number, number) => void,
     onIssueSelect: (string) => void,
     onIssueUnselect: () => void,
-    onLineClick: (number, HTMLElement) => void,
+    onIssuesOpen: (SourceLine) => void,
+    onIssuesClose: (SourceLine) => void,
+    onLineClick: (SourceLine, HTMLElement) => void,
     onSCMClick: (SourceLine, HTMLElement) => void,
-    onSelectLocation: (flowIndex: number, locationIndex: number) => void,
+    onLocationSelect: (flowIndex: number, locationIndex: number) => void,
     onSymbolClick: (string) => void,
+    openIssuesByLine: { [number]: boolean },
     selectedIssue: string | null,
     selectedIssueLocation: IndexedIssueLocation | null,
     sources: Array<SourceLine>,
     symbolsByLine: { [number]: Array<string> }
   };
 
-  isSCMChanged (s: SourceLine, p: null | SourceLine) {
-    let changed = true;
-    if (p != null && s.scmAuthor != null && p.scmAuthor != null) {
-      changed = (s.scmAuthor !== p.scmAuthor) || (s.scmDate !== p.scmDate);
-    }
-    return changed;
-  }
-
   getDuplicationsForLine (line: SourceLine) {
     return this.props.duplicationsByLine[line.line] || EMPTY_ARRAY;
   }
@@ -103,7 +97,8 @@ export default class SourceViewerCode extends React.PureComponent {
   }
 
   getSecondaryIssueLocationMessagesForLine (line: SourceLine, issueKey: string) {
-    return this.props.issueSecondaryLocationMessagesByIssueByLine[issueKey][line.line] || EMPTY_ARRAY;
+    return this.props.issueSecondaryLocationMessagesByIssueByLine[issueKey][line.line] ||
+      EMPTY_ARRAY;
   }
 
   renderLine = (
@@ -116,10 +111,12 @@ export default class SourceViewerCode extends React.PureComponent {
   ) => {
     const { filterLine, selectedIssue, sources } = this.props;
     const filtered = filterLine ? filterLine(line) : null;
-    const secondaryIssueLocations = selectedIssue ?
-      this.getSecondaryIssueLocationsForLine(line, selectedIssue) : EMPTY_ARRAY;
-    const secondaryIssueLocationMessages = selectedIssue ?
-      this.getSecondaryIssueLocationMessagesForLine(line, selectedIssue) : EMPTY_ARRAY;
+    const secondaryIssueLocations = selectedIssue
+      ? this.getSecondaryIssueLocationsForLine(line, selectedIssue)
+      : EMPTY_ARRAY;
+    const secondaryIssueLocationMessages = selectedIssue
+      ? this.getSecondaryIssueLocationMessagesForLine(line, selectedIssue)
+      : EMPTY_ARRAY;
 
     const duplicationsCount = this.props.duplications ? this.props.duplications.length : 0;
 
@@ -128,28 +125,32 @@ export default class SourceViewerCode extends React.PureComponent {
     // for the following properties pass null if the line for sure is not impacted
     const symbolsForLine = this.props.symbolsByLine[line.line] || [];
     const { highlightedSymbol } = this.props;
-    const optimizedHighlightedSymbol = highlightedSymbol != null && symbolsForLine.includes(highlightedSymbol) ?
-      highlightedSymbol : null;
+    const optimizedHighlightedSymbol = highlightedSymbol != null &&
+      symbolsForLine.includes(highlightedSymbol)
+      ? highlightedSymbol
+      : null;
 
-    const optimizedSelectedIssue = selectedIssue != null && issuesForLine.includes(selectedIssue) ?
-      selectedIssue : null;
+    const optimizedSelectedIssue = selectedIssue != null && issuesForLine.includes(selectedIssue)
+      ? selectedIssue
+      : null;
 
     const { selectedIssueLocation } = this.props;
-    const optimizedSelectedIssueLocation =
-      selectedIssueLocation != null &&
-        secondaryIssueLocations.some(location =>
+    const optimizedSelectedIssueLocation = selectedIssueLocation != null &&
+      secondaryIssueLocations.some(
+        location =>
           location.flowIndex === selectedIssueLocation.flowIndex &&
           location.locationIndex === selectedIssueLocation.locationIndex
-        ) ? selectedIssueLocation : null;
+      )
+      ? selectedIssueLocation
+      : null;
 
     return (
-      <SourceViewerLine
+      <Line
         displayAllIssues={this.props.displayAllIssues}
         displayCoverage={displayCoverage}
         displayDuplications={displayDuplications}
         displayFiltered={displayFiltered}
         displayIssues={displayIssues}
-        displaySCM={this.isSCMChanged(line, index > 0 ? sources[index - 1] : null)}
         duplications={this.getDuplicationsForLine(line)}
         duplicationsCount={duplicationsCount}
         filtered={filtered}
@@ -165,9 +166,13 @@ export default class SourceViewerCode extends React.PureComponent {
         onDuplicationClick={this.props.onDuplicationClick}
         onIssueSelect={this.props.onIssueSelect}
         onIssueUnselect={this.props.onIssueUnselect}
+        onIssuesOpen={this.props.onIssuesOpen}
+        onIssuesClose={this.props.onIssuesClose}
         onSCMClick={this.props.onSCMClick}
-        onSelectLocation={this.props.onSelectLocation}
+        onLocationSelect={this.props.onLocationSelect}
         onSymbolClick={this.props.onSymbolClick}
+        openIssues={this.props.openIssuesByLine[line.line] || false}
+        previousLine={index > 0 ? sources[index - 1] : undefined}
         secondaryIssueLocations={secondaryIssueLocations}
         secondaryIssueLocationMessages={secondaryIssueLocationMessages}
         selectedIssue={optimizedSelectedIssue}
@@ -187,48 +192,60 @@ export default class SourceViewerCode extends React.PureComponent {
 
     return (
       <div>
-        {this.props.hasSourcesBefore && (
+        {this.props.hasSourcesBefore &&
           <div className="source-viewer-more-code">
-            {this.props.loadingSourcesBefore ? (
-                <div className="js-component-viewer-loading-before">
+            {this.props.loadingSourcesBefore
+              ? <div className="js-component-viewer-loading-before">
                   <i className="spinner"/>
-                  <span className="note spacer-left">{translate('source_viewer.loading_more_code')}</span>
+                  <span className="note spacer-left">
+                    {translate('source_viewer.loading_more_code')}
+                  </span>
                 </div>
-              ) : (
-                <button className="js-component-viewer-source-before" onClick={this.props.loadSourcesBefore}>
+              : <button
+                  className="js-component-viewer-source-before"
+                  onClick={this.props.loadSourcesBefore}>
                   {translate('source_viewer.load_more_code')}
-                </button>
+                </button>}
+          </div>}
+
+        <table className="source-table">
+          <tbody>
+            {hasFileIssues &&
+              this.renderLine(
+                ZERO_LINE,
+                -1,
+                hasCoverage,
+                hasDuplications,
+                displayFiltered,
+                hasIssues
               )}
-          </div>
-        )}
-
-        <TooltipsContainer>
-          <table className="source-table">
-            <tbody>
-              {hasFileIssues && (
-                this.renderLine(ZERO_LINE, -1, hasCoverage, hasDuplications, displayFiltered, hasIssues)
-              )}
-              {sources.map((line, index) => (
-                this.renderLine(line, index, hasCoverage, hasDuplications, displayFiltered, hasIssues)
+            {sources.map((line, index) =>
+              this.renderLine(
+                line,
+                index,
+                hasCoverage,
+                hasDuplications,
+                displayFiltered,
+                hasIssues
               ))}
-            </tbody>
-          </table>
-        </TooltipsContainer>
+          </tbody>
+        </table>
 
-        {this.props.hasSourcesAfter && (
+        {this.props.hasSourcesAfter &&
           <div className="source-viewer-more-code">
-            {this.props.loadingSourcesAfter ? (
-                <div className="js-component-viewer-loading-after">
+            {this.props.loadingSourcesAfter
+              ? <div className="js-component-viewer-loading-after">
                   <i className="spinner"/>
-                  <span className="note spacer-left">{translate('source_viewer.loading_more_code')}</span>
+                  <span className="note spacer-left">
+                    {translate('source_viewer.loading_more_code')}
+                  </span>
                 </div>
-              ) : (
-                <button className="js-component-viewer-source-after" onClick={this.props.loadSourcesAfter}>
+              : <button
+                  className="js-component-viewer-source-after"
+                  onClick={this.props.loadSourcesAfter}>
                   {translate('source_viewer.load_more_code')}
-                </button>
-              )}
-          </div>
-        )}
+                </button>}
+          </div>}
       </div>
     );
   }
index 14dedd85572f25f4a038082e76419ba53da818b4..af05c46d954aae04245fcc0d4451cf79d65a8c67 100644 (file)
@@ -22,13 +22,12 @@ import React from 'react';
 import { Link } from 'react-router';
 import QualifierIcon from '../shared/qualifier-icon';
 import FavoriteContainer from '../controls/FavoriteContainer';
-import Workspace from '../workspace/main';
 import { getProjectUrl, getIssuesUrl } 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.Component {
+export default class SourceViewerHeader extends React.PureComponent {
   props: {
     component: {
       canMarkAsFavorite: boolean,
@@ -64,11 +63,21 @@ export default class SourceViewerHeader extends React.Component {
   openInWorkspace = (e: SyntheticInputEvent) => {
     e.preventDefault();
     const { key } = this.props.component;
+    const Workspace = require('../workspace/main').default;
     Workspace.openComponent({ key });
   };
 
   render () {
-    const { key, measures, path, project, projectName, q, subProject, subProjectName } = this.props.component;
+    const {
+      key,
+      measures,
+      path,
+      project,
+      projectName,
+      q,
+      subProject,
+      subProjectName
+    } = this.props.component;
     const isUnitTest = q === 'UTS';
     // TODO check if source viewer is displayed inside workspace
     const workspace = false;
@@ -85,13 +94,12 @@ export default class SourceViewerHeader extends React.Component {
               </Link>
             </div>
 
-            {subProject != null && (
+            {subProject != null &&
               <div className="component-name-parent">
                 <Link to={getProjectUrl(subProject)} className="link-with-icon">
                   <QualifierIcon qualifier="BRC"/> <span>{subProjectName}</span>
                 </Link>
-              </div>
-            )}
+              </div>}
 
             <div className="component-name-path">
               <QualifierIcon qualifier={q}/>
@@ -99,17 +107,17 @@ export default class SourceViewerHeader extends React.Component {
               <span>{collapsedDirFromPath(path)}</span>
               <span className="component-name-file">{fileFromPath(path)}</span>
 
-              {this.props.component.canMarkAsFavorite && (
-                <FavoriteContainer className="component-name-favorite" componentKey={key}/>
-              )}
+              {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')}/>
+          <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}>
@@ -121,63 +129,76 @@ export default class SourceViewerHeader extends React.Component {
                 {translate('component_viewer.new_window')}
               </a>
             </li>
-            {!workspace && (
+            {!workspace &&
               <li>
                 <a className="js-workspace" href="#" onClick={this.openInWorkspace}>
                   {translate('component_viewer.open_in_workspace')}
                 </a>
-              </li>
-            )}
+              </li>}
             <li>
               <a className="js-raw-source" href={rawSourcesLink} target="_blank">
                 {translate('component_viewer.show_raw_source')}
               </a>
             </li>
           </ul>
-         </div>
+        </div>
 
         <div className="source-viewer-header-measures">
-          {isUnitTest && (
+          {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>
-          )}
+              <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 && (
+          {!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>
-          )}
+              <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={getIssuesUrl({ resolved: 'false', componentKeys: key })}
-                    className="source-viewer-header-external-link" target="_blank">
+              <Link
+                to={getIssuesUrl({ resolved: 'false', componentKeys: key })}
+                className="source-viewer-header-external-link"
+                target="_blank">
                 {measures.issues != null ? formatMeasure(measures.issues, 'SHORT_INT') : 0}
                 {' '}
                 <i className="icon-detach"/>
               </Link>
             </span>
-            <span className="source-viewer-header-measure-label">{translate('metric.violations.name')}</span>
+            <span className="source-viewer-header-measure-label">
+              {translate('metric.violations.name')}
+            </span>
           </div>
 
-          {measures.coverage != null && (
+          {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>
-          )}
+              <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 && (
+          {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>
-          )}
+              <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>
     );
index d4881347372f6be3e8eca36752219074a71d0e64..abe72568770480f0a9ebbc5495a1b02ec5a5c7c3 100644 (file)
@@ -61,9 +61,6 @@ export default class SourceViewerIssueLocations extends React.Component {
   }
 
   componentWillReceiveProps (nextProps: Props) {
-    /* eslint-disable no-console */
-    console.log('foo');
-
     if (nextProps.selectedLocation !== this.props.selectedLocation) {
       this.setState({ locationBlink: false });
     }
@@ -296,15 +293,10 @@ export default class SourceViewerIssueLocations extends React.Component {
                 {flows.map(
                   (flow, flowIndex) =>
                     flow.locations != null &&
-                      this
-                          .reverseLocations(flow.locations)
-                          .map((location, locationIndex) =>
-                          this.renderLocation(
-                            location,
-                            flowIndex,
-                            locationIndex,
-                            flow.locations || []
-                          ))
+                    this.reverseLocations(
+                      flow.locations
+                    ).map((location, locationIndex) =>
+                      this.renderLocation(location, flowIndex, locationIndex, flow.locations || []))
                 )}
               </ul>
               <DraggableCore axis="y" onDrag={this.handleDrag} offsetParent={document.body}>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssuesIndicator.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssuesIndicator.js
deleted file mode 100644 (file)
index f699394..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import { connect } from 'react-redux';
-import SeverityIcon from '../shared/severity-icon';
-import { getIssueByKey } from '../../store/rootReducer';
-import { sortBySeverity } from '../../helpers/issues';
-
-class SourceViewerIssuesIndicator extends React.Component {
-  props: {
-    issue: { severity: string }
-  };
-
-  render () {
-    return (
-      <SeverityIcon severity={this.props.issue.severity}/>
-    );
-  }
-}
-
-const mapStateToProps = (state, ownProps: { issues: Array<string> }) => {
-  const issues = ownProps.issues.map(issueKey => getIssueByKey(state, issueKey));
-  return { issue: sortBySeverity(issues)[0] };
-};
-
-export default connect(mapStateToProps)(SourceViewerIssuesIndicator);
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js
deleted file mode 100644 (file)
index 5a53275..0000000
+++ /dev/null
@@ -1,410 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import classNames from 'classnames';
-import times from 'lodash/times';
-import ConnectedIssue from '../issue/ConnectedIssue';
-import SourceViewerIssuesIndicator from './SourceViewerIssuesIndicator';
-import { translate } from '../../helpers/l10n';
-import { splitByTokens, highlightSymbol, highlightIssueLocations, generateHTML } from './helpers/highlight';
-import type { SourceLine } from './types';
-import type { LinearIssueLocation, IndexedIssueLocation, IndexedIssueLocationMessage } from './helpers/indexing';
-
-type Props = {
-  displayAllIssues: boolean,
-  displayCoverage: boolean,
-  displayDuplications: boolean,
-  displayFiltered: boolean,
-  displayIssues: boolean,
-  displaySCM: boolean,
-  duplications: Array<number>,
-  duplicationsCount: number,
-  filtered: boolean | null,
-  highlighted: boolean,
-  highlightedSymbol: string | null,
-  issueLocations: Array<LinearIssueLocation>,
-  issues: Array<string>,
-  line: SourceLine,
-  loadDuplications: (SourceLine, HTMLElement) => void,
-  onClick: (number, HTMLElement) => void,
-  onCoverageClick: (SourceLine, HTMLElement) => void,
-  onDuplicationClick: (number, number) => void,
-  onIssueSelect: (string) => void,
-  onIssueUnselect: () => void,
-  onSCMClick: (SourceLine, HTMLElement) => void,
-  onSelectLocation: (flowIndex: number, locationIndex: number) => void,
-  onSymbolClick: (string) => void,
-  selectedIssue: string | null,
-  secondaryIssueLocations: Array<IndexedIssueLocation>,
-  // $FlowFixMe
-  secondaryIssueLocationMessages: Array<IndexedIssueLocationMessage>,
-  selectedIssueLocation: IndexedIssueLocation | null
-};
-
-type State = {
-  issuesOpen: boolean
-};
-
-export default class SourceViewerLine extends React.PureComponent {
-  codeNode: HTMLElement;
-  props: Props;
-  issueElements: { [string]: HTMLElement } = {};
-  issueViews: { [string]: { destroy: () => void } } = {};
-  state: State = { issuesOpen: false };
-  symbols: NodeList<HTMLElement>;
-
-  componentDidMount () {
-    this.attachEvents();
-  }
-
-  componentWillUpdate () {
-    this.detachEvents();
-  }
-
-  componentDidUpdate () {
-    this.attachEvents();
-  }
-
-  componentWillUnmount () {
-    this.detachEvents();
-  }
-
-  attachEvents () {
-    this.symbols = this.codeNode.querySelectorAll('.sym');
-    for (const symbol of this.symbols) {
-      symbol.addEventListener('click', this.handleSymbolClick);
-    }
-  }
-
-  detachEvents () {
-    if (this.symbols) {
-      for (const symbol of this.symbols) {
-        symbol.removeEventListener('click', this.handleSymbolClick);
-      }
-    }
-  }
-
-  handleClick = (e: SyntheticInputEvent) => {
-    e.preventDefault();
-    this.props.onClick(this.props.line.line, e.target);
-  };
-
-  handleCoverageClick = (e: SyntheticInputEvent) => {
-    e.preventDefault();
-    this.props.onCoverageClick(this.props.line, e.target);
-  };
-
-  handleIssuesIndicatorClick = (e: SyntheticInputEvent) => {
-    e.preventDefault();
-    this.setState(prevState => {
-      // TODO not sure if side effects allowed here
-      if (!prevState.issuesOpen) {
-        const { issues } = this.props;
-        if (issues.length > 0) {
-          this.props.onIssueSelect(issues[0]);
-        }
-      } else {
-        this.props.onIssueUnselect();
-      }
-
-      return { issuesOpen: !prevState.issuesOpen };
-    });
-  }
-
-  handleSCMClick = (e: SyntheticInputEvent) => {
-    e.preventDefault();
-    this.props.onSCMClick(this.props.line, e.target);
-  }
-
-  handleSymbolClick = (e: Object) => {
-    e.preventDefault();
-    const key = e.currentTarget.className.match(/sym-\d+/);
-    if (key && key[0]) {
-      this.props.onSymbolClick(key[0]);
-    }
-  };
-
-  handleIssueSelect = (issueKey: string) => {
-    this.props.onIssueSelect(issueKey);
-  };
-
-  renderLineNumber () {
-    const { line } = this.props;
-    return (
-      <td className="source-meta source-line-number"
-          // don't display 0
-          data-line-number={line.line ? line.line : undefined}
-          role={line.line ? 'button' : undefined}
-          tabIndex={line.line ? 0 : undefined}
-          onClick={line.line ? this.handleClick : undefined}/>
-    );
-  }
-
-  renderSCM () {
-    const { line } = this.props;
-    const clickable = !!line.line;
-    return (
-      <td className="source-meta source-line-scm"
-          data-line-number={line.line}
-          role={clickable ? 'button' : undefined}
-          tabIndex={clickable ? 0 : undefined}
-          onClick={clickable ? this.handleSCMClick : undefined}>
-        {this.props.displaySCM && (
-          <div className="source-line-scm-inner" data-author={line.scmAuthor}/>
-        )}
-      </td>
-    );
-  }
-
-  renderCoverage () {
-    const { line } = this.props;
-    const className = 'source-meta source-line-coverage' +
-      (line.coverageStatus != null ? ` source-line-${line.coverageStatus}` : '');
-    return (
-      <td className={className}
-          data-line-number={line.line}
-          title={line.coverageStatus != null && translate('source_viewer.tooltip', line.coverageStatus)}
-          data-placement={line.coverageStatus != null && 'right'}
-          data-toggle={line.coverageStatus != null && 'tooltip'}
-          role={line.coverageStatus != null ? 'button' : undefined}
-          tabIndex={line.coverageStatus != null ? 0 : undefined}
-          onClick={line.coverageStatus != null && this.handleCoverageClick}>
-        <div className="source-line-bar"/>
-      </td>
-    );
-  }
-
-  renderDuplications () {
-    const { line } = this.props;
-    const className = classNames('source-meta', 'source-line-duplications', {
-      'source-line-duplicated': line.duplicated
-    });
-
-    const handleDuplicationClick = (e: SyntheticInputEvent) => {
-      e.preventDefault();
-      this.props.loadDuplications(this.props.line, e.target);
-    };
-
-    return (
-      <td className={className}
-          title={line.duplicated && translate('source_viewer.tooltip.duplicated_line')}
-          data-placement={line.duplicated && 'right'}
-          data-toggle={line.duplicated && 'tooltip'}
-          role="button"
-          tabIndex="0"
-          onClick={handleDuplicationClick}>
-        <div className="source-line-bar"/>
-      </td>
-    );
-  }
-
-  renderDuplicationsExtra () {
-    const { duplications, duplicationsCount } = this.props;
-    return times(duplicationsCount).map(index => this.renderDuplication(index, duplications.includes(index)));
-  }
-
-  renderDuplication = (index: number, duplicated: boolean) => {
-    const className = classNames('source-meta', 'source-line-duplications-extra', {
-      'source-line-duplicated': duplicated
-    });
-
-    const handleDuplicationClick = (e: SyntheticInputEvent) => {
-      e.preventDefault();
-      this.props.onDuplicationClick(index, this.props.line.line);
-    };
-
-    return (
-      <td key={index}
-          className={className}
-          data-line-number={this.props.line.line}
-          data-index={index}
-          title={duplicated ? translate('source_viewer.tooltip.duplicated_block') : undefined}
-          data-placement={duplicated ? 'right' : undefined}
-          data-toggle={duplicated ? 'tooltip' : undefined}
-          role={duplicated ? 'button' : undefined}
-          tabIndex={duplicated ? '0' : undefined}
-          onClick={duplicated ? handleDuplicationClick : undefined}>
-        <div className="source-line-bar"/>
-      </td>
-    );
-  };
-
-  renderIssuesIndicator () {
-    const { issues } = this.props;
-    const hasIssues = issues.length > 0;
-    const className = classNames('source-meta', 'source-line-issues', { 'source-line-with-issues': hasIssues });
-    const onClick = hasIssues ? this.handleIssuesIndicatorClick : undefined;
-
-    return (
-      <td className={className}
-          data-line-number={this.props.line.line}
-          role="button"
-          tabIndex="0"
-          onClick={onClick}>
-        {hasIssues && (
-          <SourceViewerIssuesIndicator issues={issues}/>
-        )}
-        {issues.length > 1 && (
-          <span className="source-line-issues-counter">{issues.length}</span>
-        )}
-      </td>
-    );
-  }
-
-  isSecondaryIssueLocationSelected (location: IndexedIssueLocation | IndexedIssueLocationMessage) {
-    const { selectedIssueLocation } = this.props;
-    if (selectedIssueLocation == null) {
-      return false;
-    } else {
-      return selectedIssueLocation.flowIndex === location.flowIndex &&
-        selectedIssueLocation.locationIndex === location.locationIndex;
-    }
-  }
-
-  handleLocationMessageClick (flowIndex: number, locationIndex: number, e: SyntheticInputEvent) {
-    e.preventDefault();
-    this.props.onSelectLocation(flowIndex, locationIndex);
-  }
-
-  renderSecondaryIssueLocationMessage = (location: IndexedIssueLocationMessage) => {
-    const className = classNames('source-viewer-issue-location', 'issue-location-message', {
-      'selected': this.isSecondaryIssueLocationSelected(location)
-    });
-
-    const limitString = (str: string) => (
-      str.length > 30 ? str.substr(0, 30) + '...' : str
-    );
-
-    return (
-      <a
-        key={`${location.flowIndex}-${location.locationIndex}`}
-        href="#"
-        className={className}
-        title={location.msg}
-        onClick={e => this.handleLocationMessageClick(location.flowIndex, location.locationIndex, e)}>
-        {location.index && (
-          <strong>{location.index}: </strong>
-        )}
-        {limitString(location.msg)}
-      </a>
-    );
-  };
-
-  renderSecondaryIssueLocationMessages (locations: Array<IndexedIssueLocationMessage>) {
-    return (
-      <div className="source-line-issue-locations">
-        {locations.map(this.renderSecondaryIssueLocationMessage)}
-      </div>
-    );
-  }
-
-  renderCode () {
-    const { line, highlightedSymbol, issueLocations, issues, secondaryIssueLocations } = this.props;
-    const { secondaryIssueLocationMessages } = this.props;
-    const className = classNames('source-line-code', 'code', { 'has-issues': issues.length > 0 });
-
-    const code = line.code || '';
-    let tokens = splitByTokens(code);
-
-    if (highlightedSymbol) {
-      tokens = highlightSymbol(tokens, highlightedSymbol);
-    }
-
-    if (issueLocations.length > 0) {
-      tokens = highlightIssueLocations(tokens, issueLocations);
-    }
-
-    if (secondaryIssueLocations) {
-      const linearLocations = secondaryIssueLocations.map(location => ({
-        from: location.from,
-        line: location.line,
-        to: location.to
-      }));
-      tokens = highlightIssueLocations(tokens, linearLocations, 'issue-location');
-      const { selectedIssueLocation } = this.props;
-      if (selectedIssueLocation != null) {
-        const x = secondaryIssueLocations.find(location => this.isSecondaryIssueLocationSelected(location));
-        if (x) {
-          tokens = highlightIssueLocations(tokens, [x], 'selected');
-        }
-      }
-    }
-
-    const finalCode = generateHTML(tokens);
-
-    const showIssues = (this.state.issuesOpen || this.props.displayAllIssues) && issues.length > 0;
-
-    return (
-      <td className={className} data-line-number={line.line}>
-        <div className="source-line-code-inner">
-          <pre ref={node => this.codeNode = node} dangerouslySetInnerHTML={{ __html: finalCode }}/>
-          {secondaryIssueLocationMessages != null && secondaryIssueLocationMessages.length > 0 && (
-            this.renderSecondaryIssueLocationMessages(secondaryIssueLocationMessages)
-          )}
-        </div>
-        {showIssues && (
-          <div className="issue-list">
-            {issues.map(issue => (
-              <ConnectedIssue
-                key={issue}
-                issueKey={issue}
-                onClick={this.handleIssueSelect}
-                selected={this.props.selectedIssue === issue}/>
-            ))}
-          </div>
-        )}
-      </td>
-    );
-  }
-
-  render () {
-    const { line, duplicationsCount, filtered } = this.props;
-    const className = classNames('source-line', {
-      'source-line-highlighted': this.props.highlighted,
-      'source-line-shadowed': filtered === false,
-      'source-line-filtered': filtered === true
-    });
-
-    return (
-      <tr className={className} data-line-number={line.line}>
-        {this.renderLineNumber()}
-
-        {this.renderSCM()}
-
-        {this.props.displayCoverage && this.renderCoverage()}
-
-        {this.props.displayDuplications && this.renderDuplications()}
-
-        {duplicationsCount > 0 && this.renderDuplicationsExtra()}
-
-        {this.props.displayIssues && !this.props.displayAllIssues && this.renderIssuesIndicator()}
-
-        {this.props.displayFiltered && (
-          <td className="source-meta source-line-filtered-container" data-line-number={line.line}>
-            <div className="source-line-bar"/>
-          </td>
-        )}
-
-        {this.renderCode()}
-      </tr>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewer.js b/server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewer.js
deleted file mode 100644 (file)
index d673bd4..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import { connect } from 'react-redux';
-import StandaloneSourceViewerBase from './StandaloneSourceViewerBase';
-import { receiveFavorites } from '../../store/favorites/duck';
-import { receiveIssues } from '../../store/issues/duck';
-
-const mapStateToProps = null;
-
-const onReceiveComponent = (component: { key: string, canMarkAsFavorite: boolean, fav: boolean }) => dispatch => {
-  if (component.canMarkAsFavorite) {
-    const favorites = [];
-    const notFavorites = [];
-    if (component.fav) {
-      favorites.push({ key: component.key });
-    } else {
-      notFavorites.push({ key: component.key });
-    }
-    dispatch(receiveFavorites(favorites, notFavorites));
-  }
-};
-
-const onReceiveIssues = (issues: Array<*>) => dispatch => {
-  dispatch(receiveIssues(issues));
-};
-
-const mapDispatchToProps = { onReceiveComponent, onReceiveIssues };
-
-export default connect(mapStateToProps, mapDispatchToProps)(StandaloneSourceViewerBase);
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewerBase.js
deleted file mode 100644 (file)
index ea28e00..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import SourceViewerBase from './SourceViewerBase';
-
-type State = {
-  selectedIssue: string | null
-};
-
-export default class StandaloneSourceViewerBase extends React.Component {
-  state: State = {
-    selectedIssue: null
-  };
-
-  handleIssueSelect = (issue: string) => {
-    this.setState({ selectedIssue: issue });
-  };
-
-  handleIssueUnselect = () => {
-    this.setState({ selectedIssue: null });
-  };
-
-  render () {
-    return (
-      <SourceViewerBase
-        {...this.props}
-        onIssueSelect={this.handleIssueSelect}
-        onIssueUnselect={this.handleIssueUnselect}
-        selectedIssue={this.state.selectedIssue}/>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js
new file mode 100644 (file)
index 0000000..c9da31d
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import classNames from 'classnames';
+import times from 'lodash/times';
+import LineNumber from './LineNumber';
+import LineSCM from './LineSCM';
+import LineCoverage from './LineCoverage';
+import LineDuplications from './LineDuplications';
+import LineDuplicationBlock from './LineDuplicationBlock';
+import LineIssuesIndicatorContainer from './LineIssuesIndicatorContainer';
+import LineCode from './LineCode';
+import { TooltipsContainer } from '../../mixins/tooltips-mixin';
+import type { SourceLine } from '../types';
+import type {
+  LinearIssueLocation,
+  IndexedIssueLocation,
+  IndexedIssueLocationMessage
+} from '../helpers/indexing';
+
+type Props = {
+  displayAllIssues: boolean,
+  displayCoverage: boolean,
+  displayDuplications: boolean,
+  displayFiltered: boolean,
+  displayIssues: boolean,
+  duplications: Array<number>,
+  duplicationsCount: number,
+  filtered: boolean | null,
+  highlighted: boolean,
+  highlightedSymbol: string | null,
+  issueLocations: Array<LinearIssueLocation>,
+  issues: Array<string>,
+  line: SourceLine,
+  loadDuplications: (SourceLine, HTMLElement) => void,
+  onClick: (SourceLine, HTMLElement) => void,
+  onCoverageClick: (SourceLine, HTMLElement) => void,
+  onDuplicationClick: (number, number) => void,
+  onIssueSelect: (string) => void,
+  onIssueUnselect: () => void,
+  onIssuesOpen: (SourceLine) => void,
+  onIssuesClose: (SourceLine) => void,
+  onSCMClick: (SourceLine, HTMLElement) => void,
+  onLocationSelect: (flowIndex: number, locationIndex: number) => void,
+  onSymbolClick: (string) => void,
+  openIssues: boolean,
+  previousLine?: SourceLine,
+  selectedIssue: string | null,
+  secondaryIssueLocations: Array<IndexedIssueLocation>,
+  // $FlowFixMe
+  secondaryIssueLocationMessages: Array<IndexedIssueLocationMessage>,
+  selectedIssueLocation: IndexedIssueLocation | null
+};
+
+export default class Line extends React.PureComponent {
+  props: Props;
+
+  handleIssuesIndicatorClick = () => {
+    if (this.props.openIssues) {
+      this.props.onIssuesClose(this.props.line);
+      this.props.onIssueUnselect();
+    } else {
+      this.props.onIssuesOpen(this.props.line);
+
+      const { issues } = this.props;
+      if (issues.length > 0) {
+        this.props.onIssueSelect(issues[0]);
+      }
+    }
+  };
+
+  render () {
+    const { line, duplications, duplicationsCount, filtered } = this.props;
+    const className = classNames('source-line', {
+      'source-line-highlighted': this.props.highlighted,
+      'source-line-shadowed': filtered === false,
+      'source-line-filtered': filtered === true
+    });
+
+    return (
+      <TooltipsContainer>
+        <tr className={className} data-line-number={line.line}>
+          <LineNumber line={line} onClick={this.props.onClick}/>
+
+          <LineSCM
+            line={line}
+            onClick={this.props.onSCMClick}
+            previousLine={this.props.previousLine}/>
+
+          {this.props.displayCoverage &&
+            <LineCoverage line={line} onClick={this.props.onCoverageClick}/>}
+
+          {this.props.displayDuplications &&
+            <LineDuplications line={line} onClick={this.props.loadDuplications}/>}
+
+          {times(duplicationsCount).map(index => (
+            <LineDuplicationBlock
+              duplicated={duplications.includes(index)}
+              index={index}
+              key={index}
+              line={this.props.line}
+              onClick={this.props.onDuplicationClick}/>
+          ))}
+
+          {this.props.displayIssues &&
+            !this.props.displayAllIssues &&
+            <LineIssuesIndicatorContainer
+              issueKeys={this.props.issues}
+              line={line}
+              onClick={this.handleIssuesIndicatorClick}/>}
+
+          {this.props.displayFiltered &&
+            <td className="source-meta source-line-filtered-container" data-line-number={line.line}>
+              <div className="source-line-bar"/>
+            </td>}
+
+          <LineCode
+            highlightedSymbol={this.props.highlightedSymbol}
+            issueKeys={this.props.issues}
+            issueLocations={this.props.issueLocations}
+            line={line}
+            onIssueSelect={this.props.onIssueSelect}
+            onLocationSelect={this.props.onLocationSelect}
+            onSymbolClick={this.props.onSymbolClick}
+            secondaryIssueLocationMessages={this.props.secondaryIssueLocationMessages}
+            secondaryIssueLocations={this.props.secondaryIssueLocations}
+            selectedIssue={this.props.selectedIssue}
+            selectedIssueLocation={this.props.selectedIssueLocation}
+            showIssues={this.props.openIssues || this.props.displayAllIssues}/>
+        </tr>
+      </TooltipsContainer>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js
new file mode 100644 (file)
index 0000000..944922c
--- /dev/null
@@ -0,0 +1,224 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import classNames from 'classnames';
+import LineIssuesList from './LineIssuesList';
+import {
+  splitByTokens,
+  highlightSymbol,
+  highlightIssueLocations,
+  generateHTML
+} from '../helpers/highlight';
+import type { Tokens } from '../helpers/highlight';
+import type { SourceLine } from '../types';
+import type {
+  LinearIssueLocation,
+  IndexedIssueLocation,
+  IndexedIssueLocationMessage
+} from '../helpers/indexing';
+
+type Props = {
+  highlightedSymbol: string | null,
+  issueKeys: Array<string>,
+  issueLocations: Array<LinearIssueLocation>,
+  line: SourceLine,
+  onIssueSelect: (issueKey: string) => void,
+  onLocationSelect: (flowIndex: number, locationIndex: number) => void,
+  onSymbolClick: (symbol: string) => void,
+  // $FlowFixMe
+  secondaryIssueLocations: Array<IndexedIssueLocation>,
+  secondaryIssueLocationMessages: Array<IndexedIssueLocationMessage>,
+  selectedIssue: string | null,
+  selectedIssueLocation: IndexedIssueLocation | null,
+  showIssues: boolean
+};
+
+type State = {
+  tokens: Tokens
+};
+
+export default class LineCode extends React.PureComponent {
+  codeNode: HTMLElement;
+  props: Props;
+  state: State;
+  symbols: NodeList<HTMLElement>;
+
+  constructor (props: Props) {
+    super(props);
+    this.state = {
+      tokens: splitByTokens(props.line.code || '')
+    };
+  }
+
+  componentDidMount () {
+    this.attachEvents();
+  }
+
+  componentWillReceiveProps (nextProps: Props) {
+    if (nextProps.line.code !== this.props.line.code) {
+      this.setState({
+        tokens: splitByTokens(nextProps.line.code || '')
+      });
+    }
+  }
+
+  componentWillUpdate () {
+    this.detachEvents();
+  }
+
+  componentDidUpdate () {
+    this.attachEvents();
+  }
+
+  componentWillUnmount () {
+    this.detachEvents();
+  }
+
+  attachEvents () {
+    this.symbols = this.codeNode.querySelectorAll('.sym');
+    for (const symbol of this.symbols) {
+      symbol.addEventListener('click', this.handleSymbolClick);
+    }
+  }
+
+  detachEvents () {
+    if (this.symbols) {
+      for (const symbol of this.symbols) {
+        symbol.removeEventListener('click', this.handleSymbolClick);
+      }
+    }
+  }
+
+  handleSymbolClick = (e: Object) => {
+    e.preventDefault();
+    const key = e.currentTarget.className.match(/sym-\d+/);
+    if (key && key[0]) {
+      this.props.onSymbolClick(key[0]);
+    }
+  };
+
+  handleLocationMessageClick = (
+    e: SyntheticInputEvent,
+    flowIndex: number,
+    locationIndex: number
+  ) => {
+    e.preventDefault();
+    this.props.onLocationSelect(flowIndex, locationIndex);
+  };
+
+  isSecondaryIssueLocationSelected (location: IndexedIssueLocation | IndexedIssueLocationMessage) {
+    const { selectedIssueLocation } = this.props;
+    if (selectedIssueLocation == null) {
+      return false;
+    } else {
+      return selectedIssueLocation.flowIndex === location.flowIndex &&
+        selectedIssueLocation.locationIndex === location.locationIndex;
+    }
+  }
+
+  renderSecondaryIssueLocationMessage = (location: IndexedIssueLocationMessage) => {
+    const className = classNames('source-viewer-issue-location', 'issue-location-message', {
+      selected: this.isSecondaryIssueLocationSelected(location)
+    });
+
+    const limitString = (str: string) => str.length > 30 ? str.substr(0, 30) + '...' : str;
+
+    return (
+      <a
+        key={`${location.flowIndex}-${location.locationIndex}`}
+        href="#"
+        className={className}
+        title={location.msg}
+        onClick={e =>
+          this.handleLocationMessageClick(e, location.flowIndex, location.locationIndex)}>
+        {location.index && <strong>{location.index}: </strong>}
+        {limitString(location.msg)}
+      </a>
+    );
+  };
+
+  renderSecondaryIssueLocationMessages (locations: Array<IndexedIssueLocationMessage>) {
+    return (
+      <div className="source-line-issue-locations">
+        {locations.map(this.renderSecondaryIssueLocationMessage)}
+      </div>
+    );
+  }
+
+  render () {
+    const {
+      highlightedSymbol,
+      issueKeys,
+      issueLocations,
+      line,
+      onIssueSelect,
+      secondaryIssueLocationMessages,
+      secondaryIssueLocations,
+      selectedIssue,
+      selectedIssueLocation,
+      showIssues
+    } = this.props;
+
+    let tokens = [...this.state.tokens];
+
+    if (highlightedSymbol) {
+      tokens = highlightSymbol(tokens, highlightedSymbol);
+    }
+
+    if (issueLocations.length > 0) {
+      tokens = highlightIssueLocations(tokens, issueLocations);
+    }
+
+    if (secondaryIssueLocations) {
+      tokens = highlightIssueLocations(tokens, secondaryIssueLocations, 'issue-location');
+      if (selectedIssueLocation != null) {
+        const x = secondaryIssueLocations.find(location =>
+          this.isSecondaryIssueLocationSelected(location));
+        if (x) {
+          tokens = highlightIssueLocations(tokens, [x], 'selected');
+        }
+      }
+    }
+
+    const finalCode = generateHTML(tokens);
+
+    const className = classNames('source-line-code', 'code', {
+      'has-issues': issueKeys.length > 0
+    });
+
+    return (
+      <td className={className} data-line-number={line.line}>
+        <div className="source-line-code-inner">
+          <pre ref={node => this.codeNode = node} dangerouslySetInnerHTML={{ __html: finalCode }}/>
+          {secondaryIssueLocationMessages != null &&
+            secondaryIssueLocationMessages.length > 0 &&
+            this.renderSecondaryIssueLocationMessages(secondaryIssueLocationMessages)}
+        </div>
+        {showIssues &&
+          issueKeys.length > 0 &&
+          <LineIssuesList
+            issueKeys={issueKeys}
+            onIssueClick={onIssueSelect}
+            selectedIssue={selectedIssue}/>}
+      </td>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js
new file mode 100644 (file)
index 0000000..55e7aad
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { translate } from '../../../helpers/l10n';
+import type { SourceLine } from '../types';
+
+type Props = {
+  line: SourceLine,
+  onClick: (SourceLine, HTMLElement) => void
+};
+
+export default class LineCoverage extends React.PureComponent {
+  props: Props;
+
+  handleClick = (e: SyntheticInputEvent) => {
+    e.preventDefault();
+    this.props.onClick(this.props.line, e.target);
+  };
+
+  render () {
+    const { line } = this.props;
+    const className = 'source-meta source-line-coverage' +
+      (line.coverageStatus != null ? ` source-line-${line.coverageStatus}` : '');
+    const title = line.coverageStatus != null
+      ? translate('source_viewer.tooltip', line.coverageStatus)
+      : undefined;
+    return (
+      <td
+        className={className}
+        data-line-number={line.line}
+        title={title}
+        data-placement={line.coverageStatus != null ? 'right' : undefined}
+        data-toggle={line.coverageStatus != null ? 'tooltip' : undefined}
+        role={line.coverageStatus != null ? 'button' : undefined}
+        tabIndex={line.coverageStatus != null ? 0 : undefined}
+        onClick={line.coverageStatus != null ? this.handleClick : undefined}>
+        <div className="source-line-bar"/>
+      </td>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js
new file mode 100644 (file)
index 0000000..021be8e
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import classNames from 'classnames';
+import { translate } from '../../../helpers/l10n';
+import type { SourceLine } from '../types';
+
+type Props = {
+  duplicated: boolean,
+  index: number,
+  line: SourceLine,
+  onClick: (index: number, lineNumber: number) => void
+};
+
+export default class LineDuplicationBlock extends React.PureComponent {
+  props: Props;
+
+  handleClick = (e: SyntheticInputEvent) => {
+    e.preventDefault();
+    this.props.onClick(this.props.index, this.props.line.line);
+  };
+
+  render () {
+    const { duplicated, index, line } = this.props;
+    const className = classNames('source-meta', 'source-line-duplications-extra', {
+      'source-line-duplicated': duplicated
+    });
+
+    return (
+      <td
+        key={index}
+        className={className}
+        data-line-number={line.line}
+        data-index={index}
+        title={duplicated ? translate('source_viewer.tooltip.duplicated_block') : undefined}
+        data-placement={duplicated ? 'right' : undefined}
+        data-toggle={duplicated ? 'tooltip' : undefined}
+        role={duplicated ? 'button' : undefined}
+        tabIndex={duplicated ? '0' : undefined}
+        onClick={duplicated ? this.handleClick : undefined}>
+        <div className="source-line-bar"/>
+      </td>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js
new file mode 100644 (file)
index 0000000..941227d
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import classNames from 'classnames';
+import { translate } from '../../../helpers/l10n';
+import type { SourceLine } from '../types';
+
+type Props = {
+  line: SourceLine,
+  onClick: (SourceLine, HTMLElement) => void
+};
+
+export default class LineDuplications extends React.PureComponent {
+  props: Props;
+
+  handleClick = (e: SyntheticInputEvent) => {
+    e.preventDefault();
+    this.props.onClick(this.props.line, e.target);
+  };
+
+  render () {
+    const { line } = this.props;
+    const className = classNames('source-meta', 'source-line-duplications', {
+      'source-line-duplicated': line.duplicated
+    });
+    const title = line.duplicated ? translate('source_viewer.tooltip.duplicated_line') : undefined;
+
+    return (
+      <td
+        className={className}
+        title={title}
+        data-placement={line.duplicated ? 'right' : undefined}
+        data-toggle={line.duplicated ? 'tooltip' : undefined}
+        role={line.duplicated ? 'button' : undefined}
+        tabIndex={line.duplicated ? 0 : undefined}
+        onClick={line.duplicated ? this.handleClick : undefined}>
+        <div className="source-line-bar"/>
+      </td>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js
new file mode 100644 (file)
index 0000000..2001742
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import classNames from 'classnames';
+import SeverityIcon from '../../shared/severity-icon';
+import { sortBySeverity } from '../../../helpers/issues';
+import type { SourceLine } from '../types';
+
+type Props = {
+  issues: Array<{ severity: string }>,
+  line: SourceLine,
+  onClick: () => void
+};
+
+export default class LineIssuesIndicator extends React.PureComponent {
+  props: Props;
+
+  handleClick = (e: SyntheticInputEvent) => {
+    e.preventDefault();
+    this.props.onClick();
+  };
+
+  render () {
+    const { issues, line } = this.props;
+    const hasIssues = issues.length > 0;
+    const className = classNames('source-meta', 'source-line-issues', {
+      'source-line-with-issues': hasIssues
+    });
+    const mostImportantIssue = hasIssues ? sortBySeverity(issues)[0] : null;
+
+    return (
+      <td
+        className={className}
+        data-line-number={line.line}
+        role={hasIssues ? 'button' : undefined}
+        tabIndex={hasIssues ? '0' : undefined}
+        onClick={hasIssues ? this.handleClick : undefined}>
+        {mostImportantIssue != null && <SeverityIcon severity={mostImportantIssue.severity}/>}
+        {issues.length > 1 && <span className="source-line-issues-counter">{issues.length}</span>}
+      </td>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicatorContainer.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicatorContainer.js
new file mode 100644 (file)
index 0000000..8d15af0
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+// @flow
+import { connect } from 'react-redux';
+import LineIssuesIndicator from './LineIssuesIndicator';
+import { getIssueByKey } from '../../../store/rootReducer';
+
+const mapStateToProps = (state, ownProps: { issueKeys: Array<string> }) => ({
+  issues: ownProps.issueKeys.map(issueKey => getIssueByKey(state, issueKey))
+});
+
+export default connect(mapStateToProps)(LineIssuesIndicator);
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js
new file mode 100644 (file)
index 0000000..0238b02
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import ConnectedIssue from '../../issue/ConnectedIssue';
+
+type Props = {
+  issueKeys: Array<string>,
+  onIssueClick: (issueKey: string) => void,
+  selectedIssue: string | null
+};
+
+export default class LineIssuesList extends React.PureComponent {
+  props: Props;
+
+  render () {
+    const { issueKeys, onIssueClick, selectedIssue } = this.props;
+
+    return (
+      <div className="issue-list">
+        {issueKeys.map(issueKey => (
+          <ConnectedIssue
+            issueKey={issueKey}
+            key={issueKey}
+            onClick={onIssueClick}
+            selected={selectedIssue === issueKey}/>
+        ))}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.js
new file mode 100644 (file)
index 0000000..a477bfb
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import type { SourceLine } from '../types';
+
+type Props = {
+  line: SourceLine,
+  onClick: (SourceLine, HTMLElement) => void
+};
+
+export default class LineNumber extends React.PureComponent {
+  props: Props;
+
+  handleClick = (e: SyntheticInputEvent) => {
+    e.preventDefault();
+    this.props.onClick(this.props.line, e.target);
+  };
+
+  render () {
+    const { line } = this.props.line;
+
+    return (
+      <td
+        className="source-meta source-line-number"
+        /* don't display 0 */
+        data-line-number={line ? line : undefined}
+        role={line ? 'button' : undefined}
+        tabIndex={line ? 0 : undefined}
+        onClick={line ? this.handleClick : undefined}/>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.js
new file mode 100644 (file)
index 0000000..b856b23
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import type { SourceLine } from '../types';
+
+type Props = {
+  line: SourceLine,
+  previousLine?: SourceLine,
+  onClick: (SourceLine, HTMLElement) => void
+};
+
+export default class LineSCM extends React.PureComponent {
+  props: Props;
+
+  handleClick = (e: SyntheticInputEvent) => {
+    e.preventDefault();
+    this.props.onClick(this.props.line, e.target);
+  };
+
+  isSCMChanged (s: SourceLine, p?: SourceLine) {
+    let changed = true;
+    if (p != null && s.scmAuthor != null && p.scmAuthor != null) {
+      changed = s.scmAuthor !== p.scmAuthor || s.scmDate !== p.scmDate;
+    }
+    return changed;
+  }
+
+  render () {
+    const { line, previousLine } = this.props;
+    const clickable = !!line.line;
+    return (
+      <td
+        className="source-meta source-line-scm"
+        data-line-number={line.line}
+        role={clickable ? 'button' : undefined}
+        tabIndex={clickable ? 0 : undefined}
+        onClick={clickable ? this.handleClick : undefined}>
+        {this.isSCMChanged(line, previousLine) &&
+          <div className="source-line-scm-inner" data-author={line.scmAuthor}/>}
+      </td>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js
new file mode 100644 (file)
index 0000000..eedeb69
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+// import { click } from '../../../../helpers/testUtils';
+import LineCode from '../LineCode';
+
+it('render code', () => {
+  const line = {
+    line: 3,
+    code: '<span class="k">class</span> <span class="sym sym-1">Foo</span> {'
+  };
+  const issueLocations = [{ from: 0, to: 5, line: 3 }];
+  const secondaryIssueLocations = [{ from: 6, to: 9, line: 3 }];
+  const secondaryIssueLocationMessages = [{ msg: 'Fix that', flowIndex: 0, locationIndex: 0 }];
+  const selectedIssueLocation = { from: 6, to: 9, line: 3, flowIndex: 0, locationIndex: 0 };
+  const wrapper = shallow(
+    <LineCode
+      highlightedSymbol="sym1"
+      issueKeys={['issue-1', 'issue-2']}
+      issueLocations={issueLocations}
+      line={line}
+      onIssueSelect={jest.fn()}
+      onSelectLocation={jest.fn()}
+      onSymbolClick={jest.fn()}
+      secondaryIssueLocations={secondaryIssueLocations}
+      secondaryIssueLocationMessages={secondaryIssueLocationMessages}
+      selectedIssue="issue-1"
+      selectedIssueLocation={selectedIssueLocation}
+      showIssues={true}/>
+  );
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js
new file mode 100644 (file)
index 0000000..5dcee39
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import { click } from '../../../../helpers/testUtils';
+import LineCoverage from '../LineCoverage';
+
+it('render covered line', () => {
+  const line = { line: 3, coverageStatus: 'covered' };
+  const onClick = jest.fn();
+  const wrapper = shallow(<LineCoverage line={line} onClick={onClick}/>);
+  expect(wrapper).toMatchSnapshot();
+  click(wrapper);
+  expect(onClick).toHaveBeenCalled();
+});
+
+it('render uncovered line', () => {
+  const line = { line: 3, coverageStatus: 'uncovered' };
+  const onClick = jest.fn();
+  const wrapper = shallow(<LineCoverage line={line} onClick={onClick}/>);
+  expect(wrapper).toMatchSnapshot();
+  click(wrapper);
+  expect(onClick).toHaveBeenCalled();
+});
+
+it('render line with unknown coverage', () => {
+  const line = { line: 3 };
+  const onClick = jest.fn();
+  const wrapper = shallow(<LineCoverage line={line} onClick={onClick}/>);
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js
new file mode 100644 (file)
index 0000000..e16dd8b
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import { click } from '../../../../helpers/testUtils';
+import LineDuplicationBlock from '../LineDuplicationBlock';
+
+it('render duplicated line', () => {
+  const line = { line: 3, duplicated: true };
+  const onClick = jest.fn();
+  const wrapper = shallow(
+    <LineDuplicationBlock index={1} duplicated={true} line={line} onClick={onClick}/>
+  );
+  expect(wrapper).toMatchSnapshot();
+  click(wrapper);
+  expect(onClick).toHaveBeenCalled();
+});
+
+it('render not duplicated line', () => {
+  const line = { line: 3, duplicated: false };
+  const onClick = jest.fn();
+  const wrapper = shallow(
+    <LineDuplicationBlock index={1} duplicated={false} line={line} onClick={onClick}/>
+  );
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js
new file mode 100644 (file)
index 0000000..1f11c8b
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import { click } from '../../../../helpers/testUtils';
+import LineDuplications from '../LineDuplications';
+
+it('render duplicated line', () => {
+  const line = { line: 3, duplicated: true };
+  const onClick = jest.fn();
+  const wrapper = shallow(<LineDuplications line={line} onClick={onClick}/>);
+  expect(wrapper).toMatchSnapshot();
+  click(wrapper);
+  expect(onClick).toHaveBeenCalled();
+});
+
+it('render not duplicated line', () => {
+  const line = { line: 3, duplicated: false };
+  const onClick = jest.fn();
+  const wrapper = shallow(<LineDuplications line={line} onClick={onClick}/>);
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.js
new file mode 100644 (file)
index 0000000..c2bb88e
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import { click } from '../../../../helpers/testUtils';
+import LineIssuesIndicator from '../LineIssuesIndicator';
+
+it('render highest severity', () => {
+  const line = { line: 3 };
+  const issues = [{ severity: 'MINOR' }, { severity: 'CRITICAL' }];
+  const onClick = jest.fn();
+  const wrapper = shallow(<LineIssuesIndicator issues={issues} line={line} onClick={onClick}/>);
+  expect(wrapper).toMatchSnapshot();
+
+  click(wrapper);
+  expect(onClick).toHaveBeenCalled();
+
+  const nextIssues = [{ severity: 'MINOR' }, { severity: 'INFO' }];
+  wrapper.setProps({ issues: nextIssues });
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('no issues', () => {
+  const line = { line: 3 };
+  const issues = [];
+  const onClick = jest.fn();
+  const wrapper = shallow(<LineIssuesIndicator issues={issues} line={line} onClick={onClick}/>);
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js
new file mode 100644 (file)
index 0000000..8f60222
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import LineIssuesList from '../LineIssuesList';
+
+it('render issues list', () => {
+  const line = { line: 3 };
+  const issueKeys = ['foo', 'bar'];
+  const onIssueClick = jest.fn();
+  const wrapper = shallow(
+    <LineIssuesList
+      issueKeys={issueKeys}
+      line={line}
+      onIssueClick={onIssueClick}
+      selectedIssue="foo"/>
+  );
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.js
new file mode 100644 (file)
index 0000000..eb120a2
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import { click } from '../../../../helpers/testUtils';
+import LineNumber from '../LineNumber';
+
+it('render line 3', () => {
+  const line = { line: 3 };
+  const onClick = jest.fn();
+  const wrapper = shallow(<LineNumber line={line} onClick={onClick}/>);
+  expect(wrapper).toMatchSnapshot();
+  click(wrapper);
+  expect(onClick).toHaveBeenCalled();
+});
+
+it('render line 0', () => {
+  const line = { line: 0 };
+  const onClick = jest.fn();
+  const wrapper = shallow(<LineNumber line={line} onClick={onClick}/>);
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.js
new file mode 100644 (file)
index 0000000..f1a812d
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import { click } from '../../../../helpers/testUtils';
+import LineSCM from '../LineSCM';
+
+it('render scm details', () => {
+  const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
+  const previousLine = { line: 2, scmAuthor: 'bar', scmDate: '2017-01-02' };
+  const onClick = jest.fn();
+  const wrapper = shallow(<LineSCM line={line} onClick={onClick} previousLine={previousLine}/>);
+  expect(wrapper).toMatchSnapshot();
+  click(wrapper);
+  expect(onClick).toHaveBeenCalled();
+});
+
+it('render scm details for the first line', () => {
+  const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
+  const onClick = jest.fn();
+  const wrapper = shallow(<LineSCM line={line} onClick={onClick}/>);
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('does not render scm details', () => {
+  const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
+  const previousLine = { line: 2, scmAuthor: 'foo', scmDate: '2017-01-01' };
+  const onClick = jest.fn();
+  const wrapper = shallow(<LineSCM line={line} onClick={onClick} previousLine={previousLine}/>);
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('does not allow to click', () => {
+  const line = { scmAuthor: 'foo', scmDate: '2017-01-01' };
+  const onClick = jest.fn();
+  const wrapper = shallow(<LineSCM line={line} onClick={onClick}/>);
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap
new file mode 100644 (file)
index 0000000..ecf619b
--- /dev/null
@@ -0,0 +1,34 @@
+exports[`test render code 1`] = `
+<td
+  className="source-line-code code has-issues"
+  data-line-number={3}>
+  <div
+    className="source-line-code-inner">
+    <pre
+      dangerouslySetInnerHTML={
+        Object {
+          "__html": "<span class=\"k source-line-code-issue\">class</span><span class=\"\"> </span><span class=\"sym sym-1 issue-location\">Foo</span><span class=\"\"> {</span>",
+        }
+      } />
+    <div
+      className="source-line-issue-locations">
+      <a
+        className="source-viewer-issue-location issue-location-message selected"
+        href="#"
+        onClick={[Function]}
+        title="Fix that">
+        Fix that
+      </a>
+    </div>
+  </div>
+  <LineIssuesList
+    issueKeys={
+      Array [
+        "issue-1",
+        "issue-2",
+      ]
+    }
+    onIssueClick={[Function]}
+    selectedIssue="issue-1" />
+</td>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap
new file mode 100644 (file)
index 0000000..ccf5c4d
--- /dev/null
@@ -0,0 +1,38 @@
+exports[`test render covered line 1`] = `
+<td
+  className="source-meta source-line-coverage source-line-covered"
+  data-line-number={3}
+  data-placement="right"
+  data-toggle="tooltip"
+  onClick={[Function]}
+  role="button"
+  tabIndex={0}
+  title="source_viewer.tooltip.covered">
+  <div
+    className="source-line-bar" />
+</td>
+`;
+
+exports[`test render line with unknown coverage 1`] = `
+<td
+  className="source-meta source-line-coverage"
+  data-line-number={3}>
+  <div
+    className="source-line-bar" />
+</td>
+`;
+
+exports[`test render uncovered line 1`] = `
+<td
+  className="source-meta source-line-coverage source-line-uncovered"
+  data-line-number={3}
+  data-placement="right"
+  data-toggle="tooltip"
+  onClick={[Function]}
+  role="button"
+  tabIndex={0}
+  title="source_viewer.tooltip.uncovered">
+  <div
+    className="source-line-bar" />
+</td>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap
new file mode 100644 (file)
index 0000000..b94d4b3
--- /dev/null
@@ -0,0 +1,25 @@
+exports[`test render duplicated line 1`] = `
+<td
+  className="source-meta source-line-duplications-extra source-line-duplicated"
+  data-index={1}
+  data-line-number={3}
+  data-placement="right"
+  data-toggle="tooltip"
+  onClick={[Function]}
+  role="button"
+  tabIndex="0"
+  title="source_viewer.tooltip.duplicated_block">
+  <div
+    className="source-line-bar" />
+</td>
+`;
+
+exports[`test render not duplicated line 1`] = `
+<td
+  className="source-meta source-line-duplications-extra"
+  data-index={1}
+  data-line-number={3}>
+  <div
+    className="source-line-bar" />
+</td>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap
new file mode 100644 (file)
index 0000000..7e977c8
--- /dev/null
@@ -0,0 +1,21 @@
+exports[`test render duplicated line 1`] = `
+<td
+  className="source-meta source-line-duplications source-line-duplicated"
+  data-placement="right"
+  data-toggle="tooltip"
+  onClick={[Function]}
+  role="button"
+  tabIndex={0}
+  title="source_viewer.tooltip.duplicated_line">
+  <div
+    className="source-line-bar" />
+</td>
+`;
+
+exports[`test render not duplicated line 1`] = `
+<td
+  className="source-meta source-line-duplications">
+  <div
+    className="source-line-bar" />
+</td>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap
new file mode 100644 (file)
index 0000000..a945f76
--- /dev/null
@@ -0,0 +1,37 @@
+exports[`test no issues 1`] = `
+<td
+  className="source-meta source-line-issues"
+  data-line-number={3} />
+`;
+
+exports[`test render highest severity 1`] = `
+<td
+  className="source-meta source-line-issues source-line-with-issues"
+  data-line-number={3}
+  onClick={[Function]}
+  role="button"
+  tabIndex="0">
+  <severity-icon
+    severity="CRITICAL" />
+  <span
+    className="source-line-issues-counter">
+    2
+  </span>
+</td>
+`;
+
+exports[`test render highest severity 2`] = `
+<td
+  className="source-meta source-line-issues source-line-with-issues"
+  data-line-number={3}
+  onClick={[Function]}
+  role="button"
+  tabIndex="0">
+  <severity-icon
+    severity="MINOR" />
+  <span
+    className="source-line-issues-counter">
+    2
+  </span>
+</td>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap
new file mode 100644 (file)
index 0000000..9279cc1
--- /dev/null
@@ -0,0 +1,13 @@
+exports[`test render issues list 1`] = `
+<div
+  className="issue-list">
+  <Connect(Connect(Issue))
+    issueKey="foo"
+    onClick={[Function]}
+    selected={true} />
+  <Connect(Connect(Issue))
+    issueKey="bar"
+    onClick={[Function]}
+    selected={false} />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.js.snap
new file mode 100644 (file)
index 0000000..a14778f
--- /dev/null
@@ -0,0 +1,13 @@
+exports[`test render line 0 1`] = `
+<td
+  className="source-meta source-line-number" />
+`;
+
+exports[`test render line 3 1`] = `
+<td
+  className="source-meta source-line-number"
+  data-line-number={3}
+  onClick={[Function]}
+  role="button"
+  tabIndex={0} />
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.js.snap
new file mode 100644 (file)
index 0000000..34828c8
--- /dev/null
@@ -0,0 +1,43 @@
+exports[`test does not allow to click 1`] = `
+<td
+  className="source-meta source-line-scm">
+  <div
+    className="source-line-scm-inner"
+    data-author="foo" />
+</td>
+`;
+
+exports[`test does not render scm details 1`] = `
+<td
+  className="source-meta source-line-scm"
+  data-line-number={3}
+  onClick={[Function]}
+  role="button"
+  tabIndex={0} />
+`;
+
+exports[`test render scm details 1`] = `
+<td
+  className="source-meta source-line-scm"
+  data-line-number={3}
+  onClick={[Function]}
+  role="button"
+  tabIndex={0}>
+  <div
+    className="source-line-scm-inner"
+    data-author="foo" />
+</td>
+`;
+
+exports[`test render scm details for the first line 1`] = `
+<td
+  className="source-meta source-line-scm"
+  data-line-number={3}
+  onClick={[Function]}
+  role="button"
+  tabIndex={0}>
+  <div
+    className="source-line-scm-inner"
+    data-author="foo" />
+</td>
+`;
index c0ca46bb9d19d15a46819fd16d78b03edc8430cb..c448c519b56863c37c90bd32f72547533167adf5 100644 (file)
@@ -21,8 +21,8 @@
 import escapeHtml from 'escape-html';
 import type { LinearIssueLocation } from './indexing';
 
-type Token = { className: string, text: string };
-type Tokens = Array<Token>;
+export type Token = { className: string, text: string };
+export type Tokens = Array<Token>;
 
 const ISSUE_LOCATION_CLASS = 'source-line-code-issue';
 
@@ -33,7 +33,7 @@ export const splitByTokens = (code: string, rootClassName: string = ''): Tokens
   [].forEach.call(container.childNodes, node => {
     if (node.nodeType === 1) {
       // ELEMENT NODE
-      const fullClassName = rootClassName ? (rootClassName + ' ' + node.className) : node.className;
+      const fullClassName = rootClassName ? rootClassName + ' ' + node.className : node.className;
       const innerTokens = splitByTokens(node.innerHTML, fullClassName);
       tokens = tokens.concat(innerTokens);
     }
@@ -45,11 +45,13 @@ export const splitByTokens = (code: string, rootClassName: string = ''): Tokens
   return tokens;
 };
 
-export const highlightSymbol = (tokens: Tokens, symbol: string): Tokens => (
-  tokens.map(token => token.className.includes(symbol) ?
-    { ...token, className: `${token.className} highlighted` } :
-    token
-));
+export const highlightSymbol = (tokens: Tokens, symbol: string): Tokens =>
+  tokens.map(
+    token =>
+      token.className.includes(symbol)
+        ? { ...token, className: `${token.className} highlighted` }
+        : token
+  );
 
 /**
  * Intersect two ranges
@@ -58,7 +60,12 @@ export const highlightSymbol = (tokens: Tokens, symbol: string): Tokens => (
  * @param s2 Start position of the second range
  * @param e2 End position of the second range
  */
-const intersect = (s1: number, e1: number, s2: number, e2: number): { from: number, to: number } => {
+const intersect = (
+  s1: number,
+  e1: number,
+  s2: number,
+  e2: number
+): { from: number, to: number } => {
   return { from: Math.max(s1, s2), to: Math.min(e1, e2) };
 };
 
@@ -94,9 +101,9 @@ export const highlightIssueLocations = (
         nextTokens.push({ className: token.className, text: p1 });
       }
       if (p2.length) {
-        const newClassName = token.className.indexOf(rootClassName) === -1 ?
-            `${token.className} ${rootClassName}` :
-            token.className;
+        const newClassName = token.className.indexOf(rootClassName) === -1
+          ? `${token.className} ${rootClassName}`
+          : token.className;
         nextTokens.push({ className: newClassName, text: p2 });
       }
       if (p3.length) {
@@ -110,7 +117,7 @@ export const highlightIssueLocations = (
 };
 
 export const generateHTML = (tokens: Tokens): string => {
-  return tokens.map(token => (
-      `<span class="${token.className}">${escapeHtml(token.text)}</span>`
-  )).join('');
+  return tokens
+      .map(token => `<span class="${token.className}">${escapeHtml(token.text)}</span>`)
+      .join('');
 };
index dcfe2f273faa402b3cab4433c1a0d7ceccd19578..13b2926d4ca39eb680f392c1014dd2fffb6b9e1e 100644 (file)
@@ -34,7 +34,7 @@ export type IndexedIssueLocation = {
   from: number,
   line: number,
   locationIndex: number,
-  to: number,
+  to: number
 };
 
 export type IndexedIssueLocationMessage = {
@@ -81,7 +81,9 @@ export const locationsByLine = (issues: Array<Issue>): { [number]: Array<LinearI
   return index;
 };
 
-export const locationsByIssueAndLine = (issues: Array<Issue>): IndexedIssueLocationsByIssueAndLine => {
+export const locationsByIssueAndLine = (
+  issues: Array<Issue>
+): IndexedIssueLocationsByIssueAndLine => {
   const index = {};
   issues.forEach(issue => {
     const byLine = {};
@@ -102,7 +104,9 @@ export const locationsByIssueAndLine = (issues: Array<Issue>): IndexedIssueLocat
   return index;
 };
 
-export const locationMessagesByIssueAndLine = (issues: Array<Issue>): IndexedIssueLocationMessagesByIssueAndLine => {
+export const locationMessagesByIssueAndLine = (
+  issues: Array<Issue>
+): IndexedIssueLocationMessagesByIssueAndLine => {
   const index = {};
   issues.forEach(issue => {
     const byLine = {};
@@ -158,7 +162,8 @@ export const symbolsByLine = (sources: Array<SourceLine>) => {
 export const findLocationByIndex = (
   locations: IndexedIssueLocationsByIssueAndLine,
   flowIndex: number,
-  locationIndex: number) => {
+  locationIndex: number
+) => {
   const issueKeys = Object.keys(locations);
   for (const issueKey of issueKeys) {
     const lineNumbers = Object.keys(locations[issueKey]);
index 70af97af1a568ddd38693e093a99ff4dafcbafc7..54459e3534b718fc619c825ca857c6efb9e8a6dc 100644 (file)
@@ -20,7 +20,9 @@
 // @flow
 import type { TextRange, Issue } from '../../issue/types';
 
-export const getLinearLocations = (textRange?: TextRange): Array<{ line: number, from: number, to: number }> => {
+export const getLinearLocations = (
+  textRange?: TextRange
+): Array<{ line: number, from: number, to: number }> => {
   if (!textRange) {
     return [];
   }
@@ -36,7 +38,9 @@ export const getLinearLocations = (textRange?: TextRange): Array<{ line: number,
   return locations;
 };
 
-export const getIssueLocations = (issue: Issue): Array<{
+export const getIssueLocations = (
+  issue: Issue
+): Array<{
   msg: string,
   flowIndex: number,
   locationIndex: number,
index ddc2963c0e7847c52bfca26cab4b09e1826e27fc..3a9d00566ced7279b10400a3be8aa2f214d66a4b 100644 (file)
@@ -35,10 +35,17 @@ const buildQuery = (component: string): Query => ({
   s: 'FILE_LINE'
 });
 
-export const loadPage = (query: Query, page: number, pageSize: number = PAGE_SIZE): Promise<Issues> => {
-  return searchIssues({ ...query, p: page, ps: pageSize }).then(r => (
-    r.issues.map(issue => parseIssueFromResponse(issue, r.components, r.users, r.rules))
-  ));
+export const loadPage = (
+  query: Query,
+  page: number,
+  pageSize: number = PAGE_SIZE
+): Promise<Issues> => {
+  return searchIssues({
+    ...query,
+    p: page,
+    ps: pageSize
+  }).then(r =>
+    r.issues.map(issue => parseIssueFromResponse(issue, r.components, r.users, r.rules)));
 };
 
 export const loadPageAndNext = (
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js
new file mode 100644 (file)
index 0000000..145d5db
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import $ from 'jquery';
+import groupBy from 'lodash/groupBy';
+import Popup from '../../common/popup';
+import Template from './templates/source-viewer-coverage-popup.hbs';
+
+export default Popup.extend({
+  template: Template,
+
+  events: {
+    'click a[data-key]': 'goToFile'
+  },
+
+  onRender () {
+    Popup.prototype.onRender.apply(this, arguments);
+    this.$('.bubble-popup-container').isolatedScroll();
+  },
+
+  goToFile (e) {
+    e.stopPropagation();
+    const key = $(e.currentTarget).data('key');
+    const Workspace = require('../../workspace/main').default;
+    Workspace.openComponent({ key });
+  },
+
+  serializeData () {
+    const row = this.options.line || {};
+    const tests = groupBy(this.options.tests, 'fileKey');
+    const testFiles = Object.keys(tests).map(fileKey => {
+      const testSet = tests[fileKey];
+      const test = testSet[0];
+      return {
+        file: {
+          key: test.fileKey,
+          longName: test.fileName
+        },
+        tests: testSet
+      };
+    });
+    return { testFiles, row };
+  }
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js
new file mode 100644 (file)
index 0000000..d8ef03e
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import $ from 'jquery';
+import groupBy from 'lodash/groupBy';
+import sortBy from 'lodash/sortBy';
+import Popup from '../../common/popup';
+import Template from './templates/source-viewer-duplication-popup.hbs';
+
+export default Popup.extend({
+  template: Template,
+
+  events: {
+    'click a[data-key]': 'goToFile'
+  },
+
+  goToFile (e) {
+    e.stopPropagation();
+    const key = $(e.currentTarget).data('key');
+    const line = $(e.currentTarget).data('line');
+    const Workspace = require('../../workspace/main').default;
+    Workspace.openComponent({ key, line });
+  },
+
+  serializeData () {
+    const that = this;
+    const groupedBlocks = groupBy(this.options.blocks, '_ref');
+    let duplications = Object.keys(groupedBlocks).map(fileRef => {
+      return {
+        blocks: groupedBlocks[fileRef],
+        file: this.options.files[fileRef]
+      };
+    });
+    duplications = sortBy(duplications, d => {
+      const a = d.file.projectName !== that.options.component.projectName;
+      const b = d.file.subProjectName !== that.options.component.subProjectName;
+      const c = d.file.key !== that.options.component.key;
+      return '' + a + b + c;
+    });
+    return {
+      duplications,
+      component: this.options.component,
+      inRemovedComponent: this.options.inRemovedComponent
+    };
+  }
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js
new file mode 100644 (file)
index 0000000..e65d748
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import Popup from '../../common/popup';
+import Template from './templates/source-viewer-line-options-popup.hbs';
+
+export default Popup.extend({
+  template: Template,
+
+  events: {
+    'click .js-get-permalink': 'getPermalink'
+  },
+
+  getPermalink (e) {
+    e.preventDefault();
+    const { component, line } = this.options;
+    const url = `${window.baseUrl}/component/index?id=${encodeURIComponent(component.key)}&line=${line}`;
+    const windowParams = 'resizable=1,scrollbars=1,status=1';
+    window.open(url, component.name, windowParams);
+  }
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/scm-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/scm-popup.js
new file mode 100644 (file)
index 0000000..06cbf45
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import Popup from '../../common/popup';
+import Template from './templates/source-viewer-scm-popup.hbs';
+
+export default Popup.extend({
+  template: Template,
+
+  events: {
+    'click': 'onClick'
+  },
+
+  onRender () {
+    Popup.prototype.onRender.apply(this, arguments);
+    this.$('.bubble-popup-container').isolatedScroll();
+  },
+
+  onClick (e) {
+    e.stopPropagation();
+  },
+
+  serializeData () {
+    return {
+      ...Popup.prototype.serializeData.apply(this, arguments),
+      line: this.options.line
+    };
+  }
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-coverage-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-coverage-popup.hbs
new file mode 100644 (file)
index 0000000..57c6301
--- /dev/null
@@ -0,0 +1,39 @@
+<div class="bubble-popup-container">
+  <div class="bubble-popup-title">
+    {{#if row.lineHits}}
+      {{t 'source_viewer.covered'}}
+      {{#if row.conditions}}
+        ({{default row.coveredConditions 0}} of {{row.conditions}} {{t 'source_viewer.conditions'}})
+      {{/if}}
+    {{else}}
+      {{t 'source_viewer.not_covered'}}
+      {{#if row.conditions}}
+        ({{row.conditions}} {{t 'source_viewer.conditions'}})
+      {{/if}}
+    {{/if}}
+  </div>
+
+  {{#each testFiles}}
+    <div class="bubble-popup-section">
+      <a class="component-viewer-popup-test-file link-action" data-key="{{file.key}}" title="{{file.longName}}">
+        <span>{{collapsePath file.longName}}</span>
+      </a>
+      <ul class="bubble-popup-list">
+        {{#each tests}}
+          <li class="component-viewer-popup-test" title="{{name}}">
+            <i class="component-viewer-popup-test-status {{testStatusIconClass status}}"></i>
+            <span class="component-viewer-popup-test-name">
+              <a class="component-viewer-popup-test-file link-action" title="{{name}}"
+                 data-key="{{../file.key}}" data-method="{{name}}">
+                {{name}}
+              </a>
+            </span>
+            <span class="component-viewer-popup-test-duration">{{durationInMs}}ms</span>
+          </li>
+        {{/each}}
+      </ul>
+    </div>
+  {{/each}}
+</div>
+
+<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-duplication-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-duplication-popup.hbs
new file mode 100644 (file)
index 0000000..ea8fc2b
--- /dev/null
@@ -0,0 +1,45 @@
+<div class="bubble-popup-container">
+  {{#if inRemovedComponent}}
+    <div class="alert alert-warning spacer-bottom">{{t 'duplications.dups_found_on_deleted_resource'}}</div>
+  {{/if}}
+  {{#notEmpty duplications}}
+    <div class="bubble-popup-title">{{t 'component_viewer.transition.duplication'}}</div>
+    {{#each duplications}}
+      <div class="bubble-popup-section">
+        <div class="component-name">
+          {{#notEqComponents file ../component}}
+            <div class="component-name-parent">
+              <i class="icon-qualifier-trk"></i>&nbsp;<a href="{{dashboardUrl file.project}}">{{file.projectName}}</a>
+            </div>
+            {{#if file.subProjectName}}
+              <div class="component-name-parent">
+                <i class="icon-qualifier-trk"></i>&nbsp;<a
+                  href="{{dashboardUrl file.subProject}}">{{file.subProjectName}}</a>
+              </div>
+            {{/if}}
+          {{/notEqComponents}}
+
+          {{#notEq file.key ../component.key}}
+            <div class="component-name-path">
+              <a class="link-action" data-key="{{file.key}}" title="{{file.name}}">
+                <span>{{collapsedDirFromPath file.name}}</span><span
+                  class="component-name-file">{{fileFromPath file.name}}</span>
+              </a>
+            </div>
+          {{/notEq}}
+
+          <div class="component-name-path">
+            Lines:
+            {{#joinEach blocks ','}}
+              <a class="link-action" data-key="{{../file.key}}" data-line="{{this.from}}">
+                {{this.from}} – {{sum from size -1}}
+              </a>
+            {{/joinEach}}
+          </div>
+        </div>
+      </div>
+    {{/each}}
+  {{/notEmpty}}
+</div>
+
+<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-line-options-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-line-options-popup.hbs
new file mode 100644 (file)
index 0000000..c6b9b41
--- /dev/null
@@ -0,0 +1,7 @@
+<div class="bubble-popup-container">
+  <div class="bubble-popup-section">
+    <a href="#" class="js-get-permalink link-action">{{t 'component_viewer.get_permalink'}}</a>
+  </div>
+</div>
+
+<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-scm-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-scm-popup.hbs
new file mode 100644 (file)
index 0000000..dd82aca
--- /dev/null
@@ -0,0 +1,15 @@
+<div class="bubble-popup-container">
+  <div class="bubble-popup-section">
+    {{line.scmAuthor}}
+  </div>
+  <div class="bubble-popup-section">
+    {{dt line.scmDate}}
+  </div>
+  {{#if line.scmRevision}}
+    <div class="bubble-popup-section">
+      {{line.scmRevision}}
+    </div>
+  {{/if}}
+</div>
+
+<div class="bubble-popup-arrow"></div>
index 6371f9e8cb3ac49bc0787669a81dde24ec1fca4c..bd79e0fd79c142663685d455a41c4f5ce089c546 100644 (file)
@@ -53,9 +53,8 @@
 .issue-location-message {
   display: inline-block;
   vertical-align: top;
-  line-height: 16px;
-  height: 17px;
-  border: 1px solid #ffeaea;
+  line-height: 18px;
+  height: 18px;
   box-sizing: border-box;
   background-color: #ffeaea;
 }
@@ -76,6 +75,7 @@
 
 .issue-location-message {
   padding: 0 10px;
+  border: 1px solid #ffeaea;
   color: #444 !important;
   font-size: 12px;
   white-space: nowrap;
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
new file mode 100644 (file)
index 0000000..6dee58b
--- /dev/null
@@ -0,0 +1,284 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import $ from 'jquery';
+import groupBy from 'lodash/groupBy';
+import sortBy from 'lodash/sortBy';
+import toPairs from 'lodash/toPairs';
+import ModalView from '../../common/modals';
+import Template from './templates/source-viewer-measures.hbs';
+import { getMeasures } from '../../../api/measures';
+import { getMetrics } from '../../../api/metrics';
+import { formatMeasure } from '../../../helpers/measures';
+
+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: '#e6e6e6'
+    };
+
+    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 = d3.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 = d3.svg.arc()
+          .innerRadius(radius - options.thickness)
+          .outerRadius(radius);
+      const pie = d3.layout.pie()
+          .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);
+  },
+
+  getMetrics () {
+    let metrics = '';
+    const url = window.baseUrl + '/api/metrics/search';
+    $.ajax({
+      url,
+      async: false,
+      data: { ps: 9999 }
+    }).done(data => {
+      metrics = data.metrics.filter(metric => metric.type !== 'DATA' && !metric.hidden);
+      metrics = sortBy(metrics, 'name');
+    });
+    return metrics;
+  },
+
+  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);
+    return sortBy(
+      toPairs(groupBy(metrics, 'domain')).map(domain => {
+        return {
+          name: domain[0],
+          metrics: domain[1]
+        };
+      }),
+      'name'
+    );
+  },
+
+  requestMeasures () {
+    return getMetrics().then(metrics => {
+      const metricsToRequest = metrics
+          .filter(metric => metric.type !== 'DATA' && !metric.hidden)
+          .map(metric => metric.key);
+
+      return getMeasures(this.options.component.key, metricsToRequest).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 () {
+    return new Promise(resolve => {
+      const url = window.baseUrl + '/api/issues/search';
+      const options = {
+        componentKeys: this.options.component.key,
+        resolved: false,
+        ps: 1,
+        facets: 'types,severities,tags'
+      };
+
+      $.get(url, options).done(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 => window.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;
+
+        resolve();
+      });
+    });
+  },
+
+  requestTests () {
+    return new Promise(resolve => {
+      const url = window.baseUrl + '/api/tests/list';
+      const options = { testFileKey: this.options.component.key };
+
+      $.get(url, options).done(data => {
+        this.tests = data.tests;
+        this.testSorting = 'status';
+        this.testAsc = true;
+        this.sortTests(test => `${this.testsOrder.indexOf(test.status)}_______${test.name}`);
+        resolve();
+      });
+    });
+  },
+
+  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');
+    const url = window.baseUrl + '/api/tests/covered_files';
+    const options = { testId };
+    this.testsScroll = $(e.currentTarget).scrollParent().scrollTop();
+    return $.get(url, options).done(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
new file mode 100644 (file)
index 0000000..cac99fe
--- /dev/null
@@ -0,0 +1,42 @@
+{{#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
new file mode 100644 (file)
index 0000000..2598e96
--- /dev/null
@@ -0,0 +1,36 @@
+{{#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
new file mode 100644 (file)
index 0000000..f6119c3
--- /dev/null
@@ -0,0 +1,30 @@
+{{#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="#f3ca8e"></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">Duplications</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
new file mode 100644 (file)
index 0000000..30a3914
--- /dev/null
@@ -0,0 +1,47 @@
+<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
new file mode 100644 (file)
index 0000000..f0c81d0
--- /dev/null
@@ -0,0 +1,29 @@
+<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">Comments</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
new file mode 100644 (file)
index 0000000..6b0d5a1
--- /dev/null
@@ -0,0 +1,73 @@
+<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'}}
+        {{log selectedTest}}
+        <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
new file mode 100644 (file)
index 0000000..c9b33c3
--- /dev/null
@@ -0,0 +1,40 @@
+<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
new file mode 100644 (file)
index 0000000..ebcc2e7
--- /dev/null
@@ -0,0 +1,68 @@
+<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>
diff --git a/server/sonar-web/src/main/js/components/__tests__/source-viewer-test.js b/server/sonar-web/src/main/js/components/__tests__/source-viewer-test.js
deleted file mode 100644 (file)
index 7b04a17..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import helper from '../source-viewer/helpers/code-with-issue-locations-helper';
-
-describe('Code With Issue Locations Helper', () => {
-  it('should be a function', () => {
-    expect(helper).toBeTruthy();
-  });
-
-  it('should mark one location', () => {
-    const code = '<span class="k">if</span> (<span class="sym-2 sym">a</span> + <span class="c">1</span>) {';
-    const locations = [{ from: 1, to: 5 }];
-    const result = helper(code, locations, 'x');
-    expect(result).toBe([
-      '<span class="k">i</span>',
-      '<span class="k x">f</span>',
-      '<span class=" x"> (</span>',
-      '<span class="sym-2 sym x">a</span>',
-      '<span class=""> + </span>',
-      '<span class="c">1</span>',
-      '<span class="">) {</span>'
-    ].join(''));
-  });
-
-  it('should mark two locations', () => {
-    const code = 'abcdefghijklmnopqrst';
-    const locations = [
-      { from: 1, to: 6 },
-      { from: 11, to: 16 }
-    ];
-    const result = helper(code, locations, 'x');
-    expect(result).toBe([
-      '<span class="">a</span>',
-      '<span class=" x">bcdef</span>',
-      '<span class="">ghijk</span>',
-      '<span class=" x">lmnop</span>',
-      '<span class="">qrst</span>'
-    ].join(''));
-  });
-
-  it('should mark one locations', () => {
-    const code = '<span class="cppd"> * Copyright (C) 2008-2014 SonarSource</span>';
-    const locations = [{ from: 15, to: 20 }];
-    const result = helper(code, locations, 'x');
-    expect(result).toBe([
-      '<span class="cppd"> * Copyright (C</span>',
-      '<span class="cppd x">) 200</span>',
-      '<span class="cppd">8-2014 SonarSource</span>'
-    ].join(''));
-  });
-
-  it('should mark two locations', () => {
-    const code = '<span class="cppd"> * Copyright (C) 2008-2014 SonarSource</span>';
-    const locations = [
-      { from: 24, to: 29 },
-      { from: 15, to: 20 }
-    ];
-    const result = helper(code, locations, 'x');
-    expect(result).toBe([
-      '<span class="cppd"> * Copyright (C</span>',
-      '<span class="cppd x">) 200</span>',
-      '<span class="cppd">8-20</span>',
-      '<span class="cppd x">14 So</span>',
-      '<span class="cppd">narSource</span>'
-    ].join(''));
-  });
-
-  it('should parse line with < and >', () => {
-    const code = '<span class="j">#include &lt;stdio.h&gt;</span>';
-    const result = helper(code, []);
-    expect(result).toBe('<span class="j">#include &lt;stdio.h&gt;</span>');
-  });
-
-  it('should parse syntax and usage highlighting', () => {
-    const code = '<span class="k"><span class="sym-3 sym">this</span></span>';
-    const expected = '<span class="k sym-3 sym">this</span>';
-    const result = helper(code, []);
-    expect(result).toBe(expected);
-  });
-
-  it('should parse nested tags', () => {
-    const code = '<span class="k"><span class="sym-3 sym">this</span> is</span>';
-    const expected = '<span class="k sym-3 sym">this</span><span class="k"> is</span>';
-    const result = helper(code, []);
-    expect(result).toBe(expected);
-  });
-});
-
index cdd7d95de952a1d893271dbe91117b1ea9e50053..0bb7fbd1b31cd816216bf492c4212e1933a7f35e 100644 (file)
@@ -29,7 +29,6 @@ import DeleteCommentView from './views/DeleteCommentView';
 import SetSeverityFormView from './views/set-severity-form-view';
 import SetTypeFormView from './views/set-type-form-view';
 import TagsFormView from './views/tags-form-view';
-import Workspace from '../workspace/main';
 import Template from './templates/issue.hbs';
 import getCurrentUserFromStore from '../../app/utils/getCurrentUserFromStore';
 
@@ -242,6 +241,8 @@ export default Marionette.ItemView.extend({
     e.preventDefault();
     e.stopPropagation();
     const ruleKey = this.model.get('rule');
+    // lazy load Workspace
+    const Workspace = require('../workspace/main').default;
     Workspace.openRule({ key: ruleKey });
   },
 
diff --git a/server/sonar-web/src/main/js/components/source-viewer/header.js b/server/sonar-web/src/main/js/components/source-viewer/header.js
deleted file mode 100644 (file)
index 739037e..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-/* @flow */
-import $ from 'jquery';
-import Marionette from 'backbone.marionette';
-import MoreActionsView from './more-actions';
-import MeasuresOverlay from './measures-overlay';
-import Template from './templates/source-viewer-header.hbs';
-import { addFavorite, removeFavorite } from '../../api/favorites';
-
-export default Marionette.ItemView.extend({
-  template: Template,
-
-  events () {
-    return {
-      'click .js-favorite': 'toggleFavorite',
-      'click .js-actions': 'showMoreActions',
-      'click .js-permalink': 'getPermalink'
-    };
-  },
-
-  toggleFavorite () {
-    if (this.model.get('fav')) {
-      removeFavorite(this.model.get('key')).then(() => {
-        this.model.set('fav', false);
-        this.render();
-      });
-    } else {
-      addFavorite(this.model.get('key')).then(() => {
-        this.model.set('fav', true);
-        this.render();
-      });
-    }
-  },
-
-  showMoreActions (e) {
-    e.stopPropagation();
-    $('body').click();
-    const view = new MoreActionsView({ parent: this });
-    view.render().$el.appendTo(this.$el);
-  },
-
-  getPermalink () {
-    let query = 'id=' + encodeURIComponent(this.model.get('key'));
-    const windowParams = 'resizable=1,scrollbars=1,status=1';
-    if (this.options.viewer.highlightedLine) {
-      query = query + '&line=' + this.options.viewer.highlightedLine;
-    }
-    window.open(window.baseUrl + '/component/index?' + query, this.model.get('name'), windowParams);
-  },
-
-  showRawSources () {
-    const url = window.baseUrl + '/api/sources/raw?key=' + encodeURIComponent(this.model.get('key'));
-    const windowParams = 'resizable=1,scrollbars=1,status=1';
-    window.open(url, this.model.get('name'), windowParams);
-  },
-
-  showMeasures () {
-    new MeasuresOverlay({
-      model: this.model,
-      large: true
-    }).render();
-  },
-
-  serializeData () {
-    return {
-      ...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
-      path: this.model.get('path') || this.model.get('longName')
-    };
-  }
-});
diff --git a/server/sonar-web/src/main/js/components/source-viewer/helpers/code-with-issue-locations-helper.js b/server/sonar-web/src/main/js/components/source-viewer/helpers/code-with-issue-locations-helper.js
deleted file mode 100644 (file)
index 4b0307c..0000000
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import escapeHtml from 'escape-html';
-
-/**
- * Intersect two ranges
- * @param {number} s1 Start position of the first range
- * @param {number} e1 End position of the first range
- * @param {number} s2 Start position of the second range
- * @param {number} e2 End position of the second range
- * @returns {{from: number, to: number}}
- */
-function intersect (s1, e1, s2, e2) {
-  return { from: Math.max(s1, s2), to: Math.min(e1, e2) };
-}
-
-/**
- * Get the substring of a string
- * @param {string} str A string
- * @param {number} from "From" offset
- * @param {number} to "To" offset
- * @param {number} acc Global offset to eliminate
- * @returns {string}
- */
-function part (str, from, to, acc) {
-  // we do not want negative number as the first argument of `substr`
-  return from >= acc ? str.substr(from - acc, to - from) : str.substr(0, to - from);
-}
-
-/**
- * Split a code html into tokens
- * @param {string} code
- * @param {string} rootClassName
- * @returns {Array}
- */
-function splitByTokens (code, rootClassName = '') {
-  const container = document.createElement('div');
-  let tokens = [];
-  container.innerHTML = code;
-  [].forEach.call(container.childNodes, node => {
-    if (node.nodeType === 1) {
-      // ELEMENT NODE
-      const fullClassName = rootClassName ? (rootClassName + ' ' + node.className) : node.className;
-      const innerTokens = splitByTokens(node.innerHTML, fullClassName);
-      tokens = tokens.concat(innerTokens);
-    }
-    if (node.nodeType === 3) {
-      // TEXT NODE
-      tokens.push({ className: rootClassName, text: node.nodeValue });
-    }
-  });
-  return tokens;
-}
-
-/**
- * Highlight issue locations in the list of tokens
- * @param {Array} tokens
- * @param {Array} issueLocations
- * @param {string} className
- * @returns {Array}
- */
-function highlightIssueLocations (tokens, issueLocations, className) {
-  issueLocations.forEach(location => {
-    const nextTokens = [];
-    let acc = 0;
-    tokens.forEach(token => {
-      const x = intersect(acc, acc + token.text.length, location.from, location.to);
-      const p1 = part(token.text, acc, x.from, acc);
-      const p2 = part(token.text, x.from, x.to, acc);
-      const p3 = part(token.text, x.to, acc + token.text.length, acc);
-      if (p1.length) {
-        nextTokens.push({ className: token.className, text: p1 });
-      }
-      if (p2.length) {
-        const newClassName = token.className.indexOf(className) === -1 ?
-            [token.className, className].join(' ') : token.className;
-        nextTokens.push({ className: newClassName, text: p2 });
-      }
-      if (p3.length) {
-        nextTokens.push({ className: token.className, text: p3 });
-      }
-      acc += token.text.length;
-    });
-    tokens = nextTokens.slice();
-  });
-  return tokens;
-}
-
-/**
- * Generate an html string from the list of tokens
- * @param {Array} tokens
- * @returns {string}
- */
-function generateHTML (tokens) {
-  return tokens.map(token => (
-      `<span class="${token.className}">${escapeHtml(token.text)}</span>`
-  )).join('');
-}
-
-/**
- * Take the initial source code, split by tokens,
- * highlight issues and generate result html
- * @param {string} code
- * @param {Array} issueLocations
- * @param {string} [optionalClassName]
- * @returns {string}
- */
-function doTheStuff (code, issueLocations, optionalClassName) {
-  const _code = code || '&nbsp;';
-  const _issueLocations = issueLocations || [];
-  const _className = optionalClassName ? optionalClassName : 'source-line-code-issue';
-  return generateHTML(highlightIssueLocations(splitByTokens(_code), _issueLocations, _className));
-}
-
-export default doTheStuff;
-
diff --git a/server/sonar-web/src/main/js/components/source-viewer/main.js b/server/sonar-web/src/main/js/components/source-viewer/main.js
deleted file mode 100644 (file)
index 8b1725d..0000000
+++ /dev/null
@@ -1,790 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import $ from 'jquery';
-import moment from 'moment';
-import sortBy from 'lodash/sortBy';
-import toPairs from 'lodash/toPairs';
-import Marionette from 'backbone.marionette';
-import Source from './source';
-import Issues from '../issue/collections/issues';
-import IssueView from '../issue/issue-view';
-import HeaderView from './header';
-import SCMPopupView from './popups/scm-popup';
-import CoveragePopupView from './popups/coverage-popup';
-import DuplicationPopupView from './popups/duplication-popup';
-import LineActionsPopupView from './popups/line-actions-popup';
-import highlightLocations from './helpers/code-with-issue-locations-helper';
-import Template from './templates/source-viewer.hbs';
-import IssueLocationTemplate from './templates/source-viewer-issue-location.hbs';
-import { translateWithParameters } from '../../helpers/l10n';
-
-const HIGHLIGHTED_ROW_CLASS = 'source-line-highlighted';
-
-export default Marionette.LayoutView.extend({
-  className: 'source-viewer',
-  template: Template,
-  issueLocationTemplate: IssueLocationTemplate,
-
-  ISSUES_LIMIT: 3000,
-
-  LINES_AROUND: 500,
-
-  // keep it twice bigger than LINES_AROUND
-  LINES_LIMIT: 1000,
-  TOTAL_LINES_LIMIT: 1000,
-
-  regions: {
-    headerRegion: '.source-viewer-header'
-  },
-
-  ui: {
-    sourceBeforeSpinner: '.js-component-viewer-source-before',
-    sourceAfterSpinner: '.js-component-viewer-source-after'
-  },
-
-  events () {
-    return {
-      'click .sym': 'highlightUsages',
-      'click .source-line-scm': 'showSCMPopup',
-      'click .source-line-covered': 'showCoveragePopup',
-      'click .source-line-partially-covered': 'showCoveragePopup',
-      'click .source-line-uncovered': 'showCoveragePopup',
-      'click .source-line-duplications': 'showDuplications',
-      'click .source-line-duplications-extra': 'showDuplicationPopup',
-      'click .source-line-with-issues': 'onLineIssuesClick',
-      'click .source-line-number[data-line-number]': 'onLineNumberClick',
-      'mouseenter .source-line-filtered .source-line-filtered-container': 'showFilteredTooltip',
-      'mouseleave .source-line-filtered .source-line-filtered-container': 'hideFilteredTooltip',
-      'click @ui.sourceBeforeSpinner': 'loadSourceBefore',
-      'click @ui.sourceAfterSpinner': 'loadSourceAfter'
-    };
-  },
-
-  initialize () {
-    if (this.model == null) {
-      this.model = new Source();
-    }
-    this.issues = new Issues();
-    this.listenTo(this.issues, 'change:severity', this.onIssuesSeverityChange);
-    this.listenTo(this.issues, 'locations', this.toggleIssueLocations);
-    this.issueViews = [];
-    this.highlightedLine = null;
-    this.listenTo(this, 'loaded', this.onLoaded);
-  },
-
-  renderHeader () {
-    this.headerRegion.show(new HeaderView({
-      viewer: this,
-      model: this.model
-    }));
-  },
-
-  onRender () {
-    this.renderHeader();
-    this.renderIssues();
-    if (this.model.has('filterLinesFunc')) {
-      this.filterLines(this.model.get('filterLinesFunc'));
-    }
-    this.$('[data-toggle="tooltip"]').tooltip({ container: 'body' });
-  },
-
-  onDestroy () {
-    this.issueViews.forEach(view => view.destroy());
-    this.issueViews = [];
-    this.clearTooltips();
-    this.unbindScrollEvents();
-  },
-
-  clearTooltips () {
-    this.$('[data-toggle="tooltip"]').tooltip('destroy');
-  },
-
-  onLoaded () {
-    this.bindScrollEvents();
-  },
-
-  open (id, options) {
-    const that = this;
-    const opts = typeof options === 'object' ? options : {};
-    const finalize = function () {
-      that.requestIssues().done(() => {
-        if (!that.isDestroyed) {
-          that.render();
-          that.trigger('loaded');
-        }
-      });
-    };
-    Object.assign(this.options, { workspace: false, ...opts });
-    this.model
-        .clear()
-        .set(this.model.defaults())
-        .set({ uuid: id });
-    this.requestComponent().done(() => {
-      that.requestSource(opts.aroundLine)
-          .done(finalize)
-          .fail(() => {
-            that.model.set({
-              source: [
-                { line: 0 }
-              ]
-            });
-            finalize();
-          });
-    });
-    return this;
-  },
-
-  requestComponent () {
-    const that = this;
-    const url = window.baseUrl + '/api/components/app';
-    const data = { uuid: this.model.id };
-    return $.ajax({
-      url,
-      data,
-      type: 'GET',
-      statusCode: {
-        404 () {
-          that.model.set({ exist: false });
-          that.render();
-          that.trigger('loaded');
-        }
-      }
-    }).done(r => {
-      that.model.set(r);
-      that.model.set({ isUnitTest: r.q === 'UTS' });
-    });
-  },
-
-  linesLimit (aroundLine) {
-    if (aroundLine) {
-      return {
-        from: Math.max(1, aroundLine - this.LINES_AROUND),
-        to: aroundLine + this.LINES_AROUND
-      };
-    }
-    return { from: 1, to: this.LINES_AROUND };
-  },
-
-  getCoverageStatus (row) {
-    let status = null;
-    if (row.lineHits > 0) {
-      status = 'partially-covered';
-    }
-    if (row.lineHits > 0 && row.conditions === row.coveredConditions) {
-      status = 'covered';
-    }
-    if (row.lineHits === 0 || row.coveredConditions === 0) {
-      status = 'uncovered';
-    }
-    return status;
-  },
-
-  requestSource (aroundLine) {
-    const that = this;
-    const url = window.baseUrl + '/api/sources/lines';
-    const data = { uuid: this.model.id, ...this.linesLimit(aroundLine) };
-    return $.ajax({
-      url,
-      data,
-      statusCode: {
-        // don't display global error
-        403: null
-      }
-    }).done(r => {
-      let source = (r.sources || []).slice(0);
-      if (source.length === 0 || (source.length > 0 && source[0].line === 1)) {
-        source.unshift({ line: 0 });
-      }
-      source = source.map(row => {
-        return { ...row, coverageStatus: that.getCoverageStatus(row) };
-      });
-      const firstLine = source.length > 0 ? source[0].line : null;
-      const linesRequested = data.to - data.from + 1;
-      that.model.set({
-        source,
-        hasCoverage: that.model.hasCoverage(source),
-        hasSourceBefore: firstLine > 1,
-        hasSourceAfter: r.sources.length === linesRequested
-      });
-      that.model.checkIfHasDuplications();
-    }).fail(request => {
-      if (request.status === 403) {
-        that.model.set({
-          source: [],
-          hasSourceBefore: false,
-          hasSourceAfter: false,
-          canSeeCode: false
-        });
-      }
-    });
-  },
-
-  requestDuplications () {
-    const that = this;
-    const url = window.baseUrl + '/api/duplications/show';
-    const options = { uuid: this.model.id };
-    return $.get(url, options, data => {
-      const hasDuplications = data.duplications != null;
-      let duplications = [];
-      if (hasDuplications) {
-        duplications = {};
-        data.duplications.forEach(d => {
-          d.blocks.forEach(b => {
-            if (b._ref === '1') {
-              const lineFrom = b.from;
-              const lineTo = b.from + b.size - 1;
-              for (let j = lineFrom; j <= lineTo; j++) {
-                duplications[j] = true;
-              }
-            }
-          });
-        });
-        duplications = toPairs(duplications).map(line => {
-          return {
-            line: +line[0],
-            duplicated: line[1]
-          };
-        });
-      }
-      that.model.addMeta(duplications);
-      that.model.addDuplications(data.duplications);
-      that.model.set({
-        duplications: data.duplications,
-        duplicationsParsed: duplications,
-        duplicationFiles: data.files
-      });
-    });
-  },
-
-  requestIssues () {
-    const that = this;
-    const options = {
-      data: {
-        componentUuids: this.model.id,
-        f: 'component,componentId,project,subProject,rule,status,resolution,author,assignee,debt,' +
-        'line,message,severity,creationDate,updateDate,closeDate,tags,comments,attr,actions,' +
-        'transitions',
-        additionalFields: '_all',
-        resolved: false,
-        s: 'FILE_LINE',
-        asc: true,
-        ps: this.ISSUES_LIMIT
-      }
-    };
-    return this.issues.fetch(options).done(() => {
-      that.addIssuesPerLineMeta(that.issues);
-    });
-  },
-
-  _sortBySeverity (issues) {
-    const order = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'];
-    return sortBy(issues, issue => order.indexOf(issue.severity));
-  },
-
-  addIssuesPerLineMeta (issues) {
-    const that = this;
-    const lines = {};
-    issues.forEach(issue => {
-      const line = issue.get('line') || 0;
-      if (!Array.isArray(lines[line])) {
-        lines[line] = [];
-      }
-      lines[line].push(issue.toJSON());
-    });
-    const issuesPerLine = toPairs(lines).map(line => {
-      return {
-        line: +line[0],
-        issues: that._sortBySeverity(line[1])
-      };
-    });
-    this.model.addMeta(issuesPerLine);
-    this.addIssueLocationsMeta(issues);
-  },
-
-  addIssueLocationsMeta (issues) {
-    const issueLocations = [];
-    issues.forEach(issue => {
-      issue.getLinearLocations().forEach(location => {
-        const record = issueLocations.find(row => row.line === location.line);
-        if (record) {
-          record.issueLocations.push({ from: location.from, to: location.to });
-        } else {
-          issueLocations.push({
-            line: location.line,
-            issueLocations: [{ from: location.from, to: location.to }]
-          });
-        }
-      });
-    });
-    this.model.addMeta(issueLocations);
-  },
-
-  renderIssues () {
-    this.$('.issue-list').addClass('hidden');
-  },
-
-  renderIssue (issue) {
-    const issueView = new IssueView({
-      el: '#issue-' + issue.get('key'),
-      model: issue
-    });
-    this.issueViews.push(issueView);
-    issueView.render();
-  },
-
-  addIssue (issue) {
-    const line = issue.get('line') || 0;
-    const code = this.$(`.source-line-code[data-line-number=${line}]`);
-    const issueBox = `<div class="issue" id="issue-${issue.get('key')}" data-key="${issue.get('key')}">`;
-    code.addClass('has-issues');
-    let issueList = code.find('.issue-list');
-    if (issueList.length === 0) {
-      code.append('<div class="issue-list"></div>');
-      issueList = code.find('.issue-list');
-    }
-    issueList
-        .append(issueBox)
-        .removeClass('hidden');
-    this.renderIssue(issue);
-  },
-
-  showIssuesForLine (line) {
-    this.$(`.source-line-code[data-line-number="${line}"]`).find('.issue-list').removeClass('hidden');
-    const issues = this.issues.filter(issue => (
-        (issue.get('line') === line) || (!issue.get('line') && !line)
-    ));
-    issues.forEach(this.renderIssue, this);
-  },
-
-  onIssuesSeverityChange () {
-    const that = this;
-    this.addIssuesPerLineMeta(this.issues);
-    this.$('.source-line-with-issues').each(function () {
-      const line = +$(this).data('line-number');
-      const row = that.model.get('source').find(row => row.line === line);
-      const issue = row.issues[0];
-      $(this).html(`<i class="icon-severity-${issue.severity.toLowerCase()}"></i>`);
-    });
-  },
-
-  highlightUsages (e) {
-    const highlighted = $(e.currentTarget).is('.highlighted');
-    const key = e.currentTarget.className.match(/sym-\d+/);
-    if (key) {
-      this.$('.sym.highlighted').removeClass('highlighted');
-      if (!highlighted) {
-        this.$('.sym.' + key[0]).addClass('highlighted');
-      }
-    }
-  },
-
-  showSCMPopup (e) {
-    e.stopPropagation();
-    $('body').click();
-    const line = +$(e.currentTarget).data('line-number');
-    const row = this.model.get('source').find(row => row.line === line);
-    const popup = new SCMPopupView({
-      triggerEl: $(e.currentTarget),
-      line: row
-    });
-    popup.render();
-  },
-
-  showCoveragePopup (e) {
-    e.stopPropagation();
-    $('body').click();
-    this.clearTooltips();
-    const line = $(e.currentTarget).data('line-number');
-    const row = this.model.get('source').find(row => row.line === line);
-    const url = window.baseUrl + '/api/tests/list';
-    const options = {
-      sourceFileId: this.model.id,
-      sourceFileLineNumber: line,
-      ps: 1000
-    };
-    return $.get(url, options).done(data => {
-      const popup = new CoveragePopupView({
-        line: row,
-        tests: data.tests,
-        triggerEl: $(e.currentTarget)
-      });
-      popup.render();
-    });
-  },
-
-  showDuplications (e) {
-    const that = this;
-    const lineNumber = $(e.currentTarget).closest('.source-line').data('line-number');
-    this.clearTooltips();
-    this.requestDuplications().done(() => {
-      that.render();
-      that.$el.addClass('source-duplications-expanded');
-
-      // immediately show dropdown popup if there is only one duplicated block
-      if (that.model.get('duplications').length === 1) {
-        const dupsBlock = that.$(`.source-line[data-line-number=${lineNumber}]`)
-            .find('.source-line-duplications-extra');
-        dupsBlock.click();
-      }
-    });
-  },
-
-  showDuplicationPopup (e) {
-    e.stopPropagation();
-    $('body').click();
-    this.clearTooltips();
-    const index = $(e.currentTarget).data('index');
-    const line = $(e.currentTarget).data('line-number');
-    let blocks = this.model.get('duplications')[index - 1].blocks;
-    const inRemovedComponent = blocks.some(b => b._ref == null);
-    let foundOne = false;
-    blocks = blocks.filter(b => {
-      const outOfBounds = b.from > line || b.from + b.size < line;
-      const currentFile = b._ref === '1';
-      const shouldDisplayForCurrentFile = outOfBounds || foundOne;
-      const shouldDisplay = !currentFile || shouldDisplayForCurrentFile;
-      const isOk = (b._ref != null) && shouldDisplay;
-      if (b._ref === '1' && !outOfBounds) {
-        foundOne = true;
-      }
-      return isOk;
-    });
-    const popup = new DuplicationPopupView({
-      blocks,
-      inRemovedComponent,
-      component: this.model.toJSON(),
-      files: this.model.get('duplicationFiles'),
-      triggerEl: $(e.currentTarget)
-    });
-    popup.render();
-  },
-
-  onLineIssuesClick (e) {
-    const line = $(e.currentTarget).data('line-number');
-    const issuesList = $(e.currentTarget).parent().find('.issue-list');
-    const areIssuesRendered = issuesList.find('.issue-inner').length > 0;
-    if (issuesList.is('.hidden')) {
-      if (areIssuesRendered) {
-        issuesList.removeClass('hidden');
-      } else {
-        this.showIssuesForLine(line);
-      }
-    } else {
-      issuesList.addClass('hidden');
-    }
-  },
-
-  showLineActionsPopup (e) {
-    e.stopPropagation();
-    $('body').click();
-    const line = $(e.currentTarget).data('line-number');
-    const popup = new LineActionsPopupView({
-      line,
-      triggerEl: $(e.currentTarget),
-      component: this.model.toJSON()
-    });
-    popup.render();
-  },
-
-  onLineNumberClick (e) {
-    const row = $(e.currentTarget).closest('.source-line');
-    const line = row.data('line-number');
-    const highlighted = row.is('.' + HIGHLIGHTED_ROW_CLASS);
-    if (!highlighted) {
-      this.highlightLine(line);
-      this.showLineActionsPopup(e);
-    } else {
-      this.removeHighlighting();
-    }
-  },
-
-  removeHighlighting () {
-    this.highlightedLine = null;
-    this.$('.' + HIGHLIGHTED_ROW_CLASS).removeClass(HIGHLIGHTED_ROW_CLASS);
-  },
-
-  highlightLine (line) {
-    const row = this.$(`.source-line[data-line-number=${line}]`);
-    this.removeHighlighting();
-    this.highlightedLine = line;
-    row.addClass(HIGHLIGHTED_ROW_CLASS);
-    return this;
-  },
-
-  bindScrollEvents () {
-    // no op
-  },
-
-  unbindScrollEvents () {
-    // no op
-  },
-
-  onScroll () {
-    // no op
-  },
-
-  scrollToLine (line) {
-    const row = this.$(`.source-line[data-line-number=${line}]`);
-    if (row.length > 0) {
-      let p = this.$el.scrollParent();
-      if (p.is(document) || p.is('body')) {
-        p = $(window);
-      }
-      const pTopOffset = p.offset() != null ? p.offset().top : 0;
-      const pHeight = p.height();
-      const goal = row.offset().top - pHeight / 3 - pTopOffset;
-      p.scrollTop(goal);
-    }
-    return this;
-  },
-
-  scrollToFirstLine (line) {
-    const row = this.$(`.source-line[data-line-number=${line}]`);
-    if (row.length > 0) {
-      let p = this.$el.scrollParent();
-      if (p.is(document) || p.is('body')) {
-        p = $(window);
-      }
-      const pTopOffset = p.offset() != null ? p.offset().top : 0;
-      const goal = row.offset().top - pTopOffset;
-      p.scrollTop(goal);
-    }
-    return this;
-  },
-
-  scrollToLastLine (line) {
-    const row = this.$(`.source-line[data-line-number=${line}]`);
-    if (row.length > 0) {
-      let p = this.$el.scrollParent();
-      if (p.is(document) || p.is('body')) {
-        p = $(window);
-      }
-      const pTopOffset = p.offset() != null ? p.offset().top : 0;
-      const pHeight = p.height();
-      const goal = row.offset().top - pTopOffset - pHeight + row.height();
-      p.scrollTop(goal);
-    }
-    return this;
-  },
-
-  loadSourceBefore (e) {
-    e.preventDefault();
-    this.unbindScrollEvents();
-    this.$('.js-component-viewer-loading-before').removeClass('hidden');
-    this.$('.js-component-viewer-source-before').addClass('hidden');
-    const that = this;
-    let source = this.model.get('source');
-    const firstLine = source[0].line;
-    const url = window.baseUrl + '/api/sources/lines';
-    const options = {
-      uuid: this.model.id,
-      from: Math.max(1, firstLine - this.LINES_AROUND),
-      to: firstLine - 1
-    };
-    return $.get(url, options).done(data => {
-      source = (data.sources || []).concat(source);
-      if (source.length > that.TOTAL_LINES_LIMIT + 1) {
-        source = source.slice(0, that.TOTAL_LINES_LIMIT);
-        that.model.set({ hasSourceAfter: true });
-      }
-      if (source.length === 0 || (source.length > 0 && source[0].line === 1)) {
-        source.unshift({ line: 0 });
-      }
-      source = source.map(row => {
-        return { ...row, coverageStatus: that.getCoverageStatus(row) };
-      });
-      that.model.set({
-        source,
-        hasCoverage: that.model.hasCoverage(source),
-        hasSourceBefore: data.sources.length === that.LINES_AROUND && source.length > 0 && source[0].line > 0
-      });
-      that.addIssuesPerLineMeta(that.issues);
-      if (that.model.has('duplications')) {
-        that.model.addDuplications(that.model.get('duplications'));
-        that.model.addMeta(that.model.get('duplicationsParsed'));
-      }
-      that.model.checkIfHasDuplications();
-      that.render();
-      that.scrollToFirstLine(firstLine);
-      if (that.model.get('hasSourceBefore') || that.model.get('hasSourceAfter')) {
-        that.bindScrollEvents();
-      }
-    });
-  },
-
-  loadSourceAfter (e) {
-    e.preventDefault();
-    this.unbindScrollEvents();
-    this.$('.js-component-viewer-loading-after').removeClass('hidden');
-    this.$('.js-component-viewer-source-after').addClass('hidden');
-    const that = this;
-    let source = this.model.get('source');
-    const lastLine = source[source.length - 1].line;
-    const url = window.baseUrl + '/api/sources/lines';
-    const options = {
-      uuid: this.model.id,
-      from: lastLine + 1,
-      to: lastLine + this.LINES_AROUND
-    };
-    return $.get(url, options).done(data => {
-      source = source.concat(data.sources);
-      if (source.length > that.TOTAL_LINES_LIMIT + 1) {
-        source = source.slice(source.length - that.TOTAL_LINES_LIMIT);
-        that.model.set({ hasSourceBefore: true });
-      }
-      source = source.map(row => {
-        return { ...row, coverageStatus: that.getCoverageStatus(row) };
-      });
-      that.model.set({
-        source,
-        hasCoverage: that.model.hasCoverage(source),
-        hasSourceAfter: data.sources.length === that.LINES_AROUND
-      });
-      that.addIssuesPerLineMeta(that.issues);
-      if (that.model.has('duplications')) {
-        that.model.addDuplications(that.model.get('duplications'));
-        that.model.addMeta(that.model.get('duplicationsParsed'));
-      }
-      that.model.checkIfHasDuplications();
-      that.render();
-      that.scrollToLastLine(lastLine);
-      if (that.model.get('hasSourceBefore') || that.model.get('hasSourceAfter')) {
-        that.bindScrollEvents();
-      }
-    }).fail(() => {
-      that.model.set({
-        hasSourceAfter: false
-      });
-      that.render();
-      if (that.model.get('hasSourceBefore') || that.model.get('hasSourceAfter')) {
-        that.bindScrollEvents();
-      }
-    });
-  },
-
-  filterLines (func) {
-    const lines = this.model.get('source');
-    const $lines = this.$('.source-line');
-    this.model.set('filterLinesFunc', func);
-    lines.forEach((line, idx) => {
-      const $line = $($lines[idx]);
-      const filtered = func(line) && line.line > 0;
-      $line.toggleClass('source-line-shadowed', !filtered);
-      $line.toggleClass('source-line-filtered', filtered);
-    });
-  },
-
-  filterLinesByDate (date, label) {
-    const sinceDate = moment(date).toDate();
-    this.sinceLabel = label;
-    this.filterLines(line => {
-      const scmDate = moment(line.scmDate).toDate();
-      return scmDate >= sinceDate;
-    });
-  },
-
-  showFilteredTooltip (e) {
-    $(e.currentTarget).tooltip({
-      container: 'body',
-      placement: 'right',
-      title: translateWithParameters('source_viewer.tooltip.new_code', this.sinceLabel),
-      trigger: 'manual'
-    }).tooltip('show');
-  },
-
-  hideFilteredTooltip (e) {
-    $(e.currentTarget).tooltip('destroy');
-  },
-
-  toggleIssueLocations (issue) {
-    if (this.locationsShowFor === issue) {
-      this.hideIssueLocations();
-    } else {
-      this.hideIssueLocations();
-      this.showIssueLocations(issue);
-    }
-  },
-
-  showIssueLocations (issue) {
-    this.locationsShowFor = issue;
-    const primaryLocation = {
-      msg: issue.get('message'),
-      textRange: issue.get('textRange')
-    };
-    let _locations = [primaryLocation];
-    issue.get('flows').forEach(flow => {
-      const flowLocationsCount = Array.isArray(flow.locations) ? flow.locations.length : 0;
-      const flowLocations = flow.locations.map((location, index) => {
-        const _location = { ...location };
-        if (flowLocationsCount > 1) {
-          Object.assign(_location, { index: flowLocationsCount - index });
-        }
-        return _location;
-      });
-      _locations = [].concat(_locations, flowLocations);
-    });
-    _locations.forEach(this.showIssueLocation, this);
-  },
-
-  showIssueLocation (location, index) {
-    if (location && location.textRange) {
-      const line = location.textRange.startLine;
-      const row = this.$(`.source-line-code[data-line-number="${line}"]`);
-
-      if (index > 0 && location.msg) {
-        // render location marker only for
-        // secondary locations and execution flows
-        // and only if message is not empty
-        const renderedFlowLocation = this.renderIssueLocation(location);
-        row.find('.source-line-issue-locations').prepend(renderedFlowLocation);
-      }
-
-      this.highlightIssueLocationInCode(location);
-    }
-  },
-
-  renderIssueLocation (location) {
-    location.msg = location.msg ? location.msg : ' ';
-    return this.issueLocationTemplate(location);
-  },
-
-  highlightIssueLocationInCode (location) {
-    for (let line = location.textRange.startLine; line <= location.textRange.endLine; line++) {
-      const row = this.$(`.source-line-code[data-line-number="${line}"]`);
-
-      // get location for the current line
-      const from = line === location.textRange.startLine ? location.textRange.startOffset : 0;
-      const to = line === location.textRange.endLine ? location.textRange.endOffset : 999999;
-      const _location = { from, to };
-
-      // mark issue location in the source code
-      const codeEl = row.find('.source-line-code-inner > pre');
-      const code = codeEl.html();
-      const newCode = highlightLocations(code, [_location], 'source-line-code-secondary-issue');
-      codeEl.html(newCode);
-    }
-  },
-
-  hideIssueLocations () {
-    this.locationsShowFor = null;
-    this.$('.source-line-issue-locations').empty();
-    this.$('.source-line-code-secondary-issue').removeClass('source-line-code-secondary-issue');
-  }
-});
diff --git a/server/sonar-web/src/main/js/components/source-viewer/measures-overlay.js b/server/sonar-web/src/main/js/components/source-viewer/measures-overlay.js
deleted file mode 100644 (file)
index 4baf170..0000000
+++ /dev/null
@@ -1,284 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import $ from 'jquery';
-import groupBy from 'lodash/groupBy';
-import sortBy from 'lodash/sortBy';
-import toPairs from 'lodash/toPairs';
-import ModalView from '../common/modals';
-import Template from './templates/source-viewer-measures.hbs';
-import { getMeasures } from '../../api/measures';
-import { getMetrics } from '../../api/metrics';
-import { formatMeasure } from '../../helpers/measures';
-
-export default ModalView.extend({
-  template: Template,
-  testsOrder: ['ERROR', 'FAILURE', 'OK', 'SKIPPED'],
-
-  initialize () {
-    this.testsScroll = 0;
-    const requests = [this.requestMeasures(), this.requestIssues()];
-    if (this.model.get('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: '#e6e6e6'
-    };
-
-    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 = d3.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 = d3.svg.arc()
-          .innerRadius(radius - options.thickness)
-          .outerRadius(radius);
-      const pie = d3.layout.pie()
-          .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);
-  },
-
-  getMetrics () {
-    let metrics = '';
-    const url = window.baseUrl + '/api/metrics/search';
-    $.ajax({
-      url,
-      async: false,
-      data: { ps: 9999 }
-    }).done(data => {
-      metrics = data.metrics.filter(metric => metric.type !== 'DATA' && !metric.hidden);
-      metrics = sortBy(metrics, 'name');
-    });
-    return metrics;
-  },
-
-  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);
-    return sortBy(
-      toPairs(groupBy(metrics, 'domain')).map(domain => {
-        return {
-          name: domain[0],
-          metrics: domain[1]
-        };
-      }),
-      'name'
-    );
-  },
-
-  requestMeasures () {
-    return getMetrics().then(metrics => {
-      const metricsToRequest = metrics
-          .filter(metric => metric.type !== 'DATA' && !metric.hidden)
-          .map(metric => metric.key);
-
-      return getMeasures(this.model.key(), metricsToRequest).then(measures => {
-        let nextMeasures = this.model.get('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.model.set({
-          measures: nextMeasures,
-          measuresToDisplay: this.prepareMetrics(metrics)
-        });
-      });
-    });
-  },
-
-  requestIssues () {
-    return new Promise(resolve => {
-      const that = this;
-      const url = window.baseUrl + '/api/issues/search';
-      const options = {
-        componentUuids: this.model.id,
-        resolved: false,
-        ps: 1,
-        facets: 'types,severities,tags'
-      };
-
-      $.get(url, options).done(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 => window.severityComparator(facet.val));
-
-        const tagsFacet = data.facets.find(facet => facet.property === 'tags').values;
-
-        that.model.set({
-          tagsFacet,
-          typesFacet: sortedTypesFacet,
-          severitiesFacet: sortedSeveritiesFacet,
-          issuesCount: data.total
-        });
-
-        resolve();
-      });
-    });
-  },
-
-  requestTests () {
-    return new Promise(resolve => {
-      const that = this;
-      const url = window.baseUrl + '/api/tests/list';
-      const options = { testFileId: this.model.id };
-
-      $.get(url, options).done(data => {
-        that.model.set({ tests: data.tests });
-        that.testSorting = 'status';
-        that.testAsc = true;
-        that.sortTests(test => `${that.testsOrder.indexOf(test.status)}_______${test.name}`);
-        resolve();
-      });
-    });
-  },
-
-  sortTests (condition) {
-    let tests = this.model.get('tests');
-    if (Array.isArray(tests)) {
-      tests = sortBy(tests, condition);
-      if (!this.testAsc) {
-        tests.reverse();
-      }
-      this.model.set({ 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 () {
-    const that = this;
-    if (this.testSorting === 'status') {
-      this.testAsc = !this.testAsc;
-    }
-    this.sortTests(test => `${that.testsOrder.indexOf(test.status)}_______${test.name}`);
-    this.testSorting = 'status';
-    this.render();
-  },
-
-  showTest (e) {
-    const that = this;
-    const testId = $(e.currentTarget).data('id');
-    const url = window.baseUrl + '/api/tests/covered_files';
-    const options = { testId };
-    this.testsScroll = $(e.currentTarget).scrollParent().scrollTop();
-    return $.get(url, options).done(data => {
-      that.coveredFiles = data.files;
-      that.selectedTest = that.model.get('tests').find(test => test.id === testId);
-      that.render();
-    });
-  },
-
-  showAllMeasures () {
-    this.$('.js-all-measures').removeClass('hidden');
-    this.$('.js-show-all-measures').remove();
-  },
-
-  serializeData () {
-    return {
-      ...ModalView.prototype.serializeData.apply(this, arguments),
-      testSorting: this.testSorting,
-      selectedTest: this.selectedTest,
-      coveredFiles: this.coveredFiles || []
-    };
-  }
-});
diff --git a/server/sonar-web/src/main/js/components/source-viewer/more-actions.js b/server/sonar-web/src/main/js/components/source-viewer/more-actions.js
deleted file mode 100644 (file)
index aba02a8..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import $ from 'jquery';
-import Marionette from 'backbone.marionette';
-import Workspace from '../workspace/main';
-import Template from './templates/source-viewer-more-actions.hbs';
-
-export default Marionette.ItemView.extend({
-  className: 'source-viewer-header-more-actions',
-  template: Template,
-
-  events: {
-    'click .js-measures': 'showMeasures',
-    'click .js-new-window': 'openNewWindow',
-    'click .js-workspace': 'openInWorkspace',
-    'click .js-raw-source': 'showRawSource'
-  },
-
-  onRender () {
-    const that = this;
-    $('body').on('click.component-viewer-more-actions', () => {
-      $('body').off('click.component-viewer-more-actions');
-      that.destroy();
-    });
-  },
-
-  showMeasures () {
-    this.options.parent.showMeasures();
-  },
-
-  openNewWindow () {
-    this.options.parent.getPermalink();
-  },
-
-  openInWorkspace () {
-    const key = this.options.parent.model.get('key');
-    Workspace.openComponent({ key });
-  },
-
-  showRawSource () {
-    this.options.parent.showRawSources();
-  },
-
-  serializeData () {
-    const options = this.options.parent.options.viewer.options;
-    return {
-      ...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
-      options
-    };
-  }
-});
diff --git a/server/sonar-web/src/main/js/components/source-viewer/popups/coverage-popup.js b/server/sonar-web/src/main/js/components/source-viewer/popups/coverage-popup.js
deleted file mode 100644 (file)
index 68fd0cc..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import $ from 'jquery';
-import groupBy from 'lodash/groupBy';
-import Popup from '../../common/popup';
-import Workspace from '../../workspace/main';
-import Template from '../templates/source-viewer-coverage-popup.hbs';
-
-export default Popup.extend({
-  template: Template,
-
-  events: {
-    'click a[data-key]': 'goToFile'
-  },
-
-  onRender () {
-    Popup.prototype.onRender.apply(this, arguments);
-    this.$('.bubble-popup-container').isolatedScroll();
-  },
-
-  goToFile (e) {
-    e.stopPropagation();
-    const key = $(e.currentTarget).data('key');
-    Workspace.openComponent({ key });
-  },
-
-  serializeData () {
-    const row = this.options.line || {};
-    const tests = groupBy(this.options.tests, 'fileKey');
-    const testFiles = Object.keys(tests).map(fileKey => {
-      const testSet = tests[fileKey];
-      const test = testSet[0];
-      return {
-        file: {
-          key: test.fileKey,
-          longName: test.fileName
-        },
-        tests: testSet
-      };
-    });
-    return { testFiles, row };
-  }
-});
diff --git a/server/sonar-web/src/main/js/components/source-viewer/popups/duplication-popup.js b/server/sonar-web/src/main/js/components/source-viewer/popups/duplication-popup.js
deleted file mode 100644 (file)
index da54233..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import $ from 'jquery';
-import groupBy from 'lodash/groupBy';
-import sortBy from 'lodash/sortBy';
-import Popup from '../../common/popup';
-import Workspace from '../../workspace/main';
-import Template from '../templates/source-viewer-duplication-popup.hbs';
-
-export default Popup.extend({
-  template: Template,
-
-  events: {
-    'click a[data-key]': 'goToFile'
-  },
-
-  goToFile (e) {
-    e.stopPropagation();
-    const key = $(e.currentTarget).data('key');
-    const line = $(e.currentTarget).data('line');
-    Workspace.openComponent({ key, line });
-  },
-
-  serializeData () {
-    const that = this;
-    const groupedBlocks = groupBy(this.options.blocks, '_ref');
-    let duplications = Object.keys(groupedBlocks).map(fileRef => {
-      return {
-        blocks: groupedBlocks[fileRef],
-        file: this.options.files[fileRef]
-      };
-    });
-    duplications = sortBy(duplications, d => {
-      const a = d.file.projectName !== that.options.component.projectName;
-      const b = d.file.subProjectName !== that.options.component.subProjectName;
-      const c = d.file.key !== that.options.component.key;
-      return '' + a + b + c;
-    });
-    return {
-      duplications,
-      component: this.options.component,
-      inRemovedComponent: this.options.inRemovedComponent
-    };
-  }
-});
diff --git a/server/sonar-web/src/main/js/components/source-viewer/popups/line-actions-popup.js b/server/sonar-web/src/main/js/components/source-viewer/popups/line-actions-popup.js
deleted file mode 100644 (file)
index a2d94f5..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import Popup from '../../common/popup';
-import Template from '../templates/source-viewer-line-options-popup.hbs';
-
-export default Popup.extend({
-  template: Template,
-
-  events: {
-    'click .js-get-permalink': 'getPermalink'
-  },
-
-  getPermalink (e) {
-    e.preventDefault();
-    const { component, line } = this.options;
-    const url = `${window.baseUrl}/component/index?id=${encodeURIComponent(component.key)}&line=${line}`;
-    const windowParams = 'resizable=1,scrollbars=1,status=1';
-    window.open(url, component.name, windowParams);
-  }
-});
diff --git a/server/sonar-web/src/main/js/components/source-viewer/popups/scm-popup.js b/server/sonar-web/src/main/js/components/source-viewer/popups/scm-popup.js
deleted file mode 100644 (file)
index f140e37..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import Popup from '../../common/popup';
-import Template from '../templates/source-viewer-scm-popup.hbs';
-
-export default Popup.extend({
-  template: Template,
-
-  events: {
-    'click': 'onClick'
-  },
-
-  onRender () {
-    Popup.prototype.onRender.apply(this, arguments);
-    this.$('.bubble-popup-container').isolatedScroll();
-  },
-
-  onClick (e) {
-    e.stopPropagation();
-  },
-
-  serializeData () {
-    return {
-      ...Popup.prototype.serializeData.apply(this, arguments),
-      line: this.options.line
-    };
-  }
-});
diff --git a/server/sonar-web/src/main/js/components/source-viewer/source.js b/server/sonar-web/src/main/js/components/source-viewer/source.js
deleted file mode 100644 (file)
index 3cb1198..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import Backbone from 'backbone';
-
-export default Backbone.Model.extend({
-  idAttribute: 'uuid',
-
-  defaults () {
-    return {
-      exist: true,
-
-      hasSource: false,
-      hasCoverage: false,
-      hasDuplications: false,
-      hasSCM: false,
-
-      canSeeCode: true
-    };
-  },
-
-  key () {
-    return this.get('key');
-  },
-
-  addMeta (meta) {
-    const source = this.get('source');
-    let metaIdx = 0;
-    let metaLine = meta[metaIdx];
-    source.forEach(line => {
-      while (metaLine != null && line.line > metaLine.line) {
-        metaLine = meta[++metaIdx];
-      }
-      if (metaLine != null && line.line === metaLine.line) {
-        Object.assign(line, metaLine);
-        metaLine = meta[++metaIdx];
-      }
-    });
-    this.set({ source });
-  },
-
-  addDuplications (duplications) {
-    const source = this.get('source');
-    if (source != null) {
-      source.forEach(line => {
-        const lineDuplications = [];
-        duplications.forEach((d, i) => {
-          let duplicated = false;
-          d.blocks.forEach(b => {
-            if (b._ref === '1') {
-              const lineFrom = b.from;
-              const lineTo = b.from + b.size - 1;
-              if (line.line >= lineFrom && line.line <= lineTo) {
-                duplicated = true;
-              }
-            }
-          });
-          lineDuplications.push(duplicated ? i + 1 : false);
-        });
-        line.duplications = lineDuplications;
-      });
-    }
-    this.set({ source });
-  },
-
-  checkIfHasDuplications () {
-    const source = this.get('source');
-    let hasDuplications = false;
-    if (source != null) {
-      source.forEach(line => {
-        if (line.duplicated) {
-          hasDuplications = true;
-        }
-      });
-    }
-    this.set({ hasDuplications });
-  },
-
-  hasCoverage (source) {
-    return source.some(line => line.coverageStatus != null);
-  }
-});
diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-all.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_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/source-viewer/templates/measures/_source-viewer-measures-coverage.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_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/source-viewer/templates/measures/_source-viewer-measures-duplications.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-duplications.hbs
deleted file mode 100644 (file)
index f6119c3..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="#f3ca8e"></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">Duplications</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/source-viewer/templates/measures/_source-viewer-measures-issues.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_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/source-viewer/templates/measures/_source-viewer-measures-lines.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-lines.hbs
deleted file mode 100644 (file)
index f0c81d0..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">Comments</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/source-viewer/templates/measures/_source-viewer-measures-test-cases.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-test-cases.hbs
deleted file mode 100644 (file)
index 6b0d5a1..0000000
+++ /dev/null
@@ -1,73 +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'}}
-        {{log selectedTest}}
-        <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/source-viewer/templates/measures/_source-viewer-measures-tests.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_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/source-viewer/templates/source-viewer-coverage-popup.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-coverage-popup.hbs
deleted file mode 100644 (file)
index 57c6301..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-<div class="bubble-popup-container">
-  <div class="bubble-popup-title">
-    {{#if row.lineHits}}
-      {{t 'source_viewer.covered'}}
-      {{#if row.conditions}}
-        ({{default row.coveredConditions 0}} of {{row.conditions}} {{t 'source_viewer.conditions'}})
-      {{/if}}
-    {{else}}
-      {{t 'source_viewer.not_covered'}}
-      {{#if row.conditions}}
-        ({{row.conditions}} {{t 'source_viewer.conditions'}})
-      {{/if}}
-    {{/if}}
-  </div>
-
-  {{#each testFiles}}
-    <div class="bubble-popup-section">
-      <a class="component-viewer-popup-test-file link-action" data-key="{{file.key}}" title="{{file.longName}}">
-        <span>{{collapsePath file.longName}}</span>
-      </a>
-      <ul class="bubble-popup-list">
-        {{#each tests}}
-          <li class="component-viewer-popup-test" title="{{name}}">
-            <i class="component-viewer-popup-test-status {{testStatusIconClass status}}"></i>
-            <span class="component-viewer-popup-test-name">
-              <a class="component-viewer-popup-test-file link-action" title="{{name}}"
-                 data-key="{{../file.key}}" data-method="{{name}}">
-                {{name}}
-              </a>
-            </span>
-            <span class="component-viewer-popup-test-duration">{{durationInMs}}ms</span>
-          </li>
-        {{/each}}
-      </ul>
-    </div>
-  {{/each}}
-</div>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-duplication-popup.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-duplication-popup.hbs
deleted file mode 100644 (file)
index ea8fc2b..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-<div class="bubble-popup-container">
-  {{#if inRemovedComponent}}
-    <div class="alert alert-warning spacer-bottom">{{t 'duplications.dups_found_on_deleted_resource'}}</div>
-  {{/if}}
-  {{#notEmpty duplications}}
-    <div class="bubble-popup-title">{{t 'component_viewer.transition.duplication'}}</div>
-    {{#each duplications}}
-      <div class="bubble-popup-section">
-        <div class="component-name">
-          {{#notEqComponents file ../component}}
-            <div class="component-name-parent">
-              <i class="icon-qualifier-trk"></i>&nbsp;<a href="{{dashboardUrl file.project}}">{{file.projectName}}</a>
-            </div>
-            {{#if file.subProjectName}}
-              <div class="component-name-parent">
-                <i class="icon-qualifier-trk"></i>&nbsp;<a
-                  href="{{dashboardUrl file.subProject}}">{{file.subProjectName}}</a>
-              </div>
-            {{/if}}
-          {{/notEqComponents}}
-
-          {{#notEq file.key ../component.key}}
-            <div class="component-name-path">
-              <a class="link-action" data-key="{{file.key}}" title="{{file.name}}">
-                <span>{{collapsedDirFromPath file.name}}</span><span
-                  class="component-name-file">{{fileFromPath file.name}}</span>
-              </a>
-            </div>
-          {{/notEq}}
-
-          <div class="component-name-path">
-            Lines:
-            {{#joinEach blocks ','}}
-              <a class="link-action" data-key="{{../file.key}}" data-line="{{this.from}}">
-                {{this.from}} – {{sum from size -1}}
-              </a>
-            {{/joinEach}}
-          </div>
-        </div>
-      </div>
-    {{/each}}
-  {{/notEmpty}}
-</div>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-header.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-header.hbs
deleted file mode 100644 (file)
index a453235..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-<div class="source-viewer-header-component">
-  <div class="component-name">
-
-    {{#unless removed}}
-      {{#if projectName}}
-        <div class="component-name-parent">
-          {{qualifierIcon 'TRK'}}&nbsp;<a href="{{dashboardUrl project}}">{{projectName}}</a>
-        </div>
-        {{#if subProjectName}}
-          <div class="component-name-parent">
-            {{qualifierIcon 'TRK'}}&nbsp;<a href="{{dashboardUrl subProject}}">{{subProjectName}}</a>
-          </div>
-        {{/if}}
-      {{/if}}
-
-      <div class="component-name-path">
-        {{qualifierIcon q}}&nbsp;<span>{{collapsedDirFromPath path}}</span><span class="component-name-file">{{fileFromPath path}}</span>
-
-        {{#if canMarkAsFavorite}}
-          <a class="js-favorite component-name-favorite {{#if fav}}icon-favorite{{else}}icon-not-favorite{{/if}}"
-             title="{{#if fav}}{{t 'click_to_remove_from_favorites'}}{{else}}{{t 'click_to_add_to_favorites'}}{{/if}}">
-          </a>
-        {{/if}}
-      </div>
-    {{else}}
-      <div class="source-viewer-header-component-project removed">{{removedMessage}}</div>
-    {{/unless}}
-  </div>
-</div>
-
-{{#unless removed}}
-  <a class="js-actions source-viewer-header-actions icon-list" title="{{t 'component_viewer.more_actions'}}"></a>
-
-  <div class="source-viewer-header-measures">
-    {{#if isUnitTest}}
-      <div class="source-viewer-header-measure">
-        <span class="source-viewer-header-measure-value">{{formatMeasure measures.tests 'SHORT_INT'}}</span>
-        <span class="source-viewer-header-measure-label">{{t 'metric.tests.name'}}</span>
-      </div>
-    {{/if}}
-
-    {{#unless isUnitTest}}
-      <div class="source-viewer-header-measure">
-        <span class="source-viewer-header-measure-value">{{formatMeasure measures.lines 'SHORT_INT'}}</span>
-        <span class="source-viewer-header-measure-label">{{t 'metric.lines.name'}}</span>
-      </div>
-    {{/unless}}
-
-    <div class="source-viewer-header-measure">
-      <span class="source-viewer-header-measure-value">
-        <a class="source-viewer-header-external-link" target="_blank"
-           href="{{link '/issues/search#resolved=false|fileUuids=' uuid}}">
-          {{#if measures.issues}}{{formatMeasure measures.issues 'SHORT_INT'}}{{else}}0{{/if}}&nbsp;<i class="icon-detach"></i>
-        </a>
-      </span>
-      <span class="source-viewer-header-measure-label">{{t 'metric.violations.name'}}</span>
-    </div>
-
-    {{#notNull measures.coverage}}
-      <div class="source-viewer-header-measure">
-        <span class="source-viewer-header-measure-value">{{formatMeasure measures.coverage 'PERCENT'}}</span>
-        <span class="source-viewer-header-measure-label">{{t 'metric.coverage.name'}}</span>
-      </div>
-    {{/notNull}}
-
-    {{#notNull measures.duplicationDensity}}
-      <div class="source-viewer-header-measure">
-        <span class="source-viewer-header-measure-value">{{formatMeasure measures.duplicationDensity 'PERCENT'}}</span>
-        <span class="source-viewer-header-measure-label">{{t 'duplications'}}</span>
-      </div>
-    {{/notNull}}
-
-  </div>
-{{/unless}}
diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-issue-location.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-issue-location.hbs
deleted file mode 100644 (file)
index 181e85b..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<div class="source-viewer-issue-location" title="{{msg}}">
-  {{#if index}}<strong>{{index}}: </strong>{{/if}}
-  {{limitString msg}}
-</div>
diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-line-options-popup.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-line-options-popup.hbs
deleted file mode 100644 (file)
index c6b9b41..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-<div class="bubble-popup-container">
-  <div class="bubble-popup-section">
-    <a href="#" class="js-get-permalink link-action">{{t 'component_viewer.get_permalink'}}</a>
-  </div>
-</div>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-measures.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-measures.hbs
deleted file mode 100644 (file)
index 0df0763..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">
-        {{> 'measures/_source-viewer-measures-tests'}}
-      </div>
-    </div>
-    <div class="source-viewer-measures">
-      {{> '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">
-          {{> 'measures/_source-viewer-measures-lines'}}
-        </div>
-      </div>
-
-      <div class="source-viewer-measures-section">
-        {{> 'measures/_source-viewer-measures-issues'}}
-      </div>
-
-      {{#if measures.coverage}}
-        <div class="source-viewer-measures-section">
-          <div class="source-viewer-measures-card">
-            {{> 'measures/_source-viewer-measures-coverage'}}
-          </div>
-        </div>
-      {{/if}}
-
-      <div class="source-viewer-measures-section">
-        {{> 'measures/_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">
-    {{> 'measures/_source-viewer-measures-all'}}
-  </div>
-</div>
-
-<div class="modal-foot">
-  <a class="js-modal-close" href="#">{{t 'close'}}</a>
-</div>
diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-more-actions.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-more-actions.hbs
deleted file mode 100644 (file)
index c5a8541..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-<a class="js-measures">{{t 'component_viewer.show_details'}}</a>
-<br>
-<a class="js-new-window">{{t 'component_viewer.new_window'}}</a>
-{{#unless options.workspace}}
-  <br>
-  <a class="js-workspace">{{t 'component_viewer.open_in_workspace'}}</a>
-{{/unless}}
-<br>
-<a class="js-raw-source">{{t 'component_viewer.show_raw_source'}}</a>
diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-scm-popup.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-scm-popup.hbs
deleted file mode 100644 (file)
index dd82aca..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-<div class="bubble-popup-container">
-  <div class="bubble-popup-section">
-    {{line.scmAuthor}}
-  </div>
-  <div class="bubble-popup-section">
-    {{dt line.scmDate}}
-  </div>
-  {{#if line.scmRevision}}
-    <div class="bubble-popup-section">
-      {{line.scmRevision}}
-    </div>
-  {{/if}}
-</div>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer.hbs
deleted file mode 100644 (file)
index ede6aed..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-<div class="source-viewer-header"></div>
-
-{{#if canSeeCode}}
-
-  {{#if exist}}
-
-    {{#if hasSourceBefore}}
-      <div class="source-viewer-more-code">
-        <button class="js-component-viewer-source-before">
-          {{t 'source_viewer.load_more_code'}}
-        </button>
-        <div class="js-component-viewer-loading-before hidden">
-          <i class="spinner"></i>
-          <span class="note spacer-left">{{t 'source_viewer.loading_more_code'}}</span>
-        </div>
-      </div>
-    {{/if}}
-
-    <table class="source-table">
-      {{#eachWithPrevious source}}
-        <tr class="source-line {{#eq line 0}}{{#empty issues}}hidden{{/empty}}{{/eq}}" {{#if line}}data-line-number="{{line}}"{{/if}}>
-          <td class="source-meta source-line-number" {{#if line}}data-line-number="{{line}}"{{/if}}></td>
-
-          <td class="source-meta source-line-scm" {{#if line}}data-line-number="{{line}}"{{/if}}>
-            {{#ifSCMChanged2 this _previous}}
-              <div class="source-line-scm-inner" data-author="{{scmAuthor}}"></div>
-            {{/ifSCMChanged2}}
-          </td>
-
-          {{#if ../hasCoverage}}
-            <td class="source-meta source-line-coverage {{#notNull coverageStatus}}source-line-{{coverageStatus}}{{/notNull}}"
-                data-line-number="{{line}}" {{#notNull coverageStatus}}title="{{t 'source_viewer.tooltip' coverageStatus}}" data-placement="right" data-toggle="tooltip"{{/notNull}}>
-              <div class="source-line-bar"></div>
-            </td>
-          {{/if}}
-
-          {{#if ../hasDuplications}}
-            <td class="source-meta source-line-duplications {{#if duplicated}}source-line-duplicated{{/if}}"
-              {{#if duplicated}}title="{{t 'source_viewer.tooltip.duplicated_line'}}" data-placement="right" data-toggle="tooltip"{{/if}}>
-              <div class="source-line-bar"></div>
-            </td>
-
-            {{#each duplications}}
-              <td class="source-meta source-line-duplications-extra {{#if this}}source-line-duplicated{{/if}}"
-                  data-index="{{this}}" data-line-number="{{../line}}"
-                {{#if this}}title="{{t 'source_viewer.tooltip.duplicated_block'}}" data-placement="right" data-toggle="tooltip"{{/if}}>
-                <div class="source-line-bar"></div>
-              </td>
-            {{/each}}
-          {{/if}}
-
-          <td class="source-meta source-line-issues {{#notEmpty issues}}source-line-with-issues{{/notEmpty}}"
-              data-line-number="{{line}}">
-            {{#withFirst issues}}
-              {{severityIcon severity}}
-            {{/withFirst}}
-            {{#ifLengthGT issues 1}}
-              <span class="source-line-issues-counter">{{length issues}}</span>
-            {{/ifLengthGT}}
-          </td>
-
-          <td class="source-meta source-line-filtered-container" data-line-number="{{line}}">
-            <div class="source-line-bar"></div>
-          </td>
-
-          <td class="source-line-code code {{#notEmpty issues}}has-issues{{/notEmpty}}" data-line-number="{{line}}">
-            <div class="source-line-code-inner">
-              {{#notNull code}}
-                <pre>{{{codeWithIssueLocations code issueLocations}}}</pre>
-              {{/notNull}}
-
-              <div class="source-line-issue-locations"></div>
-            </div>
-
-            {{#notEmpty issues}}
-              <div class="issue-list">
-                {{#each issues}}
-                  <div class="issue" id="issue-{{key}}"></div>
-                {{/each}}
-              </div>
-            {{/notEmpty}}
-          </td>
-        </tr>
-      {{/eachWithPrevious}}
-    </table>
-
-    {{#if hasSourceAfter}}
-      <div class="source-viewer-more-code">
-        <button class="js-component-viewer-source-after">
-          {{t 'source_viewer.load_more_code'}}
-        </button>
-        <div class="js-component-viewer-loading-after hidden">
-          <i class="spinner"></i>
-          <span class="note spacer-left">{{t 'source_viewer.loading_more_code'}}</span>
-        </div>
-      </div>
-    {{/if}}
-
-  {{else}}
-
-    {{! does not exist }}
-    <div class="alert alert-warning spacer-top">{{t 'component_viewer.no_component'}}</div>
-
-  {{/if}}
-
-{{else}}
-
-  {{! can't see code }}
-  <div class="alert alert-warning spacer-top">{{t 'code_viewer.no_source_code_displayed_due_to_security'}}</div>
-
-{{/if}}
index 7ab96e7c683ccf5bb6130b1052aac3d98b38e567..188b08cab605f3d763ef4bdb5d3876e7bd30aeba 100644 (file)
@@ -21,7 +21,7 @@ import $ from 'jquery';
 import React from 'react';
 import { render } from 'react-dom';
 import BaseView from './base-viewer-view';
-import SourceViewer from '../../SourceViewer/StandaloneSourceViewer';
+import SourceViewer from '../../SourceViewer/SourceViewer';
 import Template from '../templates/workspace-viewer.hbs';
 import WithStore from '../../shared/WithStore';