aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main
diff options
context:
space:
mode:
authorStas Vilchik <stas-vilchik@users.noreply.github.com>2017-03-08 09:30:24 +0100
committerGitHub <noreply@github.com>2017-03-08 09:30:24 +0100
commitdba5bc28b25fe68ab6965d5f43c6f2a8c706e42d (patch)
treec24768c167fe8745808d4e3c23d05387a5860c41 /server/sonar-web/src/main
parent8b65d7659da13196cf26e2dacf1137c17f3b7a9d (diff)
downloadsonarqube-dba5bc28b25fe68ab6965d5f43c6f2a8c706e42d.tar.gz
sonarqube-dba5bc28b25fe68ab6965d5f43c6f2a8c706e42d.zip
Polish new source viewer (#1755)
Diffstat (limited to 'server/sonar-web/src/main')
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/App.js2
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js2
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js2
-rw-r--r--server/sonar-web/src/main/js/apps/component/components/App.js2
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/App.js2
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js23
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js217
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js135
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js103
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssueLocations.js16
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js410
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/Line.js152
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js224
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js59
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js63
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js59
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js61
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicatorContainer.js (renamed from server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewer.js)30
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js (renamed from server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewerBase.js)36
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.js (renamed from server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssuesIndicator.js)36
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.js61
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js50
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js48
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js43
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js39
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.js46
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js36
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.js39
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.js55
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap34
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap38
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap25
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap21
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap37
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap13
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.js.snap13
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.js.snap43
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js37
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js13
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js8
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js15
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js (renamed from server/sonar-web/src/main/js/components/source-viewer/popups/coverage-popup.js)4
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js (renamed from server/sonar-web/src/main/js/components/source-viewer/popups/duplication-popup.js)4
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js (renamed from server/sonar-web/src/main/js/components/source-viewer/popups/line-actions-popup.js)2
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/popups/scm-popup.js (renamed from server/sonar-web/src/main/js/components/source-viewer/popups/scm-popup.js)2
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-coverage-popup.hbs (renamed from server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-coverage-popup.hbs)0
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-duplication-popup.hbs (renamed from server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-duplication-popup.hbs)0
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-line-options-popup.hbs (renamed from server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-line-options-popup.hbs)0
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-scm-popup.hbs (renamed from server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-scm-popup.hbs)0
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/styles.css6
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js (renamed from server/sonar-web/src/main/js/components/source-viewer/measures-overlay.js)66
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-all.hbs (renamed from server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-all.hbs)0
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-coverage.hbs (renamed from server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-coverage.hbs)0
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-duplications.hbs (renamed from server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-duplications.hbs)0
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-issues.hbs (renamed from server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-issues.hbs)0
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-lines.hbs (renamed from server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-lines.hbs)0
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-test-cases.hbs (renamed from server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-test-cases.hbs)0
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-tests.hbs (renamed from server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-tests.hbs)0
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/views/templates/source-viewer-measures.hbs (renamed from server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-measures.hbs)14
-rw-r--r--server/sonar-web/src/main/js/components/__tests__/source-viewer-test.js105
-rw-r--r--server/sonar-web/src/main/js/components/issue/issue-view.js3
-rw-r--r--server/sonar-web/src/main/js/components/source-viewer/header.js88
-rw-r--r--server/sonar-web/src/main/js/components/source-viewer/helpers/code-with-issue-locations-helper.js133
-rw-r--r--server/sonar-web/src/main/js/components/source-viewer/main.js790
-rw-r--r--server/sonar-web/src/main/js/components/source-viewer/more-actions.js68
-rw-r--r--server/sonar-web/src/main/js/components/source-viewer/source.js98
-rw-r--r--server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-header.hbs74
-rw-r--r--server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-issue-location.hbs4
-rw-r--r--server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-more-actions.hbs9
-rw-r--r--server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer.hbs111
-rw-r--r--server/sonar-web/src/main/js/components/workspace/views/viewer-view.js2
71 files changed, 1701 insertions, 2230 deletions
diff --git a/server/sonar-web/src/main/js/apps/code/components/App.js b/server/sonar-web/src/main/js/apps/code/components/App.js
index 4208e8d409b..8e75483c1da 100644
--- a/server/sonar-web/src/main/js/apps/code/components/App.js
+++ b/server/sonar-web/src/main/js/apps/code/components/App.js
@@ -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';
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js
index bbfc0ae32bf..6f52f65713e 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js
@@ -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 {
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js
index fb0bb744bd0..4c6e64c94e4 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js
@@ -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 {
diff --git a/server/sonar-web/src/main/js/apps/component/components/App.js b/server/sonar-web/src/main/js/apps/component/components/App.js
index 041625d8243..735e89a21d1 100644
--- a/server/sonar-web/src/main/js/apps/component/components/App.js
+++ b/server/sonar-web/src/main/js/apps/component/components/App.js
@@ -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: {
diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.js b/server/sonar-web/src/main/js/apps/overview/components/App.js
index 91e636eb52a..c30c80f2630 100644
--- a/server/sonar-web/src/main/js/apps/overview/components/App.js
+++ b/server/sonar-web/src/main/js/apps/overview/components/App.js
@@ -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}/>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js
index 2c6e5b594a6..e7b999e3231 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js
@@ -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));
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
index 4a011e163c3..96e7641e525 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
@@ -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>
);
}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js
index fb98c06ab15..d0b5eff25b0 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js
@@ -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>
);
}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js
index 14dedd85572..af05c46d954 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js
@@ -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>
);
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssueLocations.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssueLocations.js
index d4881347372..abe72568770 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssueLocations.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssueLocations.js
@@ -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/SourceViewerLine.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js
deleted file mode 100644
index 5a53275c7af..00000000000
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js
+++ /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/components/Line.js b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js
new file mode 100644
index 00000000000..c9da31d1cf8
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js
@@ -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
index 00000000000..944922c0fe8
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js
@@ -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
index 00000000000..55e7aad96cd
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js
@@ -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
index 00000000000..021be8e1e7c
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js
@@ -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
index 00000000000..941227de0dc
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js
@@ -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
index 00000000000..200174283cc
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js
@@ -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/StandaloneSourceViewer.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicatorContainer.js
index d673bd44dd0..8d15af06288 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewer.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicatorContainer.js
@@ -19,29 +19,11 @@
*/
// @flow
import { connect } from 'react-redux';
-import StandaloneSourceViewerBase from './StandaloneSourceViewerBase';
-import { receiveFavorites } from '../../store/favorites/duck';
-import { receiveIssues } from '../../store/issues/duck';
+import LineIssuesIndicator from './LineIssuesIndicator';
+import { getIssueByKey } from '../../../store/rootReducer';
-const mapStateToProps = null;
+const mapStateToProps = (state, ownProps: { issueKeys: Array<string> }) => ({
+ issues: ownProps.issueKeys.map(issueKey => getIssueByKey(state, issueKey))
+});
-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);
+export default connect(mapStateToProps)(LineIssuesIndicator);
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js
index ea28e00b36f..0238b021891 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewerBase.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js
@@ -19,32 +19,30 @@
*/
// @flow
import React from 'react';
-import SourceViewerBase from './SourceViewerBase';
+import ConnectedIssue from '../../issue/ConnectedIssue';
-type State = {
+type Props = {
+ issueKeys: Array<string>,
+ onIssueClick: (issueKey: string) => void,
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 });
- };
+export default class LineIssuesList extends React.PureComponent {
+ props: Props;
render () {
+ const { issueKeys, onIssueClick, selectedIssue } = this.props;
+
return (
- <SourceViewerBase
- {...this.props}
- onIssueSelect={this.handleIssueSelect}
- onIssueUnselect={this.handleIssueUnselect}
- selectedIssue={this.state.selectedIssue}/>
+ <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/SourceViewerIssuesIndicator.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.js
index f6993949244..a477bfbad6b 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssuesIndicator.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.js
@@ -19,26 +19,32 @@
*/
// @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';
+import type { SourceLine } from '../types';
-class SourceViewerIssuesIndicator extends React.Component {
- props: {
- issue: { severity: string }
+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 (
- <SeverityIcon severity={this.props.issue.severity}/>
+ <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}/>
);
}
}
-
-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/components/LineSCM.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.js
new file mode 100644
index 00000000000..b856b23bb53
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.js
@@ -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
index 00000000000..eedeb69252e
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js
@@ -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
index 00000000000..5dcee39a040
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js
@@ -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
index 00000000000..e16dd8b6c0b
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js
@@ -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
index 00000000000..1f11c8b9e37
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+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
index 00000000000..c2bb88ec66b
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.js
@@ -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
index 00000000000..8f60222fb55
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js
@@ -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
index 00000000000..eb120a25a06
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.js
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+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
index 00000000000..f1a812d302a
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.js
@@ -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
index 00000000000..ecf619bfa06
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap
@@ -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
index 00000000000..ccf5c4d3c4f
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap
@@ -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
index 00000000000..b94d4b3bc09
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap
@@ -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
index 00000000000..7e977c88442
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap
@@ -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
index 00000000000..a945f7600ad
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap
@@ -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
index 00000000000..9279cc173b3
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap
@@ -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
index 00000000000..a14778f764a
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.js.snap
@@ -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
index 00000000000..34828c8c1ef
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js
index c0ca46bb9d1..c448c519b56 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js
@@ -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('');
};
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js
index dcfe2f273fa..13b2926d4ca 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js
@@ -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]);
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js
index 70af97af1a5..54459e3534b 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js
@@ -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,
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js
index ddc2963c0e7..3a9d00566ce 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js
@@ -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/source-viewer/popups/coverage-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js
index 68fd0ccc388..145d5dbeb47 100644
--- a/server/sonar-web/src/main/js/components/source-viewer/popups/coverage-popup.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js
@@ -20,8 +20,7 @@
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';
+import Template from './templates/source-viewer-coverage-popup.hbs';
export default Popup.extend({
template: Template,
@@ -38,6 +37,7 @@ export default Popup.extend({
goToFile (e) {
e.stopPropagation();
const key = $(e.currentTarget).data('key');
+ const Workspace = require('../../workspace/main').default;
Workspace.openComponent({ key });
},
diff --git a/server/sonar-web/src/main/js/components/source-viewer/popups/duplication-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js
index da542333a30..d8ef03e1009 100644
--- a/server/sonar-web/src/main/js/components/source-viewer/popups/duplication-popup.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js
@@ -21,8 +21,7 @@ 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';
+import Template from './templates/source-viewer-duplication-popup.hbs';
export default Popup.extend({
template: Template,
@@ -35,6 +34,7 @@ export default Popup.extend({
e.stopPropagation();
const key = $(e.currentTarget).data('key');
const line = $(e.currentTarget).data('line');
+ const Workspace = require('../../workspace/main').default;
Workspace.openComponent({ key, line });
},
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/SourceViewer/popups/line-actions-popup.js
index a2d94f568b8..e65d748e0d1 100644
--- a/server/sonar-web/src/main/js/components/source-viewer/popups/line-actions-popup.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js
@@ -18,7 +18,7 @@
* 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';
+import Template from './templates/source-viewer-line-options-popup.hbs';
export default Popup.extend({
template: Template,
diff --git a/server/sonar-web/src/main/js/components/source-viewer/popups/scm-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/scm-popup.js
index f140e37c56b..06cbf45e182 100644
--- a/server/sonar-web/src/main/js/components/source-viewer/popups/scm-popup.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/scm-popup.js
@@ -18,7 +18,7 @@
* 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';
+import Template from './templates/source-viewer-scm-popup.hbs';
export default Popup.extend({
template: Template,
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/SourceViewer/popups/templates/source-viewer-coverage-popup.hbs
index 57c6301119e..57c6301119e 100644
--- a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-coverage-popup.hbs
+++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-coverage-popup.hbs
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/SourceViewer/popups/templates/source-viewer-duplication-popup.hbs
index ea8fc2b2349..ea8fc2b2349 100644
--- a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-duplication-popup.hbs
+++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-duplication-popup.hbs
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/SourceViewer/popups/templates/source-viewer-line-options-popup.hbs
index c6b9b418866..c6b9b418866 100644
--- 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/SourceViewer/popups/templates/source-viewer-line-options-popup.hbs
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/SourceViewer/popups/templates/source-viewer-scm-popup.hbs
index dd82aca528c..dd82aca528c 100644
--- a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-scm-popup.hbs
+++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-scm-popup.hbs
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/styles.css b/server/sonar-web/src/main/js/components/SourceViewer/styles.css
index 6371f9e8cb3..bd79e0fd79c 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/styles.css
+++ b/server/sonar-web/src/main/js/components/SourceViewer/styles.css
@@ -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/source-viewer/measures-overlay.js b/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js
index 4baf170a2e8..6dee58b4c80 100644
--- a/server/sonar-web/src/main/js/components/source-viewer/measures-overlay.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js
@@ -21,11 +21,11 @@ import $ from 'jquery';
import groupBy from 'lodash/groupBy';
import sortBy from 'lodash/sortBy';
import toPairs from 'lodash/toPairs';
-import ModalView from '../common/modals';
+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';
+import { getMeasures } from '../../../api/measures';
+import { getMetrics } from '../../../api/metrics';
+import { formatMeasure } from '../../../helpers/measures';
export default ModalView.extend({
template: Template,
@@ -34,7 +34,7 @@ export default ModalView.extend({
initialize () {
this.testsScroll = 0;
const requests = [this.requestMeasures(), this.requestIssues()];
- if (this.model.get('q') === 'UTS') {
+ if (this.options.component.q === 'UTS') {
requests.push(this.requestTests());
}
Promise.all(requests).then(() => this.render());
@@ -150,8 +150,8 @@ export default ModalView.extend({
.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') || {};
+ 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);
@@ -159,20 +159,17 @@ export default ModalView.extend({
metric.value = nextMeasures[metric.key];
});
nextMeasures = this.calcAdditionalMeasures(nextMeasures);
- this.model.set({
- measures: nextMeasures,
- measuresToDisplay: this.prepareMetrics(metrics)
- });
+ this.measures = nextMeasures;
+ this.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,
+ componentKeys: this.options.component.key,
resolved: false,
ps: 1,
facets: 'types,severities,tags'
@@ -188,12 +185,10 @@ export default ModalView.extend({
const tagsFacet = data.facets.find(facet => facet.property === 'tags').values;
- that.model.set({
- tagsFacet,
- typesFacet: sortedTypesFacet,
- severitiesFacet: sortedSeveritiesFacet,
- issuesCount: data.total
- });
+ this.tagsFacet = tagsFacet;
+ this.typesFacet = sortedTypesFacet;
+ this.severitiesFacet = sortedSeveritiesFacet;
+ this.issuesCount = data.total;
resolve();
});
@@ -202,28 +197,27 @@ export default ModalView.extend({
requestTests () {
return new Promise(resolve => {
- const that = this;
const url = window.baseUrl + '/api/tests/list';
- const options = { testFileId: this.model.id };
+ const options = { testFileKey: this.options.component.key };
$.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}`);
+ 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.model.get('tests');
+ let tests = this.tests;
if (Array.isArray(tests)) {
tests = sortBy(tests, condition);
if (!this.testAsc) {
tests.reverse();
}
- this.model.set({ tests });
+ this.tests = tests;
}
},
@@ -246,25 +240,23 @@ export default ModalView.extend({
},
sortTestsByStatus () {
- const that = this;
if (this.testSorting === 'status') {
this.testAsc = !this.testAsc;
}
- this.sortTests(test => `${that.testsOrder.indexOf(test.status)}_______${test.name}`);
+ this.sortTests(test => `${this.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();
+ this.coveredFiles = data.files;
+ this.selectedTest = this.tests.find(test => test.id === testId);
+ this.render();
});
},
@@ -276,6 +268,14 @@ export default ModalView.extend({
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/source-viewer/templates/measures/_source-viewer-measures-all.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-all.hbs
index cac99fea52d..cac99fea52d 100644
--- 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/SourceViewer/views/templates/_source-viewer-measures-all.hbs
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/SourceViewer/views/templates/_source-viewer-measures-coverage.hbs
index 2598e962d77..2598e962d77 100644
--- 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/SourceViewer/views/templates/_source-viewer-measures-coverage.hbs
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/SourceViewer/views/templates/_source-viewer-measures-duplications.hbs
index f6119c38fb6..f6119c38fb6 100644
--- 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/SourceViewer/views/templates/_source-viewer-measures-duplications.hbs
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/SourceViewer/views/templates/_source-viewer-measures-issues.hbs
index 30a39146750..30a39146750 100644
--- 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/SourceViewer/views/templates/_source-viewer-measures-issues.hbs
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/SourceViewer/views/templates/_source-viewer-measures-lines.hbs
index f0c81d0349b..f0c81d0349b 100644
--- 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/SourceViewer/views/templates/_source-viewer-measures-lines.hbs
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/SourceViewer/views/templates/_source-viewer-measures-test-cases.hbs
index 6b0d5a110a6..6b0d5a110a6 100644
--- 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/SourceViewer/views/templates/_source-viewer-measures-test-cases.hbs
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/SourceViewer/views/templates/_source-viewer-measures-tests.hbs
index c9b33c392ac..c9b33c392ac 100644
--- 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/SourceViewer/views/templates/_source-viewer-measures-tests.hbs
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/SourceViewer/views/templates/source-viewer-measures.hbs
index 0df076390c9..ebcc2e79b13 100644
--- a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-measures.hbs
+++ b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/source-viewer-measures.hbs
@@ -22,34 +22,34 @@
{{#eq q 'UTS'}}
<div class="source-viewer-measures">
<div class="source-viewer-measures-section">
- {{> 'measures/_source-viewer-measures-tests'}}
+ {{> '_source-viewer-measures-tests'}}
</div>
</div>
<div class="source-viewer-measures">
- {{> 'measures/_source-viewer-measures-test-cases'}}
+ {{> '_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'}}
+ {{> '_source-viewer-measures-lines'}}
</div>
</div>
<div class="source-viewer-measures-section">
- {{> 'measures/_source-viewer-measures-issues'}}
+ {{> '_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'}}
+ {{> '_source-viewer-measures-coverage'}}
</div>
</div>
{{/if}}
<div class="source-viewer-measures-section">
- {{> 'measures/_source-viewer-measures-duplications'}}
+ {{> '_source-viewer-measures-duplications'}}
</div>
</div>
{{/eq}}
@@ -59,7 +59,7 @@
<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'}}
+ {{> '_source-viewer-measures-all'}}
</div>
</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
index 7b04a17d6fc..00000000000
--- a/server/sonar-web/src/main/js/components/__tests__/source-viewer-test.js
+++ /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);
- });
-});
-
diff --git a/server/sonar-web/src/main/js/components/issue/issue-view.js b/server/sonar-web/src/main/js/components/issue/issue-view.js
index cdd7d95de95..0bb7fbd1b31 100644
--- a/server/sonar-web/src/main/js/components/issue/issue-view.js
+++ b/server/sonar-web/src/main/js/components/issue/issue-view.js
@@ -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
index 739037e5198..00000000000
--- a/server/sonar-web/src/main/js/components/source-viewer/header.js
+++ /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
index 4b0307c0569..00000000000
--- a/server/sonar-web/src/main/js/components/source-viewer/helpers/code-with-issue-locations-helper.js
+++ /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
index 8b1725d09f3..00000000000
--- a/server/sonar-web/src/main/js/components/source-viewer/main.js
+++ /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/more-actions.js b/server/sonar-web/src/main/js/components/source-viewer/more-actions.js
deleted file mode 100644
index aba02a8e1de..00000000000
--- a/server/sonar-web/src/main/js/components/source-viewer/more-actions.js
+++ /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/source.js b/server/sonar-web/src/main/js/components/source-viewer/source.js
deleted file mode 100644
index 3cb1198e328..00000000000
--- a/server/sonar-web/src/main/js/components/source-viewer/source.js
+++ /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/source-viewer-header.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-header.hbs
deleted file mode 100644
index a4532354481..00000000000
--- a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-header.hbs
+++ /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
index 181e85b2497..00000000000
--- a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-issue-location.hbs
+++ /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-more-actions.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-more-actions.hbs
deleted file mode 100644
index c5a85413424..00000000000
--- a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-more-actions.hbs
+++ /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.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer.hbs
deleted file mode 100644
index ede6aeddde8..00000000000
--- a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer.hbs
+++ /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}}
diff --git a/server/sonar-web/src/main/js/components/workspace/views/viewer-view.js b/server/sonar-web/src/main/js/components/workspace/views/viewer-view.js
index 7ab96e7c683..188b08cab60 100644
--- a/server/sonar-web/src/main/js/components/workspace/views/viewer-view.js
+++ b/server/sonar-web/src/main/js/components/workspace/views/viewer-view.js
@@ -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';